diff --git a/.editorconfig b/.editorconfig index 1cf10e7865..947b0dbf9e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,10 @@ indent_size = 2 [*.py] indent_style = space -[/tgui/**/*.{js,styl,ract,json,html}] +[/tgui/**/*.{js,ts,tsx,jsx,scss,json,html}] +indent_style = space +indent_size = 2 + +[*.ts] indent_style = space indent_size = 2 diff --git a/.github/workflows/ci_suite.yml b/.github/workflows/ci_suite.yml index 93a9dc45b2..028c3b8a70 100644 --- a/.github/workflows/ci_suite.yml +++ b/.github/workflows/ci_suite.yml @@ -74,7 +74,7 @@ jobs: run: | ls -h $HOME/ source $HOME/BYOND/byond/bin/byondsetup - DreamMaker roguetown.dme + tools/build/build.sh - name: Check for errors id: check-errors diff --git a/.gitignore b/.gitignore index 57ad6d71b5..ea823cbec1 100644 --- a/.gitignore +++ b/.gitignore @@ -55,14 +55,14 @@ __pycache__/ # Distribution / packaging .Python env/ -build/ +/build/ develop-eggs/ dist/ downloads/ eggs/ +/lib/ +/lib64/ .eggs/ -lib/ -lib64/ parts/ sdist/ var/ @@ -197,6 +197,7 @@ Temporary Items *.vscode/* !/.vscode/extensions.json !/.vscode/launch.json +!/.vscode/tasks.json tools/MapAtmosFixer/MapAtmosFixer/obj/* tools/MapAtmosFixer/MapAtmosFixer/bin/* @@ -227,3 +228,5 @@ tools/MapAtmosFixer/MapAtmosFixer/bin/* !/config/title_music/sounds/title1.ogg /config/title_screens/images/* !/config/title_screens/images/exclude + +*.test.dme diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..2111c71e8d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,86 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "process", + "command": "tools/build/build.sh", + "windows": { + "command": ".\\tools\\build\\build.bat" + }, + "options": { + "env": { + "DM_EXE": "${config:dreammaker.byondPath}" + } + }, + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], + "group": { + "kind": "build", + "isDefault": true + }, + "dependsOn": "dm: reparse", + "label": "Build All" + }, + { + "type": "dreammaker", + "dme": "tgstation.dme", + "problemMatcher": ["$dreammaker"], + "group": "build", + "label": "dm: build - tgstation.dme" + }, + { + "command": "${command:dreammaker.reparse}", + "group": "build", + "label": "dm: reparse" + }, + { + "type": "shell", + "command": "bin/tgui-build", + "windows": { + "command": ".\\bin\\tgui-build.cmd" + }, + "problemMatcher": ["$tsc", "$eslint-stylish"], + "group": "build", + "label": "tgui: build" + }, + { + "type": "shell", + "command": "bin/tgui-dev", + "windows": { + "command": ".\\bin\\tgui-dev.cmd" + }, + "problemMatcher": ["$tsc", "$eslint-stylish"], + "group": "build", + "label": "tgui: dev server" + }, + { + "type": "shell", + "command": "bin/tgfont", + "windows": { + "command": ".\\bin\\tgfont.cmd" + }, + "problemMatcher": ["$tsc", "$eslint-stylish"], + "group": "build", + "label": "tgui: rebuild tgfont" + }, + { + "type": "process", + "command": "tools/build/build.sh", + "args": ["-DLOWMEMORYMODE"], + "windows": { + "command": ".\\tools\\build\\build.bat", + "args": ["-DLOWMEMORYMODE"] + }, + "options": { + "env": { + "DM_EXE": "${config:dreammaker.byondPath}" + } + }, + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], + "group": { + "kind": "build" + }, + "dependsOn": "dm: reparse", + "label": "Build All (low memory mode)" + }, + ] +} diff --git a/BUILD.cmd b/BUILD.cmd new file mode 100644 index 0000000000..dc791f60c9 --- /dev/null +++ b/BUILD.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\tools\build\build.bat" --wait-on-error build %* diff --git a/README.md b/README.md index 9670cdaa90..d114fc37be 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,15 @@ This is the codebase for Solaris Ridge; a new high-fantasy take on [Space Statio **Please note that this repository contains sexually explicit content and is not suitable for those under the age of 18.** ## Compilation -At the moment, Solaris Ridge **does not use juke build**. In order to build the repository, you presently need to compile it within DreamMaker! + +**The quick way**. Find `bin/server.cmd` in this folder and double click it to automatically build and host the server on port 1337. + +**The long way**. Find `bin/build.cmd` in this folder, and double click it to initiate the build. It consists of multiple steps and might take around 1-5 minutes to compile. If it closes, it means it has finished its job. You can then [setup the server](.github/guides/RUNNING_A_SERVER.md) normally by opening `tgstation.dmb` in DreamDaemon. + +**Building tgstation in DreamMaker directly is deprecated and might produce errors**, such as `'tgui.bundle.js': cannot find file`. + +**[How to compile in VSCode and other build options](tools/build/README.md).** + ## LICENSE diff --git a/bin/build.cmd b/bin/build.cmd new file mode 100644 index 0000000000..98c2ef45e1 --- /dev/null +++ b/bin/build.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error build %* diff --git a/bin/clean.cmd b/bin/clean.cmd new file mode 100644 index 0000000000..8eacd92ebd --- /dev/null +++ b/bin/clean.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error clean-all %* diff --git a/bin/server.cmd b/bin/server.cmd new file mode 100644 index 0000000000..c6e6642baf --- /dev/null +++ b/bin/server.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error server %* diff --git a/bin/test.cmd b/bin/test.cmd new file mode 100644 index 0000000000..a76a9c6745 --- /dev/null +++ b/bin/test.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error dm-test %* diff --git a/bin/tgfont.cmd b/bin/tgfont.cmd new file mode 100644 index 0000000000..b768c81d65 --- /dev/null +++ b/bin/tgfont.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error tg-font %* diff --git a/bin/tgui-bench.cmd b/bin/tgui-bench.cmd new file mode 100644 index 0000000000..333115f795 --- /dev/null +++ b/bin/tgui-bench.cmd @@ -0,0 +1,3 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error tgui-bench %* +pause diff --git a/bin/tgui-build.cmd b/bin/tgui-build.cmd new file mode 100644 index 0000000000..7804fc6daa --- /dev/null +++ b/bin/tgui-build.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error tgui tgui-lint tgui-test %* diff --git a/bin/tgui-dev.cmd b/bin/tgui-dev.cmd new file mode 100644 index 0000000000..25ff3495d4 --- /dev/null +++ b/bin/tgui-dev.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error tgui-dev %* diff --git a/bin/tgui-sonar.cmd b/bin/tgui-sonar.cmd new file mode 100644 index 0000000000..e083f65362 --- /dev/null +++ b/bin/tgui-sonar.cmd @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\..\tools\build\build.bat" --wait-on-error tgui-sonar %* diff --git a/code/__DEFINES/_helpers.dm b/code/__DEFINES/_helpers.dm new file mode 100644 index 0000000000..9d8a9d7ab7 --- /dev/null +++ b/code/__DEFINES/_helpers.dm @@ -0,0 +1,2 @@ +/// Takes a datum as input, returns its ref string +#define text_ref(datum) ref(datum) diff --git a/code/__DEFINES/dcs/signals/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum.dm index 534b51e801..21c697e9dd 100644 --- a/code/__DEFINES/dcs/signals/signals_datum.dm +++ b/code/__DEFINES/dcs/signals/signals_datum.dm @@ -4,3 +4,5 @@ #define COMSIG_PREQDELETED "parent_preqdeleted" /// just before a datum's Destroy() is called: (force), at this point none of the other components chose to interrupt qdel and Destroy will be called #define COMSIG_QDELETING "parent_qdeleting" +/// from datum ui_act (usr, action) +#define COMSIG_UI_ACT "COMSIG_UI_ACT" diff --git a/code/__DEFINES/dcs/signals/signals_tgui.dm b/code/__DEFINES/dcs/signals/signals_tgui.dm new file mode 100644 index 0000000000..67b979be78 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_tgui.dm @@ -0,0 +1,2 @@ +/// Window is fully visible and we can make fragile calls +#define COMSIG_TGUI_WINDOW_VISIBLE "tgui_window_visible" diff --git a/code/__DEFINES/interaction_flags.dm b/code/__DEFINES/interaction_flags.dm index a85ffb8d7a..1ec0d450e7 100644 --- a/code/__DEFINES/interaction_flags.dm +++ b/code/__DEFINES/interaction_flags.dm @@ -16,6 +16,9 @@ #define INTERACT_ATOM_NO_FINGERPRINT_ATTACK_HAND (1<<7) /// adds hiddenprints instead of fingerprints on interact #define INTERACT_ATOM_NO_FINGERPRINT_INTERACT (1<<8) +/// ignores mobility check +#define INTERACT_ATOM_IGNORE_MOBILITY (1<<9) + /// attempt pickup on attack_hand for items #define INTERACT_ITEM_ATTACK_HAND_PICKUP (1<<0) diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index e2071413eb..9d1796ba17 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -108,8 +108,6 @@ GLOBAL_LIST_INIT(our_forest_sex, typecacheof(list( #define isslime(A) (istype(A, /mob/living/simple_animal/slime)) -#define isdrone(A) (istype(A, /mob/living/simple_animal/drone)) - #define iscat(A) (istype(A, /mob/living/simple_animal/pet/cat)) #define isdog(A) (istype(A, /mob/living/simple_animal/pet/dog)) diff --git a/code/__DEFINES/rust_g.dm b/code/__DEFINES/rust_g.dm index f63cf10da4..65597d6b3b 100644 --- a/code/__DEFINES/rust_g.dm +++ b/code/__DEFINES/rust_g.dm @@ -19,39 +19,175 @@ /* This comment bypasses grep checks */ /var/__rust_g /proc/__detect_rust_g() + var/arch_suffix = null + #ifdef OPENDREAM + arch_suffix = "64" + #endif if (world.system_type == UNIX) - if (fexists("./librust_g.so")) + if (fexists("./librust_g[arch_suffix].so")) // No need for LD_LIBRARY_PATH badness. - return __rust_g = "./librust_g.so" - else if (fexists("./rust_g")) + return __rust_g = "./librust_g[arch_suffix].so" + else if (fexists("./rust_g[arch_suffix]")) // Old dumb filename. - return __rust_g = "./rust_g" - else if (fexists("[world.GetConfig("env", "HOME")]/.byond/bin/rust_g")) + return __rust_g = "./rust_g[arch_suffix]" + else if (fexists("[world.GetConfig("env", "HOME")]/.byond/bin/rust_g[arch_suffix]")) // Old dumb filename in `~/.byond/bin`. - return __rust_g = "rust_g" + return __rust_g = "rust_g[arch_suffix]" else // It's not in the current directory, so try others - return __rust_g = "librust_g.so" + return __rust_g = "librust_g[arch_suffix].so" else - return __rust_g = "rust_g" + return __rust_g = "rust_g[arch_suffix]" #define RUST_G (__rust_g || __detect_rust_g()) #endif -#define RUSTG_JOB_NO_RESULTS_YET "NO RESULTS YET" -#define RUSTG_JOB_NO_SUCH_JOB "NO SUCH JOB" -#define RUSTG_JOB_ERROR "JOB PANICKED" +// Handle 515 call() -> call_ext() changes +#if DM_VERSION >= 515 +#define RUSTG_CALL call_ext +#else +#define RUSTG_CALL call +#endif + +/// Gets the version of rust_g +/proc/rustg_get_version() return RUSTG_CALL(RUST_G, "get_version")() + + +/** + * Sets up the Aho-Corasick automaton with its default options. + * + * The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call + * Arguments: + * * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls + * * patterns - A non-associative list of strings to search for + * * replacements - Default replacements for this automaton, used with rustg_acreplace + */ +#define rustg_setup_acreplace(key, patterns, replacements) RUSTG_CALL(RUST_G, "setup_acreplace")(key, json_encode(patterns), json_encode(replacements)) + +/** + * Sets up the Aho-Corasick automaton using supplied options. + * + * The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call + * Arguments: + * * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls + * * options - An associative list like list("anchored" = 0, "ascii_case_insensitive" = 0, "match_kind" = "Standard"). The values shown on the example are the defaults, and default values may be omitted. See the identically named methods at https://docs.rs/aho-corasick/latest/aho_corasick/struct.AhoCorasickBuilder.html to see what the options do. + * * patterns - A non-associative list of strings to search for + * * replacements - Default replacements for this automaton, used with rustg_acreplace + */ +#define rustg_setup_acreplace_with_options(key, options, patterns, replacements) RUSTG_CALL(RUST_G, "setup_acreplace")(key, json_encode(options), json_encode(patterns), json_encode(replacements)) + +/** + * Run the specified replacement engine with the provided haystack text to replace, returning replaced text. + * + * Arguments: + * * key - The key for the automaton + * * text - Text to run replacements on + */ +#define rustg_acreplace(key, text) RUSTG_CALL(RUST_G, "acreplace")(key, text) + +/** + * Run the specified replacement engine with the provided haystack text to replace, returning replaced text. + * + * Arguments: + * * key - The key for the automaton + * * text - Text to run replacements on + * * replacements - Replacements for this call. Must be the same length as the set-up patterns + */ +#define rustg_acreplace_with_replacements(key, text, replacements) RUSTG_CALL(RUST_G, "acreplace_with_replacements")(key, text, json_encode(replacements)) + +/** + * This proc generates a cellular automata noise grid which can be used in procedural generation methods. + * + * Returns a single string that goes row by row, with values of 1 representing an alive cell, and a value of 0 representing a dead cell. + * + * Arguments: + * * percentage: The chance of a turf starting closed + * * smoothing_iterations: The amount of iterations the cellular automata simulates before returning the results + * * birth_limit: If the number of neighboring cells is higher than this amount, a cell is born + * * death_limit: If the number of neighboring cells is lower than this amount, a cell dies + * * width: The width of the grid. + * * height: The height of the grid. + */ +#define rustg_cnoise_generate(percentage, smoothing_iterations, birth_limit, death_limit, width, height) \ + RUSTG_CALL(RUST_G, "cnoise_generate")(percentage, smoothing_iterations, birth_limit, death_limit, width, height) + +/** + * This proc generates a grid of perlin-like noise + * + * Returns a single string that goes row by row, with values of 1 representing an turned on cell, and a value of 0 representing a turned off cell. + * + * Arguments: + * * seed: seed for the function + * * accuracy: how close this is to the original perlin noise, as accuracy approaches infinity, the noise becomes more and more perlin-like + * * stamp_size: Size of a singular stamp used by the algorithm, think of this as the same stuff as frequency in perlin noise + * * world_size: size of the returned grid. + * * lower_range: lower bound of values selected for. (inclusive) + * * upper_range: upper bound of values selected for. (exclusive) + */ +#define rustg_dbp_generate(seed, accuracy, stamp_size, world_size, lower_range, upper_range) \ + RUSTG_CALL(RUST_G, "dbp_generate")(seed, accuracy, stamp_size, world_size, lower_range, upper_range) + + +#define rustg_dmi_strip_metadata(fname) RUSTG_CALL(RUST_G, "dmi_strip_metadata")(fname) +#define rustg_dmi_create_png(path, width, height, data) RUSTG_CALL(RUST_G, "dmi_create_png")(path, width, height, data) +#define rustg_dmi_resize_png(path, width, height, resizetype) RUSTG_CALL(RUST_G, "dmi_resize_png")(path, width, height, resizetype) +/** + * input: must be a path, not an /icon; you have to do your own handling if it is one, as icon objects can't be directly passed to rustg. + * + * output: json_encode'd list. json_decode to get a flat list with icon states in the order they're in inside the .dmi + */ +#define rustg_dmi_icon_states(fname) RUSTG_CALL(RUST_G, "dmi_icon_states")(fname) + +#define rustg_file_read(fname) RUSTG_CALL(RUST_G, "file_read")(fname) +#define rustg_file_exists(fname) (RUSTG_CALL(RUST_G, "file_exists")(fname) == "true") +#define rustg_file_write(text, fname) RUSTG_CALL(RUST_G, "file_write")(text, fname) +#define rustg_file_append(text, fname) RUSTG_CALL(RUST_G, "file_append")(text, fname) +#define rustg_file_get_line_count(fname) text2num(RUSTG_CALL(RUST_G, "file_get_line_count")(fname)) +#define rustg_file_seek_line(fname, line) RUSTG_CALL(RUST_G, "file_seek_line")(fname, "[line]") + +#ifdef RUSTG_OVERRIDE_BUILTINS + #define file2text(fname) rustg_file_read("[fname]") + #define text2file(text, fname) rustg_file_append(text, "[fname]") +#endif -#define rustg_dmi_strip_metadata(fname) call_ext(RUST_G, "dmi_strip_metadata")(fname) -#define rustg_dmi_create_png(path, width, height, data) call_ext(RUST_G, "dmi_create_png")(path, width, height, data) +/// Returns the git hash of the given revision, ex. "HEAD". +#define rustg_git_revparse(rev) RUSTG_CALL(RUST_G, "rg_git_revparse")(rev) -#define rustg_noise_get_at_coordinates(seed, x, y) call_ext(RUST_G, "noise_get_at_coordinates")(seed, x, y) +/** + * Returns the date of the given revision using the provided format. + * Defaults to returning %F which is YYYY-MM-DD. + */ +/proc/rustg_git_commit_date(rev, format = "%F") + return RUSTG_CALL(RUST_G, "rg_git_commit_date")(rev, format) -#define rustg_git_revparse(rev) call_ext(RUST_G, "rg_git_revparse")(rev) -#define rustg_git_commit_date(rev) call_ext(RUST_G, "rg_git_commit_date")(rev) +/** + * Returns the formatted datetime string of HEAD using the provided format. + * Defaults to returning %F which is YYYY-MM-DD. + * This is different to rustg_git_commit_date because it only needs the logs directory. + */ +/proc/rustg_git_commit_date_head(format = "%F") + return RUSTG_CALL(RUST_G, "rg_git_commit_date_head")(format) -#define rustg_log_write(fname, text, format) call_ext(RUST_G, "log_write")(fname, text, format) -/proc/rustg_log_close_all() return call_ext(RUST_G, "log_close_all")() +#define rustg_hash_string(algorithm, text) RUSTG_CALL(RUST_G, "hash_string")(algorithm, text) +#define rustg_hash_file(algorithm, fname) RUSTG_CALL(RUST_G, "hash_file")(algorithm, fname) +#define rustg_hash_generate_totp(seed) RUSTG_CALL(RUST_G, "generate_totp")(seed) +#define rustg_hash_generate_totp_tolerance(seed, tolerance) RUSTG_CALL(RUST_G, "generate_totp_tolerance")(seed, tolerance) + +#define RUSTG_HASH_MD5 "md5" +#define RUSTG_HASH_SHA1 "sha1" +#define RUSTG_HASH_SHA256 "sha256" +#define RUSTG_HASH_SHA512 "sha512" +#define RUSTG_HASH_XXH64 "xxh64" +#define RUSTG_HASH_BASE64 "base64" + +/// Encode a given string into base64 +#define rustg_encode_base64(str) rustg_hash_string(RUSTG_HASH_BASE64, str) +/// Decode a given base64 string +#define rustg_decode_base64(str) RUSTG_CALL(RUST_G, "decode_base64")(str) + +#ifdef RUSTG_OVERRIDE_BUILTINS + #define md5(thing) (isfile(thing) ? rustg_hash_file(RUSTG_HASH_MD5, "[thing]") : rustg_hash_string(RUSTG_HASH_MD5, thing)) +#endif #define RUSTG_HTTP_METHOD_GET "get" #define RUSTG_HTTP_METHOD_PUT "put" @@ -59,13 +195,306 @@ #define RUSTG_HTTP_METHOD_PATCH "patch" #define RUSTG_HTTP_METHOD_HEAD "head" #define RUSTG_HTTP_METHOD_POST "post" -#define rustg_http_request_blocking(method, url, body, headers) call_ext(RUST_G, "http_request_blocking")(method, url, body, headers) -#define rustg_http_request_async(method, url, body, headers) call_ext(RUST_G, "http_request_async")(method, url, body, headers) -#define rustg_http_check_request(req_id) call_ext(RUST_G, "http_check_request")(req_id) - -#define rustg_sql_connect_pool(options) call_ext(RUST_G, "sql_connect_pool")(options) -#define rustg_sql_query_async(handle, query, params) call_ext(RUST_G, "sql_query_async")(handle, query, params) -#define rustg_sql_query_blocking(handle, query, params) call_ext(RUST_G, "sql_query_blocking")(handle, query, params) -#define rustg_sql_connected(handle) call_ext(RUST_G, "sql_connected")(handle) -#define rustg_sql_disconnect_pool(handle) call_ext(RUST_G, "sql_disconnect_pool")(handle) -#define rustg_sql_check_query(job_id) call_ext(RUST_G, "sql_check_query")("[job_id]") +#define rustg_http_request_blocking(method, url, body, headers, options) RUSTG_CALL(RUST_G, "http_request_blocking")(method, url, body, headers, options) +#define rustg_http_request_async(method, url, body, headers, options) RUSTG_CALL(RUST_G, "http_request_async")(method, url, body, headers, options) +#define rustg_http_check_request(req_id) RUSTG_CALL(RUST_G, "http_check_request")(req_id) + +/// Generates a spritesheet at: [file_path][spritesheet_name]_[size_id].png +/// The resulting spritesheet arranges icons in a random order, with the position being denoted in the "sprites" return value. +/// All icons have the same y coordinate, and their x coordinate is equal to `icon_width * position`. +/// +/// hash_icons is a boolean (0 or 1), and determines if the generator will spend time creating hashes for the output field dmi_hashes. +/// These hashes can be heplful for 'smart' caching (see rustg_iconforge_cache_valid), but require extra computation. +/// +/// Spritesheet will contain all sprites listed within "sprites". +/// "sprites" format: +/// list( +/// "sprite_name" = list( // <--- this list is a [SPRITE_OBJECT] +/// icon_file = 'icons/path_to/an_icon.dmi', +/// icon_state = "some_icon_state", +/// dir = SOUTH, +/// frame = 1, +/// transform = list([TRANSFORM_OBJECT], ...) +/// ), +/// ..., +/// ) +/// TRANSFORM_OBJECT format: +/// list("type" = RUSTG_ICONFORGE_BLEND_COLOR, "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) +/// list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY) +/// list("type" = RUSTG_ICONFORGE_SCALE, "width" = 32, "height" = 32) +/// list("type" = RUSTG_ICONFORGE_CROP, "x1" = 1, "y1" = 1, "x2" = 32, "y2" = 32) // (BYOND icons index from 1,1 to the upper bound, inclusive) +/// +/// Returns a SpritesheetResult as JSON, containing fields: +/// list( +/// "sizes" = list("32x32", "64x64", ...), +/// "sprites" = list("sprite_name" = list("size_id" = "32x32", "position" = 0), ...), +/// "dmi_hashes" = list("icons/path_to/an_icon.dmi" = "d6325c5b4304fb03", ...), +/// "sprites_hash" = "a2015e5ff403fb5c", // This is the xxh64 hash of the INPUT field "sprites". +/// "error" = "[A string, empty if there were no errors.]" +/// ) +/// In the case of an unrecoverable panic from within Rust, this function ONLY returns a string containing the error. +#define rustg_iconforge_generate(file_path, spritesheet_name, sprites, hash_icons) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites, "[hash_icons]") +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites, "[hash_icons]") +/// Returns the status of an async job_id, or its result if it is completed. See RUSTG_JOB DEFINEs. +#define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]") +/// Clears all cached DMIs and images, freeing up memory. +/// This should be used after spritesheets are done being generated. +#define rustg_iconforge_cleanup RUSTG_CALL(RUST_G, "iconforge_cleanup") +/// Takes in a set of hashes, generate inputs, and DMI filepaths, and compares them to determine cache validity. +/// input_hash: xxh64 hash of "sprites" from the cache. +/// dmi_hashes: xxh64 hashes of the DMIs in a spritesheet, given by `rustg_iconforge_generate` with `hash_icons` enabled. From the cache. +/// sprites: The new input that will be passed to rustg_iconforge_generate(). +/// Returns a CacheResult with the following structure: list( +/// "result": "1" (if cache is valid) or "0" (if cache is invalid) +/// "fail_reason": "" (emtpy string if valid, otherwise a string containing the invalidation reason or an error with ERROR: prefixed.) +/// ) +/// In the case of an unrecoverable panic from within Rust, this function ONLY returns a string containing the error. +#define rustg_iconforge_cache_valid(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid")(input_hash, dmi_hashes, sprites) +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid_async")(input_hash, dmi_hashes, sprites) +/// Provided a /datum/greyscale_config typepath, JSON string containing the greyscale config, and path to a DMI file containing the base icons, +/// Loads that config into memory for later use by rustg_iconforge_gags(). The config_path is the unique identifier used later. +/// JSON Config schema: https://hackmd.io/@tgstation/GAGS-Layer-Types +/// Unsupported features: color_matrix layer type, 'or' blend_mode. May not have BYOND parity with animated icons or varying dirs between layers. +/// Returns "OK" if successful, otherwise, returns a string containing the error. +#define rustg_iconforge_load_gags_config(config_path, config_json, config_icon_path) RUSTG_CALL(RUST_G, "iconforge_load_gags_config")("[config_path]", config_json, config_icon_path) +/// Given a config_path (previously loaded by rustg_iconforge_load_gags_config), and a string of hex colors formatted as "#ff00ff#ffaa00" +/// Outputs a DMI containing all of the states within the config JSON to output_dmi_path, creating any directories leading up to it if necessary. +/// Returns "OK" if successful, otherwise, returns a string containing the error. +#define rustg_iconforge_gags(config_path, colors, output_dmi_path) RUSTG_CALL(RUST_G, "iconforge_gags")("[config_path]", colors, output_dmi_path) +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_load_gags_config_async(config_path, config_json, config_icon_path) RUSTG_CALL(RUST_G, "iconforge_load_gags_config_async")("[config_path]", config_json, config_icon_path) +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_gags_async(config_path, colors, output_dmi_path) RUSTG_CALL(RUST_G, "iconforge_gags_async")("[config_path]", colors, output_dmi_path) + +#define RUSTG_ICONFORGE_BLEND_COLOR "BlendColor" +#define RUSTG_ICONFORGE_BLEND_ICON "BlendIcon" +#define RUSTG_ICONFORGE_CROP "Crop" +#define RUSTG_ICONFORGE_SCALE "Scale" + +#define RUSTG_JOB_NO_RESULTS_YET "NO RESULTS YET" +#define RUSTG_JOB_NO_SUCH_JOB "NO SUCH JOB" +#define RUSTG_JOB_ERROR "JOB PANICKED" + +#define rustg_json_is_valid(text) (RUSTG_CALL(RUST_G, "json_is_valid")(text) == "true") + +#define rustg_log_write(fname, text, format) RUSTG_CALL(RUST_G, "log_write")(fname, text, format) +/proc/rustg_log_close_all() return RUSTG_CALL(RUST_G, "log_close_all")() + +#define rustg_noise_get_at_coordinates(seed, x, y) RUSTG_CALL(RUST_G, "noise_get_at_coordinates")(seed, x, y) + +/** + * Generates a 2D poisson disk distribution ('blue noise'), which is relatively uniform. + * + * params: + * `seed`: str + * `width`: int, width of the noisemap (see world.maxx) + * `length`: int, height of the noisemap (see world.maxy) + * `radius`: int, distance between points on the noisemap + * + * returns: + * a width*length length string of 1s and 0s representing a 2D poisson sample collapsed into a 1D string + */ +#define rustg_noise_poisson_map(seed, width, length, radius) RUSTG_CALL(RUST_G, "noise_poisson_map")(seed, width, length, radius) + +/** + * Register a list of nodes into a rust library. This list of nodes must have been serialized in a json. + * Node {// Index of this node in the list of nodes + * unique_id: usize, + * // Position of the node in byond + * x: usize, + * y: usize, + * z: usize, + * // Indexes of nodes connected to this one + * connected_nodes_id: Vec} + * It is important that the node with the unique_id 0 is the first in the json, unique_id 1 right after that, etc. + * It is also important that all unique ids follow. {0, 1, 2, 4} is not a correct list and the registering will fail + * Nodes should not link across z levels. + * A node cannot link twice to the same node and shouldn't link itself either + */ +#define rustg_register_nodes_astar(json) RUSTG_CALL(RUST_G, "register_nodes_astar")(json) + +/** + * Add a new node to the static list of nodes. Same rule as registering_nodes applies. + * This node unique_id must be equal to the current length of the static list of nodes + */ +#define rustg_add_node_astar(json) RUSTG_CALL(RUST_G, "add_node_astar")(json) + +/** + * Remove every link to the node with unique_id. Replace that node by null + */ +#define rustg_remove_node_astar(unique_id) RUSTG_CALL(RUST_G, "remove_node_astar")("[unique_id]") + +/** + * Compute the shortest path between start_node and goal_node using A*. Heuristic used is simple geometric distance + */ +#define rustg_generate_path_astar(start_node_id, goal_node_id) RUSTG_CALL(RUST_G, "generate_path_astar")("[start_node_id]", "[goal_node_id]") + +#define RUSTG_REDIS_ERROR_CHANNEL "RUSTG_REDIS_ERROR_CHANNEL" + +#define rustg_redis_connect(addr) RUSTG_CALL(RUST_G, "redis_connect")(addr) +/proc/rustg_redis_disconnect() return RUSTG_CALL(RUST_G, "redis_disconnect")() +#define rustg_redis_subscribe(channel) RUSTG_CALL(RUST_G, "redis_subscribe")(channel) +/proc/rustg_redis_get_messages() return RUSTG_CALL(RUST_G, "redis_get_messages")() +#define rustg_redis_publish(channel, message) RUSTG_CALL(RUST_G, "redis_publish")(channel, message) + +/** + * Connects to a given redis server. + * + * Arguments: + * * addr - The address of the server, for example "redis://127.0.0.1/" + */ +#define rustg_redis_connect_rq(addr) RUSTG_CALL(RUST_G, "redis_connect_rq")(addr) +/** + * Disconnects from a previously connected redis server + */ +/proc/rustg_redis_disconnect_rq() return RUSTG_CALL(RUST_G, "redis_disconnect_rq")() +/** + * https://redis.io/commands/lpush/ + * + * Arguments + * * key (string) - The key to use + * * elements (list) - The elements to push, use a list even if there's only one element. + */ +#define rustg_redis_lpush(key, elements) RUSTG_CALL(RUST_G, "redis_lpush")(key, json_encode(elements)) +/** + * https://redis.io/commands/lrange/ + * + * Arguments + * * key (string) - The key to use + * * start (string) - The zero-based index to start retrieving at + * * stop (string) - The zero-based index to stop retrieving at (inclusive) + */ +#define rustg_redis_lrange(key, start, stop) RUSTG_CALL(RUST_G, "redis_lrange")(key, start, stop) +/** + * https://redis.io/commands/lpop/ + * + * Arguments + * * key (string) - The key to use + * * count (string|null) - The amount to pop off the list, pass null to omit (thus just 1) + * + * Note: `count` was added in Redis version 6.2.0 + */ +#define rustg_redis_lpop(key, count) RUSTG_CALL(RUST_G, "redis_lpop")(key, count) + +/* + * Takes in a string and json_encode()"d lists to produce a sanitized string. + * This function operates on whitelists, there is currently no way to blacklist. + * Args: + * * text: the string to sanitize. + * * attribute_whitelist_json: a json_encode()'d list of HTML attributes to allow in the final string. + * * tag_whitelist_json: a json_encode()'d list of HTML tags to allow in the final string. + */ +#define rustg_sanitize_html(text, attribute_whitelist_json, tag_whitelist_json) RUSTG_CALL(RUST_G, "sanitize_html")(text, attribute_whitelist_json, tag_whitelist_json) + +/// Provided a static RSC file path or a raw text file path, returns the duration of the file in deciseconds as a float. +/proc/rustg_sound_length(file_path) + var/static/list/sound_cache + if(isnull(sound_cache)) + sound_cache = list() + + . = 0 + + if(!istext(file_path)) + if(!isfile(file_path)) + CRASH("rustg_sound_length error: Passed non-text object") + + if(length("[file_path]")) // Runtime generated RSC references stringify into 0-length strings. + file_path = "[file_path]" + else + CRASH("rustg_sound_length does not support non-static file refs.") + + var/cached_length = sound_cache[file_path] + if(!isnull(cached_length)) + return cached_length + + var/ret = RUSTG_CALL(RUST_G, "sound_len")(file_path) + var/as_num = text2num(ret) + if(isnull(ret)) + . = 0 + CRASH("rustg_sound_length error: [ret]") + + sound_cache[file_path] = as_num + return as_num + + +#define RUSTG_SOUNDLEN_SUCCESSES "successes" +#define RUSTG_SOUNDLEN_ERRORS "errors" +/** + * Returns a nested key-value list containing "successes" and "errors" + * The format is as follows: + * list( + * RUSTG_SOUNDLEN_SUCCESES = list("sounds/test.ogg" = 25.34), + * RUSTG_SOUNDLEN_ERRORS = list("sound/bad.png" = "SoundLen: Unable to decode file."), + *) +*/ +#define rustg_sound_length_list(file_paths) json_decode(RUSTG_CALL(RUST_G, "sound_len_list")(json_encode(file_paths))) + +#define rustg_sql_connect_pool(options) RUSTG_CALL(RUST_G, "sql_connect_pool")(options) +#define rustg_sql_query_async(handle, query, params) RUSTG_CALL(RUST_G, "sql_query_async")(handle, query, params) +#define rustg_sql_query_blocking(handle, query, params) RUSTG_CALL(RUST_G, "sql_query_blocking")(handle, query, params) +#define rustg_sql_connected(handle) RUSTG_CALL(RUST_G, "sql_connected")(handle) +#define rustg_sql_disconnect_pool(handle) RUSTG_CALL(RUST_G, "sql_disconnect_pool")(handle) +#define rustg_sql_check_query(job_id) RUSTG_CALL(RUST_G, "sql_check_query")("[job_id]") + +#define rustg_time_microseconds(id) text2num(RUSTG_CALL(RUST_G, "time_microseconds")(id)) +#define rustg_time_milliseconds(id) text2num(RUSTG_CALL(RUST_G, "time_milliseconds")(id)) +#define rustg_time_reset(id) RUSTG_CALL(RUST_G, "time_reset")(id) + +/// Returns the current timestamp (in local time), formatted with the given format string. +/// See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for documentation on the formatting syntax. +#define rustg_formatted_timestamp(format) RUSTG_CALL(RUST_G, "formatted_timestamp")(format) + +/// Returns the current timestamp (with the given UTC offset in hours), formatted with the given format string. +/// See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for documentation on the formatting syntax. +#define rustg_formatted_timestamp_tz(format, offset) RUSTG_CALL(RUST_G, "formatted_timestamp")(format, offset) + +/// Returns the timestamp as a string +/proc/rustg_unix_timestamp() + return RUSTG_CALL(RUST_G, "unix_timestamp")() + +#define rustg_raw_read_toml_file(path) json_decode(RUSTG_CALL(RUST_G, "toml_file_to_json")(path) || "null") + +/proc/rustg_read_toml_file(path) + var/list/output = rustg_raw_read_toml_file(path) + if (output["success"]) + return json_decode(output["content"]) + else + CRASH(output["content"]) + +#define rustg_raw_toml_encode(value) json_decode(RUSTG_CALL(RUST_G, "toml_encode")(json_encode(value))) + +/proc/rustg_toml_encode(value) + var/list/output = rustg_raw_toml_encode(value) + if (output["success"]) + return output["content"] + else + CRASH(output["content"]) + +#define rustg_unzip_download_async(url, unzip_directory) RUSTG_CALL(RUST_G, "unzip_download_async")(url, unzip_directory) +#define rustg_unzip_check(job_id) RUSTG_CALL(RUST_G, "unzip_check")("[job_id]") + +#define rustg_url_encode(text) RUSTG_CALL(RUST_G, "url_encode")("[text]") +#define rustg_url_decode(text) RUSTG_CALL(RUST_G, "url_decode")(text) + +#ifdef RUSTG_OVERRIDE_BUILTINS + #define url_encode(text) rustg_url_encode(text) + #define url_decode(text) rustg_url_decode(text) +#endif + +/** + * This proc generates a noise grid using worley noise algorithm + * + * Returns a single string that goes row by row, with values of 1 representing an alive cell, and a value of 0 representing a dead cell. + * + * Arguments: + * * region_size: The size of regions + * * threshold: the value that determines wether a cell is dead or alive + * * node_per_region_chance: chance of a node existiing in a region + * * size: size of the returned grid + * * node_min: minimum amount of nodes in a region (after the node_per_region_chance is applied) + * * node_max: maximum amount of nodes in a region + */ +#define rustg_worley_generate(region_size, threshold, node_per_region_chance, size, node_min, node_max) \ + RUSTG_CALL(RUST_G, "worley_generate")(region_size, threshold, node_per_region_chance, size, node_min, node_max) diff --git a/code/__DEFINES/tgui.dm b/code/__DEFINES/tgui.dm index 1b3925f43a..2d1c22b42b 100644 --- a/code/__DEFINES/tgui.dm +++ b/code/__DEFINES/tgui.dm @@ -1,4 +1,59 @@ -#define UI_INTERACTIVE 2 // Green/Interactive -#define UI_UPDATE 1 // Orange/Updates Only -#define UI_DISABLED 0 // Red/Disabled -#define UI_CLOSE -1 // Closed +/// Green eye; fully interactive +#define UI_INTERACTIVE 2 +/// Orange eye; updates but is not interactive +#define UI_UPDATE 1 +/// Red eye; disabled, does not update +#define UI_DISABLED 0 +/// UI Should close +#define UI_CLOSE -1 + +/// Maximum number of windows that can be suspended/reused +#define TGUI_WINDOW_SOFT_LIMIT 5 +/// Maximum number of open windows +#define TGUI_WINDOW_HARD_LIMIT 9 + +/// Maximum ping timeout allowed to detect zombie windows +#define TGUI_PING_TIMEOUT (4 SECONDS) +/// Used for rate-limiting to prevent DoS by excessively refreshing a TGUI window +#define TGUI_REFRESH_FULL_UPDATE_COOLDOWN (1 SECONDS) + +/// Maximum amount of chunks a payload can be split up into +#define TGUI_MAX_CHUNKS 32 + +/// Window does not exist +#define TGUI_WINDOW_CLOSED 0 +/// Window was just opened, but is still not ready to be sent data +#define TGUI_WINDOW_LOADING 1 +/// Window is free and ready to receive data +#define TGUI_WINDOW_READY 2 + +/// Though not the maximum renderable ByondUis within tgui, this is the maximum that the server will manage per-UI +#define TGUI_MANAGED_BYONDUI_LIMIT 10 + +// These are defines instead of being inline, as they're being sent over +// from tgui-core, so can't be easily played with +#define TGUI_MANAGED_BYONDUI_TYPE_RENDER "renderByondUi" +#define TGUI_MANAGED_BYONDUI_TYPE_UNMOUNT "unmountByondUi" + +#define TGUI_MANAGED_BYONDUI_PAYLOAD_ID "renderByondUi" + +/// Get a window id based on the provided pool index +#define TGUI_WINDOW_ID(index) "tgui-window-[index]" +/// Get a pool index of the provided window id +#define TGUI_WINDOW_INDEX(window_id) text2num(copytext(window_id, 13)) + +/// Creates a message packet for sending via output() +// This is {"type":type,"payload":payload}, but pre-encoded. This is much faster +// than doing it the normal way. +// To ensure this is correct, this is unit tested in tgui_create_message. +#define TGUI_CREATE_MESSAGE(type, payload) ( \ + "%7b%22type%22%3a%22[type]%22%2c%22payload%22%3a[url_encode(json_encode(payload))]%7d" \ +) + +/** + * Gets a ui_state that checks to see if the user has specific admin permissions. + * + * Arguments: + * * required_perms: Which admin permission flags to check the user for, such as [R_ADMIN] + */ +#define ADMIN_STATE(required_perms) (GLOB.admin_states[required_perms] ||= new /datum/ui_state/admin_state(required_perms)) diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index f58ec6857f..4f6508c39c 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -377,6 +377,8 @@ GLOBAL_LIST_INIT(roguetraits, list( } while (0) #define HAS_TRAIT(target, trait) (target.status_traits ? (target.status_traits[trait] ? TRUE : FALSE) : FALSE) #define HAS_TRAIT_FROM(target, trait, source) (target.status_traits ? (target.status_traits[trait] ? (source in target.status_traits[trait]) : FALSE) : FALSE) +#define HAS_TRAIT_FROM_ONLY(target, trait, source) (HAS_TRAIT(target, trait) && (source in target._status_traits[trait]) && (length(target.status_traits[trait]) == 1)) +#define HAS_TRAIT_NOT_FROM(target, trait, source) (HAS_TRAIT(target, trait) && (length(target.status_traits[trait] - source) > 0)) /* Remember to update _globalvars/traits.dm if you're adding/removing/renaming traits. @@ -587,3 +589,27 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai //for ai #define TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM "element-required" +/*/mob/living/proc/on_trait_gain(trait, source) + SEND_SIGNAL(src, COMSIG_TRAIT_GAIN, trait, source) + switch(trait) + if(TRAIT_COMMIE, TRAIT_CABAL, TRAIT_HORDE, TRAIT_DEPRAVED) + if(ishuman(src)) + var/mob/living/carbon/human/H = src + H.update_heretic_commune() + +/mob/living/proc/on_trait_loss(trait, source) + SEND_SIGNAL(src, COMSIG_TRAIT_LOSS, trait, source) + switch(trait) + if(TRAIT_COMMIE, TRAIT_CABAL, TRAIT_HORDE, TRAIT_DEPRAVED) + if(ishuman(src)) + var/mob/living/carbon/human/H = src + H.update_heretic_commune()*/ + +///The entity has AI 'access', so is either an AI, has an access wand, or is an admin ghost AI. Used to block off regular Silicons from things. +///This is put on the mob, it is used on the client for Admins but they are the exception as they use `isAdminGhostAI`. +#define TRAIT_AI_ACCESS "ai_access_trait" +#define TRAIT_UI_BLOCKED "uiblocked" +/// Prevents usage of manipulation appendages (picking, holding or using items, manipulating storage). +#define TRAIT_HANDS_BLOCKED "handsblocked" +/// This mob should never close UI even if it doesn't have a client +#define TRAIT_PRESERVE_UI_WITHOUT_CLIENT "preserve_ui_without_client" diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 742a1745fa..6d4c62a515 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -9,13 +9,6 @@ * Misc */ -///Add an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun -#define UNTYPED_LIST_ADD(list, item) (list += LIST_VALUE_WRAP_LISTS(item)) -///Remove an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun -#define UNTYPED_LIST_REMOVE(list, item) (list -= LIST_VALUE_WRAP_LISTS(item)) -///If value is a list, wrap it in a list so it can be used with list add/remove operations -#define LIST_VALUE_WRAP_LISTS(value) (islist(value) ? list(value) : value) - #define LAZYINITLIST(L) if (!L) L = list() #define UNSETEMPTY(L) if (L && !length(L)) L = null #define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = list(); } } @@ -622,3 +615,11 @@ GLOBAL_LIST_EMPTY(string_lists) return return GLOB.string_lists[string_id] = values + +// Generic listoflist safe add and removal macros: +///If value is a list, wrap it in a list so it can be used with list add/remove operations +#define LIST_VALUE_WRAP_LISTS(value) (islist(value) ? list(value) : value) +///Add an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun +#define UNTYPED_LIST_ADD(list, item) (list += LIST_VALUE_WRAP_LISTS(item)) +///Remove an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun +#define UNTYPED_LIST_REMOVE(list, item) (list -= LIST_VALUE_WRAP_LISTS(item)) diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index 54a86c1a9e..484884b2bf 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -199,9 +199,41 @@ WRITE_LOG(GLOB.character_list_log, "\[[logtime]] [text]") /* ui logging */ - -/proc/log_tgui(text) - WRITE_LOG(GLOB.tgui_log, "\[[logtime]] [text]") +/** + * Appends a tgui-related log entry. All arguments are optional. + */ +/proc/log_tgui( + user, + message, + context, + datum/tgui_window/window, + datum/src_object, +) + var/entry = "" + // Insert user info + if(!user) + entry += "" + else if(istype(user, /mob)) + var/mob/mob = user + entry += "[mob.ckey] (as [mob] at [mob.x],[mob.y],[mob.z])" + else if(istype(user, /client)) + var/client/client = user + entry += "[client.ckey]" + // Insert context + if(context) + entry += " in [context]" + else if(window) + entry += " in [window.id]" + // Resolve src_object + if(!src_object && window?.locked_by) + src_object = window.locked_by.src_object + // Insert src_object info + if(src_object) + entry += "\nUsing: [src_object.type] [REF(src_object)]" + // Insert message + if(message) + entry += "\n[message]" + WRITE_LOG(GLOB.tgui_log, "\[[logtime]] [entry]") /* For logging round startup. */ /proc/start_log(log) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 58d35cc8be..5be98db4dc 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1256,3 +1256,65 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0 var/icon/my_icon = icon(icon_path) GLOB.icon_dimensions[icon_path] = list("width" = my_icon.Width(), "height" = my_icon.Height()) return GLOB.icon_dimensions[icon_path] + +///given a text string, returns whether it is a valid dmi icons folder path +/proc/is_valid_dmi_file(icon_path) + if(!istext(icon_path) || !length(icon_path)) + return FALSE + + var/is_in_icon_folder = findtextEx(icon_path, "icons/") + var/is_dmi_file = findtextEx(icon_path, ".dmi") + + if(is_in_icon_folder && is_dmi_file) + return TRUE + return FALSE + +/// given an icon object, dmi file path, or atom/image/mutable_appearance, attempts to find and return an associated dmi file path. +/// a weird quirk about dm is that /icon objects represent both compile-time or dynamic icons in the rsc, +/// but stringifying rsc references returns a dmi file path +/// ONLY if that icon represents a completely unchanged dmi file from when the game was compiled. +/// so if the given object is associated with an icon that was in the rsc when the game was compiled, this returns a path. otherwise it returns "" +/proc/get_icon_dmi_path(icon/icon) + /// the dmi file path we attempt to return if the given object argument is associated with a stringifiable icon + /// if successful, this looks like "icons/path/to/dmi_file.dmi" + var/icon_path = "" + + if(isatom(icon) || istype(icon, /image) || istype(icon, /mutable_appearance)) + var/atom/atom_icon = icon + icon = atom_icon.icon + //atom icons compiled in from 'icons/path/to/dmi_file.dmi' are weird and not really icon objects that you generate with icon(). + //if they're unchanged dmi's then they're stringifiable to "icons/path/to/dmi_file.dmi" + + if(isicon(icon) && isfile(icon)) + //icons compiled in from 'icons/path/to/dmi_file.dmi' at compile time are weird and aren't really /icon objects, + ///but they pass both isicon() and isfile() checks. they're the easiest case since stringifying them gives us the path we want + var/icon_ref = text_ref(icon) + var/locate_icon_string = "[locate(icon_ref)]" + + icon_path = locate_icon_string + + else if(isicon(icon) && "[icon]" == "/icon") + // icon objects generated from icon() at runtime are icons, but they AREN'T files themselves, they represent icon files. + // if the files they represent are compile time dmi files in the rsc, then + // the rsc reference returned by fcopy_rsc() will be stringifiable to "icons/path/to/dmi_file.dmi" + var/rsc_ref = fcopy_rsc(icon) + + var/icon_ref = text_ref(rsc_ref) + + var/icon_path_string = "[locate(icon_ref)]" + + icon_path = icon_path_string + + else if(istext(icon)) + var/rsc_ref = fcopy_rsc(icon) + //if its the text path of an existing dmi file, the rsc reference returned by fcopy_rsc() will be stringifiable to a dmi path + + var/rsc_ref_ref = text_ref(rsc_ref) + var/rsc_ref_string = "[locate(rsc_ref_ref)]" + + icon_path = rsc_ref_string + + if(is_valid_dmi_file(icon_path)) + return icon_path + + return FALSE diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index a9b419fda2..bcbae7d329 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -872,3 +872,8 @@ GLOBAL_LIST_INIT(binary, list("0","1")) return json_decode(data) catch return + +/// Removes all non-alphanumerics from the text, keep in mind this can lead to id conflicts +/proc/sanitize_css_class_name(name) + var/static/regex/regex = new(@"[^a-zA-Z0-9]","g") + return replacetext(name, regex, "") diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index bd9f151c37..b132ba6d51 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -5,6 +5,10 @@ */ GLOBAL_LIST_INIT(traits_by_type, list( /mob = list( + TRAIT_AI_ACCESS, + TRAIT_UI_BLOCKED, + TRAIT_HANDS_BLOCKED, + TRAIT_PRESERVE_UI_WITHOUT_CLIENT, TRAIT_LEPROSY, TRAIT_GUARDSMAN, TRAIT_WOODSMAN, diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm index 84b956271d..5f09134c71 100644 --- a/code/_onclick/hud/action_button.dm +++ b/code/_onclick/hud/action_button.dm @@ -165,13 +165,7 @@ else . += hide_appearance -/atom/movable/screen/movable/action_button/MouseEntered(location,control,params) - if(!QDELETED(src)) - openToolTip(usr,src,params,title = name,content = desc,theme = actiontooltipstyle) - ..() - /atom/movable/screen/movable/action_button/MouseExited() - closeToolTip(usr) ..() /datum/hud/proc/get_action_buttons_icons() diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm index a5636a05f3..0f2e6a8e91 100644 --- a/code/_onclick/hud/alert.dm +++ b/code/_onclick/hud/alert.dm @@ -104,17 +104,6 @@ var/alert_group = ALERT_STATUS //decides where on the screen the alert shows up, if it's a debuff, status effect, or buff nomouseover = FALSE -/atom/movable/screen/alert/MouseEntered(location,control,params) - ..() -// if(!QDELETED(src)) -// openToolTip(usr,src,params,title = name,content = desc,theme = alerttooltipstyle) - - -/atom/movable/screen/alert/MouseExited() - ..() -// closeToolTip(usr) - - //Gas alerts /atom/movable/screen/alert/not_enough_oxy name = "Choking" diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm index 32a8f81da8..fa0a661b32 100644 --- a/code/_onclick/hud/radial.dm +++ b/code/_onclick/hud/radial.dm @@ -18,14 +18,10 @@ GLOBAL_LIST_EMPTY(radial_menus) /atom/movable/screen/radial/slice/MouseEntered(location, control, params) . = ..() icon_state = "radial_slice_focus" - if(tooltips) - openToolTip(usr, src, params, title = name) /atom/movable/screen/radial/slice/MouseExited(location, control, params) . = ..() icon_state = "radial_slice" - if(tooltips) - closeToolTip(usr) /atom/movable/screen/radial/slice/Click(location, control, params) if(usr.client == parent.current_user) diff --git a/code/controllers/subsystem/tgui.dm b/code/controllers/subsystem/tgui.dm index c687a3b20e..5f57355edb 100644 --- a/code/controllers/subsystem/tgui.dm +++ b/code/controllers/subsystem/tgui.dm @@ -1,3 +1,15 @@ +/*! + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/** + * tgui subsystem + * + * Contains all tgui state and subsystem code. + * + */ + SUBSYSTEM_DEF(tgui) name = "tgui" wait = 9 @@ -5,32 +17,337 @@ SUBSYSTEM_DEF(tgui) priority = FIRE_PRIORITY_TGUI runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT - var/list/currentrun = list() - var/list/open_uis = list() // A list of open UIs, grouped by src_object and ui_key. - var/list/processing_uis = list() // A list of processing UIs, ungrouped. - var/basehtml // The HTML base used for all UIs. + /// A list of UIs scheduled to process + var/list/current_run = list() + /// A list of all open UIs + var/list/all_uis = list() + /// The HTML base used for all UIs. + var/basehtml /datum/controller/subsystem/tgui/PreInit() - basehtml = file2text('tgui-next/packages/tgui/public/tgui-main.html') + basehtml = file2text('tgui/public/tgui.html') + + // Inject inline helper functions + var/helpers = file2text('tgui/public/helpers.min.js') + helpers = "" + basehtml = replacetextEx(basehtml, "", helpers) + + // Inject inline ntos-error styles + var/ntos_error = file2text('tgui/public/ntos-error.min.css') + ntos_error = "" + basehtml = replacetextEx(basehtml, "", ntos_error) + + basehtml = replacetextEx(basehtml, "", "") + /datum/controller/subsystem/tgui/Shutdown() close_all_uis() -/datum/controller/subsystem/tgui/stat_entry() - ..("P:[processing_uis.len]") +/datum/controller/subsystem/tgui/stat_entry(msg) + msg = "P:[length(all_uis)]" + return ..() -/datum/controller/subsystem/tgui/fire(resumed = 0) - if (!resumed) - src.currentrun = processing_uis.Copy() - //cache for sanic speed (lists are references anyways) - var/list/currentrun = src.currentrun - - while(currentrun.len) - var/datum/tgui/ui = currentrun[currentrun.len] - currentrun.len-- - if(ui && ui.user && ui.src_object) - ui.process() +/datum/controller/subsystem/tgui/fire(resumed = FALSE) + if(!resumed) + src.current_run = all_uis.Copy() + // Cache for sanic speed (lists are references anyways) + var/list/current_run = src.current_run + while(current_run.len) + var/datum/tgui/ui = current_run[current_run.len] + current_run.len-- + // TODO: Move user/src_object check to process() + if(ui?.user && ui.src_object) + ui.process(wait * 0.1) else - processing_uis.Remove(ui) - if (MC_TICK_CHECK) + ui.close(0) + if(MC_TICK_CHECK) return + +/** + * public + * + * Requests a usable tgui window from the pool. + * Returns null if pool was exhausted. + * + * required user mob + * return datum/tgui_window + */ +/datum/controller/subsystem/tgui/proc/request_pooled_window(mob/user) + if(!user.client) + return null + var/list/windows = user.client.tgui_windows + var/window_id + var/datum/tgui_window/window + var/window_found = FALSE + // Find a usable window + for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT) + window_id = TGUI_WINDOW_ID(i) + window = windows[window_id] + // As we are looping, create missing window datums + if(!window) + window = new(user.client, window_id, pooled = TRUE) + // Skip windows with acquired locks + if(window.locked) + continue + if(window.status == TGUI_WINDOW_READY) + return window + if(window.status == TGUI_WINDOW_CLOSED) + window.status = TGUI_WINDOW_LOADING + window_found = TRUE + break + if(!window_found) + log_tgui(user, "Error: Pool exhausted", + context = "SStgui/request_pooled_window") + return null + return window + +/** + * public + * + * Force closes all tgui windows. + * + * required user mob + */ +/datum/controller/subsystem/tgui/proc/force_close_all_windows(mob/user) + log_tgui(user, context = "SStgui/force_close_all_windows") + if(user.client) + user.client.tgui_windows = list() + for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT) + var/window_id = TGUI_WINDOW_ID(i) + user << browse(null, "window=[window_id]") + +/** + * public + * + * Force closes the tgui window by window_id. + * + * required user mob + * required window_id string + */ +/datum/controller/subsystem/tgui/proc/force_close_window(mob/user, window_id) + log_tgui(user, context = "SStgui/force_close_window") + // Close all tgui datums based on window_id. + for(var/datum/tgui/ui in user.tgui_open_uis) + if(ui.window && ui.window.id == window_id) + ui.close(can_be_suspended = FALSE) + // Close window directly just to be sure. + user << browse(null, "window=[window_id]") + +/** + * public + * + * Try to find an instance of a UI, and push an update to it. + * + * required user mob The mob who opened/is using the UI. + * required src_object datum The object/datum which owns the UI. + * optional ui datum/tgui The UI to be updated, if it exists. + * optional force_open bool If the UI should be re-opened instead of updated. + * + * return datum/tgui The found UI. + */ +/datum/controller/subsystem/tgui/proc/try_update_ui( + mob/user, + datum/src_object, + datum/tgui/ui) + // Look up a UI if it wasn't passed + if(isnull(ui)) + ui = get_open_ui(user, src_object) + // Couldn't find a UI. + if(isnull(ui)) + return null + ui.process_status() + // UI ended up with the closed status + // or is actively trying to close itself. + // FIXME: Doesn't actually fix the paper bug. + if(ui.status <= UI_CLOSE) + ui.close() + return null + ui.send_update() + return ui + +/** + * public + * + * Get a open UI given a user and src_object. + * + * required user mob The mob who opened/is using the UI. + * required src_object datum The object/datum which owns the UI. + * + * return datum/tgui The found UI. + */ +/datum/controller/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object) + // No UIs opened for this src_object + if(!LAZYLEN(src_object?.open_uis)) + return null + for(var/datum/tgui/ui in src_object.open_uis) + // Make sure we have the right user + if(ui.user == user) + return ui + return null + +/** + * public + * + * Update all UIs attached to src_object. + * + * required src_object datum The object/datum which owns the UIs. + * + * return int The number of UIs updated. + */ +/datum/controller/subsystem/tgui/proc/update_uis(datum/src_object) + // No UIs opened for this src_object + if(!LAZYLEN(src_object?.open_uis)) + return 0 + var/count = 0 + for(var/datum/tgui/ui in src_object.open_uis) + // Check if UI is valid. + if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user)) + INVOKE_ASYNC(ui, TYPE_PROC_REF(/datum/tgui, process), wait * 0.1, TRUE) + count++ + return count + +/** + * public + * + * Close all UIs attached to src_object. + * + * required src_object datum The object/datum which owns the UIs. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/close_uis(datum/src_object) + // No UIs opened for this src_object + if(!LAZYLEN(src_object?.open_uis)) + return 0 + var/count = 0 + for(var/datum/tgui/ui in src_object.open_uis) + // Check if UI is valid. + if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.close() + count++ + return count + +/** + * public + * + * Close all UIs regardless of their attachment to src_object. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/close_all_uis() + var/count = 0 + for(var/datum/tgui/ui in all_uis) + // Check if UI is valid. + if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.close() + count++ + return count + +/** + * public + * + * Update all UIs belonging to a user. + * + * required user mob The mob who opened/is using the UI. + * optional src_object datum If provided, only update UIs belonging this src_object. + * + * return int The number of UIs updated. + */ +/datum/controller/subsystem/tgui/proc/update_user_uis(mob/user, datum/src_object) + var/count = 0 + if(length(user?.tgui_open_uis) == 0) + return count + for(var/datum/tgui/ui in user.tgui_open_uis) + if(isnull(src_object) || ui.src_object == src_object) + ui.process(wait * 0.1, force = 1) + count++ + return count + +/** + * public + * + * Close all UIs belonging to a user. + * + * required user mob The mob who opened/is using the UI. + * optional src_object datum If provided, only close UIs belonging this src_object. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/close_user_uis(mob/user, datum/src_object) + var/count = 0 + if(length(user?.tgui_open_uis) == 0) + return count + for(var/datum/tgui/ui in user.tgui_open_uis) + if(isnull(src_object) || ui.src_object == src_object) + ui.close() + count++ + return count + +/** + * private + * + * Add a UI to the list of open UIs. + * + * required ui datum/tgui The UI to be added. + */ +/datum/controller/subsystem/tgui/proc/on_open(datum/tgui/ui) + ui.user?.tgui_open_uis |= ui + LAZYOR(ui.src_object.open_uis, ui) + all_uis |= ui + +/** + * private + * + * Remove a UI from the list of open UIs. + * + * required ui datum/tgui The UI to be removed. + * + * return bool If the UI was removed or not. + */ +/datum/controller/subsystem/tgui/proc/on_close(datum/tgui/ui) + // Remove it from the list of processing UIs. + all_uis -= ui + current_run -= ui + // If the user exists, remove it from them too. + if(ui.user) + ui.user.tgui_open_uis -= ui + if(ui.src_object) + LAZYREMOVE(ui.src_object.open_uis, ui) + return TRUE + +/** + * private + * + * Handle client logout, by closing all their UIs. + * + * required user mob The mob which logged out. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/on_logout(mob/user) + close_user_uis(user) + +/** + * private + * + * Handle clients switching mobs, by transferring their UIs. + * + * required user source The client's original mob. + * required user target The client's new mob. + * + * return bool If the UIs were transferred. + */ +/datum/controller/subsystem/tgui/proc/on_transfer(mob/source, mob/target) + // The old mob had no open UIs. + if(length(source?.tgui_open_uis) == 0) + return FALSE + if(isnull(target.tgui_open_uis) || !istype(target.tgui_open_uis, /list)) + target.tgui_open_uis = list() + // Transfer all the UIs. + for(var/datum/tgui/ui in source.tgui_open_uis) + // Inform the UIs of their new owner. + ui.user = target + target.tgui_open_uis += ui + // Clear the old list. + source.tgui_open_uis.Cut() + return TRUE diff --git a/code/datums/achievements/_achievement_data.dm b/code/datums/achievements/_achievement_data.dm index bfef209345..bab91b07d6 100644 --- a/code/datums/achievements/_achievement_data.dm +++ b/code/datums/achievements/_achievement_data.dm @@ -77,50 +77,3 @@ data[achievement_type] = FALSE else if(istype(A, /datum/award/score)) data[achievement_type] = 0 - -/datum/achievement_data/ui_base_html(html) - var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/achievements) - . = replacetext(html, "", assets.css_tag()) - -/datum/achievement_data/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.always_state) - ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) - if(!ui) -// var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/achievements) -// assets.send(user) - ui = new(user, src, ui_key, "achievements", "Achievements Menu", 800, 1000, master_ui, state) - ui.open() - -/datum/achievement_data/ui_data(mob/user) - var/ret_data = list() // screw standards (qustinnus you must rename src.data ok) - ret_data["categories"] = list("Bosses", "Misc") - ret_data["achievements"] = list() - - var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/achievements) - - for(var/achievement_type in SSachievements.achievements) - if(!SSachievements.achievements[achievement_type].name) //No name? we a subtype. - continue - if(isnull(data[achievement_type])) //We're still loading - continue - var/list/this = list( - "name" = SSachievements.achievements[achievement_type].name, - "desc" = SSachievements.achievements[achievement_type].desc, - "category" = SSachievements.achievements[achievement_type].category, - "icon_class" = assets.icon_class_name(SSachievements.achievements[achievement_type].icon), - "achieved" = data[achievement_type] - ) - - ret_data["achievements"] += list(this) - - return ret_data - -/client/verb/checkachievements() - set category = "OOC" - set name = "Check achievements" - set desc = "" - set hidden = 1 - if(!holder) - return - - player_details.achievements.ui_interact(usr) - diff --git a/code/datums/components/crafting/crafting.dm b/code/datums/components/crafting/crafting.dm index d982edbc53..46a787c0a2 100644 --- a/code/datums/components/crafting/crafting.dm +++ b/code/datums/components/crafting/crafting.dm @@ -420,8 +420,8 @@ if(user == parent) ui_interact(user) -/datum/component/personal_crafting/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.not_incapacitated_turf_state) - ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) +/datum/component/personal_crafting/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) if(!ui) cur_category = categories[1] if(islist(categories[cur_category])) @@ -429,11 +429,10 @@ cur_subcategory = subcats[1] else cur_subcategory = CAT_NONE - ui = new(user, src, ui_key, "personal_crafting", "Crafting Menu", 700, 800, master_ui, state) + ui = new(user, src, "PersonalCrafting", "Crafting Menu", 700, 800) + ui.set_state(GLOB.not_incapacitated_turf_state) ui.open() - - /datum/component/personal_crafting/ui_data(mob/user) var/list/data = list() data["busy"] = busy diff --git a/code/datums/components/gps.dm b/code/datums/components/gps.dm deleted file mode 100644 index fca2c1ab2b..0000000000 --- a/code/datums/components/gps.dm +++ /dev/null @@ -1,154 +0,0 @@ -///Global GPS_list. All GPS components get saved in here for easy reference. -GLOBAL_LIST_EMPTY(GPS_list) -///GPS component. Atoms that have this show up on gps. Pretty simple stuff. -/datum/component/gps - var/gpstag = "COM0" - var/tracking = TRUE - var/emped = FALSE - -/datum/component/gps/Initialize(_gpstag = "COM0") - if(!isatom(parent)) - return COMPONENT_INCOMPATIBLE - gpstag = _gpstag - GLOB.GPS_list += src - -/datum/component/gps/Destroy() - GLOB.GPS_list -= src - return ..() - -///GPS component subtype. Only gps/item's can be used to open the UI. -/datum/component/gps/item - var/updating = TRUE //Automatic updating of GPS list. Can be set to manual by user. - var/global_mode = TRUE //If disabled, only GPS signals of the same Z level are shown - -/datum/component/gps/item/Initialize(_gpstag = "COM0", emp_proof = FALSE) - . = ..() - if(. == COMPONENT_INCOMPATIBLE || !isitem(parent)) - return COMPONENT_INCOMPATIBLE - var/atom/A = parent - A.add_overlay("working") - A.name = "[initial(A.name)] ([gpstag])" - RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(interact)) - if(!emp_proof) - RegisterSignal(parent, COMSIG_ATOM_EMP_ACT, PROC_REF(on_emp_act)) - RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) - RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(on_AltClick)) - -///Called on COMSIG_ITEM_ATTACK_SELF -/datum/component/gps/item/proc/interact(datum/source, mob/user) - if(user) - ui_interact(user) - -///Called on COMSIG_PARENT_EXAMINE -/datum/component/gps/item/proc/on_examine(datum/source, mob/user, list/examine_list) - examine_list += span_notice("Alt-click to switch it [tracking ? "off":"on"].") - -///Called on COMSIG_ATOM_EMP_ACT -/datum/component/gps/item/proc/on_emp_act(datum/source, severity) - emped = TRUE - var/atom/A = parent - A.cut_overlay("working") - A.add_overlay("emp") - addtimer(CALLBACK(src, PROC_REF(reboot)), 300, TIMER_UNIQUE|TIMER_OVERRIDE) //if a new EMP happens, remove the old timer so it doesn't reactivate early - SStgui.close_uis(src) //Close the UI control if it is open. - -///Restarts the GPS after getting turned off by an EMP. -/datum/component/gps/item/proc/reboot() - emped = FALSE - var/atom/A = parent - A.cut_overlay("emp") - A.add_overlay("working") - -///Calls toggletracking -/datum/component/gps/item/proc/on_AltClick(datum/source, mob/user) - toggletracking(user) - -///Toggles the tracking for the gps -/datum/component/gps/item/proc/toggletracking(mob/user) - if(!user.canUseTopic(parent, BE_CLOSE)) - return //user not valid to use gps - if(emped) - to_chat(user, span_warning("It's busted!")) - return - var/atom/A = parent - if(tracking) - A.cut_overlay("working") - to_chat(user, span_notice("[parent] is no longer tracking, or visible to other GPS devices.")) - tracking = FALSE - else - A.add_overlay("working") - to_chat(user, span_notice("[parent] is now tracking, and visible to other GPS devices.")) - tracking = TRUE - -/datum/component/gps/item/ui_interact(mob/user, ui_key = "gps", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) // Remember to use the appropriate state. - if(emped) - to_chat(user, span_hear("[parent] fizzles weakly.")) - return - ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) - if(!ui) - // Variable window height, depending on how many GPS units there are - // to show, clamped to relatively safe range. - var/gps_window_height = CLAMP(325 + GLOB.GPS_list.len * 14, 325, 700) - ui = new(user, src, ui_key, "gps", "Global Positioning System", 470, gps_window_height, master_ui, state) //width, height - ui.open() - - ui.set_autoupdate(state = updating) - -/datum/component/gps/item/ui_data(mob/user) - var/list/data = list() - data["power"] = tracking - data["tag"] = gpstag - data["updating"] = updating - data["globalmode"] = global_mode - if(!tracking || emped) //Do not bother scanning if the GPS is off or EMPed - return data - - var/turf/curr = get_turf(parent) - data["currentArea"] = "[get_area_name(curr, TRUE)]" - data["currentCoords"] = "[curr.x], [curr.y], [curr.z]" - - var/list/signals = list() - data["signals"] = list() - - for(var/gps in GLOB.GPS_list) - var/datum/component/gps/G = gps - if(G.emped || !G.tracking || G == src) - continue - var/turf/pos = get_turf(G.parent) - if(!pos || !global_mode && pos.z != curr.z) - continue - var/list/signal = list() - signal["entrytag"] = G.gpstag //Name or 'tag' of the GPS - signal["coords"] = "[pos.x], [pos.y], [pos.z]" - if(pos.z == curr.z) //Distance/Direction calculations for same z-level only - signal["dist"] = max(get_dist(curr, pos), 0) //Distance between the src and remote GPS turfs - signal["degrees"] = round(Get_Angle(curr, pos)) //0-360 degree directional bearing, for more precision. - signals += list(signal) //Add this signal to the list of signals - data["signals"] = signals - return data - -/datum/component/gps/item/ui_act(action, params) - if(..()) - return - switch(action) - if("rename") - var/atom/parentasatom = parent - var/a = input("Please enter desired tag.", parentasatom.name, gpstag) as text|null - - if (!a) - return - - a = copytext(sanitize(a), 1, 20) - gpstag = a - . = TRUE - parentasatom.name = "global positioning system ([gpstag])" - - if("power") - toggletracking(usr) - . = TRUE - if("updating") - updating = !updating - . = TRUE - if("globalmode") - global_mode = !global_mode - . = TRUE diff --git a/code/datums/datum.dm b/code/datums/datum.dm index 14433eb5af..170c6654c0 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -17,6 +17,10 @@ */ var/gc_destroyed + /// Open uis owned by this datum + /// Lazy, since this case is semi rare + var/list/open_uis + /// Active timers with this datum as the target var/list/active_timers /// Status traits attached to this datum diff --git a/code/datums/spawners_menu.dm b/code/datums/spawners_menu.dm index 6721b7e168..1f50a6b644 100644 --- a/code/datums/spawners_menu.dm +++ b/code/datums/spawners_menu.dm @@ -6,10 +6,11 @@ qdel(src) owner = new_owner -/datum/spawners_menu/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.observer_state) - ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) +/datum/spawners_menu/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, ui_key, "spawners_menu", "Spawners Menu", 700, 600, master_ui, state) + ui = new(user, src, "SpawnersMenu", "Spawners Menu", 700, 600) + ui.set_state(GLOB.observer_state) ui.open() /datum/spawners_menu/ui_data(mob/user) diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index f1b1fa7634..02aa9ae219 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -83,19 +83,25 @@ return !(stat & (NOPOWER|BROKEN|MAINT)) /obj/machinery/can_interact(mob/user) - var/silicon = IsAdminGhost(user) + if(QDELETED(user)) + return FALSE + if((stat & (NOPOWER|BROKEN)) && !(interaction_flags_machine & INTERACT_MACHINE_OFFLINE)) return FALSE - if(!(interaction_flags_machine & INTERACT_MACHINE_OPEN)) - if(!silicon || !(interaction_flags_machine & INTERACT_MACHINE_OPEN_SILICON)) - return FALSE - if(silicon) - if(!(interaction_flags_machine & INTERACT_MACHINE_ALLOW_SILICON)) - return FALSE - else - if(interaction_flags_machine & INTERACT_MACHINE_REQUIRES_SILICON) - return FALSE + if(isAdminGhostAI(user)) + return TRUE + + if(!isliving(user)) + return FALSE + + if(!user.can_hold_items()) + return FALSE + + . = ..() + if(!.) + return FALSE + return TRUE //////////////////////////////////////////////////////////////////////////////////////////// diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 96044bd18c..ac17931496 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1045,23 +1045,9 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) else return "Mighty" -/obj/item/proc/openTip(location, control, params, user) - if(!(item_flags & FORCE_STRING_OVERRIDE)) - openToolTip(user,src,params,title = name,content = "[desc]
[force ? "Force: [force_string]" : ""]",theme = "") - else - openToolTip(user,src,params,title = name,content = "[desc]
Force: [force_string]",theme = "") - -/obj/item/MouseEntered(location, control, params) - . = ..() - if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr.client.prefs.enable_tips && !QDELETED(src)) - var/timedelay = usr.client.prefs.tip_delay/100 - var/user = usr - tip_timer = addtimer(CALLBACK(src, PROC_REF(openTip), location, control, params, user), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. - /obj/item/MouseExited() . = ..() deltimer(tip_timer)//delete any in-progress timer if the mouse is moved off the item before it finishes - closeToolTip(usr) // Called when a mob tries to use the item as a tool. diff --git a/code/game/objects/items/eightball.dm b/code/game/objects/items/eightball.dm deleted file mode 100644 index c00478c685..0000000000 --- a/code/game/objects/items/eightball.dm +++ /dev/null @@ -1,213 +0,0 @@ -/obj/item/toy/eightball - name = "magic eightball" - desc = "" - - icon = 'icons/obj/toy.dmi' - icon_state = "eightball" - w_class = WEIGHT_CLASS_TINY - - verb_say = "rattles" - - var/shaking = FALSE - var/on_cooldown = FALSE - - var/shake_time = 50 - var/cooldown_time = 100 - - var/static/list/possible_answers = list( - "It is certain", - "It is decidedly so", - "Without a doubt", - "Yes definitely", - "You may rely on it", - "As I see it, yes", - "Most likely", - "Outlook good", - "Yes", - "Signs point to yes", - "Reply hazy try again", - "Ask again later", - "Better not tell you now", - "Cannot predict now", - "Concentrate and ask again", - "Don't count on it", - "My reply is no", - "My sources say no", - "Outlook not so good", - "Very doubtful") - -/obj/item/toy/eightball/Initialize(mapload) - . = ..() - if(MakeHaunted()) - return INITIALIZE_HINT_QDEL - -/obj/item/toy/eightball/proc/MakeHaunted() - . = prob(1) - if(.) - new /obj/item/toy/eightball/haunted(loc) - -/obj/item/toy/eightball/attack_self(mob/user) - if(shaking) - return - - if(on_cooldown) - to_chat(user, span_warning("[src] was shaken recently, it needs time to settle.")) - return - - user.visible_message(span_notice("[user] starts shaking [src]."), span_notice("I start shaking [src]."), span_hear("I hear shaking and sloshing.")) - - shaking = TRUE - - start_shaking(user) - if(do_after(user, shake_time, needhand=TRUE, target=user, progress=TRUE)) - var/answer = get_answer() - say(answer) - - on_cooldown = TRUE - addtimer(CALLBACK(src, PROC_REF(clear_cooldown)), cooldown_time) - - shaking = FALSE - -/obj/item/toy/eightball/proc/start_shaking(user) - return - -/obj/item/toy/eightball/proc/get_answer() - return pick(possible_answers) - -/obj/item/toy/eightball/proc/clear_cooldown() - on_cooldown = FALSE - -// A broken magic eightball, it only says "YOU SUCK" over and over again. - -/obj/item/toy/eightball/broken - name = "broken magic eightball" - desc = "" - var/fixed_answer - -/obj/item/toy/eightball/broken/Initialize(mapload) - . = ..() - fixed_answer = pick(possible_answers) - -/obj/item/toy/eightball/broken/get_answer() - return fixed_answer - -// Haunted eightball is identical in description and function to toy, -// except it actually ASKS THE DEAD (wooooo) - -/obj/item/toy/eightball/haunted - shake_time = 150 - cooldown_time = 1800 - flags_1 = HEAR_1 - var/last_message - var/selected_message - var/list/votes - -/obj/item/toy/eightball/haunted/Initialize(mapload) - . = ..() - votes = list() - GLOB.poi_list |= src - -/obj/item/toy/eightball/haunted/Destroy() - GLOB.poi_list -= src - . = ..() - -/obj/item/toy/eightball/haunted/MakeHaunted() - return FALSE - -//ATTACK GHOST IGNORING PARENT RETURN VALUE -/obj/item/toy/eightball/haunted/attack_ghost(mob/user) - if(!shaking) - to_chat(user, span_warning("[src] is not currently being shaken.")) - return - interact(user) - return ..() - -/obj/item/toy/eightball/haunted/Hear(message, atom/movable/speaker, message_langs, raw_message, radio_freq, spans, message_mode, original_message) - . = ..() - last_message = raw_message - -/obj/item/toy/eightball/haunted/start_shaking(mob/user) - // notify ghosts that someone's shaking a haunted eightball - // and inform them of the message, (hopefully a yes/no question) - selected_message = last_message - notify_ghosts("[user] is shaking [src], hoping to get an answer to \"[selected_message]\"", source=src, enter_link="(Click to help)", action=NOTIFY_ATTACK, header = "Magic eightball") - -/obj/item/toy/eightball/haunted/Topic(href, href_list) - if(href_list["interact"]) - if(isobserver(usr)) - interact(usr) - -/obj/item/toy/eightball/haunted/proc/get_vote_tallies() - var/list/answers = list() - for(var/ckey in votes) - var/selected = votes[ckey] - if(selected in answers) - answers[selected]++ - else - answers[selected] = 1 - - return answers - - -/obj/item/toy/eightball/haunted/get_answer() - if(!votes.len) - return pick(possible_answers) - - var/list/tallied_votes = get_vote_tallies() - - // I miss python sorting, then I wouldn't have to muck about with - // all this - var/most_popular_answer - var/most_amount = 0 - // yes, if there is a tie, there is an arbitary decision - // but we never said the spirit world was fair - for(var/A in tallied_votes) - var/amount = tallied_votes[A] - if(amount > most_amount) - most_popular_answer = A - - return most_popular_answer - -/obj/item/toy/eightball/haunted/ui_interact(mob/user, ui_key="main", datum/tgui/ui=null, force_open=0, datum/tgui/master_ui=null, datum/ui_state/state = GLOB.observer_state) - - ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) - if(!ui) - ui = new(user, src, ui_key, "eightball", name, 400, 600, master_ui, state) - ui.open() - -/obj/item/toy/eightball/haunted/ui_data(mob/user) - var/list/data = list() - data["shaking"] = shaking - data["question"] = selected_message - var/list/tallied_votes = get_vote_tallies() - - data["answers"] = list() - - for(var/pa in possible_answers) - var/list/L = list() - L["answer"] = pa - var/amount = 0 - if(pa in tallied_votes) - amount = tallied_votes[pa] - L["amount"] = amount - var/selected = FALSE - if(votes[user.ckey] == pa) - selected = TRUE - L["selected"] = selected - - data["answers"] += list(L) - return data - -/obj/item/toy/eightball/haunted/ui_act(action, params) - if(..()) - return - var/mob/user = usr - - switch(action) - if("vote") - var/selected_answer = params["answer"] - if(!(selected_answer in possible_answers)) - return - else - votes[user.ckey] = selected_answer - . = TRUE diff --git a/code/game/objects/items/toys.dm b/code/game/objects/items/toys.dm index 9d4f18af30..dece755ce8 100644 --- a/code/game/objects/items/toys.dm +++ b/code/game/objects/items/toys.dm @@ -235,8 +235,7 @@ user.set_machine(src) interact(user) -/obj/item/toy/cards/cardhand/ui_interact(mob/user) - . = ..() +/obj/item/toy/cards/cardhand/interact(mob/user) var/dat = "You have:
" for(var/t in currenthand) dat += "A [t].
" diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index b1b7e7b2f2..24dfe3aa72 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -454,7 +454,7 @@ if(!usr.canUseTopic(src, BE_CLOSE) || !isturf(loc)) return - if(iscarbon(usr) || isdrone(usr)) + if(iscarbon(usr)) return toggle(usr) else to_chat(usr, span_warning("This mob type can't use this verb.")) diff --git a/code/game/objects/structures/musician.dm b/code/game/objects/structures/musician.dm index 90cf9b6db6..3dfb0108a8 100644 --- a/code/game/objects/structures/musician.dm +++ b/code/game/objects/structures/musician.dm @@ -368,9 +368,6 @@ return attack_hand(user) /obj/structure/piano/interact(mob/user) - ui_interact(user) - -/obj/structure/piano/ui_interact(mob/user) if(!user || !anchored) return diff --git a/code/game/objects/structures/noticeboard.dm b/code/game/objects/structures/noticeboard.dm index a61910393c..14cd05652c 100644 --- a/code/game/objects/structures/noticeboard.dm +++ b/code/game/objects/structures/noticeboard.dm @@ -39,10 +39,6 @@ return ..() /obj/structure/noticeboard/interact(mob/user) - ui_interact(user) - -/obj/structure/noticeboard/ui_interact(mob/user) - . = ..() var/auth = allowed(user) var/dat = "[name]
" for(var/obj/item/P in src) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 6249d90f46..6e77928728 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -32,6 +32,7 @@ GLOBAL_PROTECT(admin_verbs_default) /client/proc/set_context_menu_enabled, /client/proc/delete_player_book, /client/proc/amend_player_book, + /client/proc/enable_browser_debug, /client/proc/pull_book_file_names, /client/proc/adminwho, /client/proc/admin_spread_effect, @@ -83,7 +84,6 @@ GLOBAL_PROTECT(admin_verbs_admin) /client/proc/cmd_admin_subtle_message, /*send an message to somebody as a 'voice in their head'*/ /client/proc/cmd_admin_delete, /*delete an instance/object/mob/etc*/ /client/proc/cmd_admin_check_contents, /*displays the contents of an instance*/ - /client/proc/centcom_podlauncher,/*Open a window to launch a Supplypod and configure it or it's contents*/ /client/proc/check_antagonists, /*shows all antags*/ /client/proc/jumptocoord, /*we ghost and jump to a coordinate*/ /client/proc/Getmob, /*teleports a mob to our location*/ @@ -898,3 +898,13 @@ GLOBAL_PROTECT(admin_verbs_hideable) message_admins("[ADMIN_LOOKUPFLW(src)] has removed the bounty on [ADMIN_LOOKUPFLW(target_name)]") return to_chat(src, "Error. Bounty no longer active.") + +/client/proc/enable_browser_debug() + set category = "Debug" + set name = "Enable Browser Debug" + if(!holder) + return + + to_chat(src, "Browser tools are now enabled.") + winset(src, null, "browser-options=devtools,find,byondstorage") + diff --git a/code/modules/admin/check_antagonists.dm b/code/modules/admin/check_antagonists.dm index b6e984094a..ed52ae6551 100644 --- a/code/modules/admin/check_antagonists.dm +++ b/code/modules/admin/check_antagonists.dm @@ -177,9 +177,6 @@ lobby_players++ continue else if(M.stat != DEAD && M.mind && !isbrain(M)) - if(isdrone(M)) - drones++ - continue if(is_centcom_level(M.z)) living_skipped++ continue diff --git a/code/modules/admin/poll_management.dm b/code/modules/admin/poll_management.dm index 2674c42881..d09104bf4e 100644 --- a/code/modules/admin/poll_management.dm +++ b/code/modules/admin/poll_management.dm @@ -229,8 +229,7 @@ output += "
" var/datum/browser/panel = new(usr, "pmpanel", "Poll Management Panel", 780, 640) panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css') - if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support - panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') + panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') panel.set_content(jointext(output, "")) panel.open() @@ -527,8 +526,7 @@ panel_height = 320 var/datum/browser/panel = new(usr, "popanel", "Poll Option Panel", 370, panel_height) panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css') - if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support - panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') + panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') panel.set_content(jointext(output, "")) panel.open() diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index a30844261d..ec9e013b75 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -111,9 +111,8 @@ var/datum/browser/panel = new(usr, "banpanel", "Banning Panel", 910, panel_height) panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css') panel.add_stylesheet("banpanelcss", 'html/admin/banpanel.css') - if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements and DOM methods that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support - panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') - panel.add_script("banpaneljs", 'html/admin/banpanel.js') + panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css') + panel.add_script("banpaneljs", 'html/admin/banpanel.js') var/list/output = list("
[HrefTokenFormField()]") output += {"