diff --git a/.github/workflows/generate_client_storage.yml b/.github/workflows/generate_client_storage.yml new file mode 100644 index 00000000000..fc95e895096 --- /dev/null +++ b/.github/workflows/generate_client_storage.yml @@ -0,0 +1,33 @@ +name: "Generate Client Storage" +on: + push: + branches: + - master + paths: + - tgui/public/* + +jobs: + dispatch_repo: + if: ( !contains(github.event.head_commit.message, '[ci skip]') ) + name: Repository Dispatch + runs-on: ubuntu-latest + steps: + - name: Generate App Token + id: app-token-generation + uses: actions/create-github-app-token@v2 + if: env.APP_PRIVATE_KEY != '' && env.APP_ID != '' + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: tgstation + env: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + APP_ID: ${{ secrets.APP_ID }} + + - name: Send Repository Dispatch + if: success() + uses: peter-evans/repository-dispatch@v4 + with: + token: ${{ steps.app-token-generation.outputs.token }} + repository: tgstation/byond-client-storage + event-type: on_master_push diff --git a/cev_eris.dme b/cev_eris.dme index b79f0b9df93..f7f592d5f28 100644 --- a/cev_eris.dme +++ b/cev_eris.dme @@ -365,6 +365,7 @@ #include "code\datums\datum.dm" #include "code\datums\datum_click_handlers.dm" #include "code\datums\datum_hud.dm" +#include "code\datums\datumvars.dm" #include "code\datums\footsteps.dm" #include "code\datums\gps_data.dm" #include "code\datums\hierarchy.dm" diff --git a/code/__DEFINES/tgui.dm b/code/__DEFINES/tgui.dm index 60b7bea8547..8cdf731284d 100644 --- a/code/__DEFINES/tgui.dm +++ b/code/__DEFINES/tgui.dm @@ -24,6 +24,17 @@ /// 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 @@ -36,3 +47,4 @@ #define TGUI_CREATE_MESSAGE(type, payload) ( \ "%7b%22type%22%3a%22[type]%22%2c%22payload%22%3a[url_encode(json_encode(payload))]%7d" \ ) + diff --git a/code/_compile_options.dm b/code/_compile_options.dm index eac66e1dab0..24437ce4db9 100644 --- a/code/_compile_options.dm +++ b/code/_compile_options.dm @@ -21,6 +21,10 @@ ///Used for doing dry runs of the reference finder, to test for feature completeness //#define REFERENCE_TRACKING_DEBUG +/// If this is uncommented, additional logging (such as more in-depth tgui logging) will be enabled.alist +/// These logs prolly don't matter during production. +// #define EXTENDED_DEBUG_LOGGING + ///Run a lookup on things hard deleting by default. //#define GC_FAILURE_HARD_LOOKUP #ifdef GC_FAILURE_HARD_LOOKUP diff --git a/code/_global_vars/misc.dm b/code/_global_vars/misc.dm index 4efd8ebf8a4..d0898c1be02 100644 --- a/code/_global_vars/misc.dm +++ b/code/_global_vars/misc.dm @@ -33,4 +33,4 @@ GLOBAL_LIST_INIT(custom_kits, list( GLOBAL_DATUM(changelog_tgui, /datum/changelog) // FIXME FIXME FIXME TURN THIS BACK OFF WHEN YOU'RE DONE DIPSHIIIIIIIITTT! -GLOBAL_VAR_INIT(Debug2, TRUE) +GLOBAL_VAR_INIT(Debug2, FALSE) diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index a9fe52c17b5..dba9f38b307 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -489,6 +489,15 @@ default = -1 min_val = 0 +/datum/config_entry/number/hard_deletes_overrun_threshold + integer = FALSE + min_val = 0 + default = 0.5 + +/datum/config_entry/number/hard_deletes_overrun_limit + default = 0 + min_val = 0 + /*****************/ /* COOLDOWNS */ /*****************/ diff --git a/code/controllers/configuration/entries/resources.dm b/code/controllers/configuration/entries/resources.dm index 7aa5c1cac4f..b36166051e8 100644 --- a/code/controllers/configuration/entries/resources.dm +++ b/code/controllers/configuration/entries/resources.dm @@ -50,3 +50,7 @@ if (str_var && str_var[length(str_var)] != "/") str_var += "/" return ..(str_var) + +/datum/config_entry/string/storage_cdn_iframe + protection = CONFIG_ENTRY_LOCKED + default = "https://tgstation.github.io/byond-client-storage/iframe.html" diff --git a/code/controllers/subsystems/garbage.dm b/code/controllers/subsystems/garbage.dm index 68404a1b686..1e726cbafd6 100644 --- a/code/controllers/subsystems/garbage.dm +++ b/code/controllers/subsystems/garbage.dm @@ -63,7 +63,6 @@ SUBSYSTEM_DEF(garbage) #endif var/list/failed_hard_deletes = list() - /datum/controller/subsystem/garbage/PreInit() InitQueues() @@ -89,32 +88,38 @@ SUBSYSTEM_DEF(garbage) /datum/controller/subsystem/garbage/Shutdown() //Adds the del() log to the qdel log file - var/list/dellog = list() + var/list/del_log = list() //sort by how long it's wasted hard deleting sortTim(items, cmp=/proc/cmp_qdel_item_time, associative = TRUE) for(var/path in items) var/datum/qdel_item/I = items[path] - dellog += "Path: [path]" + var/list/entry = list() + del_log[path] = entry + if (I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG) - dellog += "\tSUSPENDED FOR LAG" + entry["SUSPENDED FOR LAG"] = TRUE if (I.failures) - dellog += "\tFailures: [I.failures]" - dellog += "\tqdel() Count: [I.qdels]" - dellog += "\tDestroy() Cost: [I.destroy_time]ms" + entry["Failures"] = I.failures + entry["qdel() Count"] = I.qdels + entry["Destroy() Cost (ms)"] = I.destroy_time + if (I.hard_deletes) - dellog += "\tTotal Hard Deletes: [I.hard_deletes]" - dellog += "\tTime Spent Hard Deleting: [I.hard_delete_time]ms" - dellog += "\tHighest Time Spent Hard Deleting: [I.hard_delete_max]ms" + entry["Total Hard Deletes"] = I.hard_deletes + entry["Time Spend Hard Deleting (ms)"] = I.hard_delete_time + entry["Highest Time Spend Hard Deleting (ms)"] = I.hard_delete_max if (I.hard_deletes_over_threshold) - dellog += "\tHard Deletes Over Threshold: [I.hard_deletes_over_threshold]" + entry["Hard Deletes Over Threshold"] = I.hard_deletes_over_threshold if (I.slept_destroy) - dellog += "\tSleeps: [I.slept_destroy]" + entry["Total Sleeps"] = I.slept_destroy if (I.no_respect_force) - dellog += "\tIgnored force: [I.no_respect_force] times" + entry["Total Ignored Force"] = I.no_respect_force if (I.no_hint) - dellog += "\tNo hint: [I.no_hint] times" - log_qdel(dellog.Join("\n")) + entry["Total No Hint"] = I.no_hint + if(LAZYLEN(I.extra_details)) + entry["Deleted Metadata"] = I.extra_details + + log_qdel("", del_log) /datum/controller/subsystem/garbage/fire() //the fact that this resets its processing each fire (rather then resume where it left off) is intentional. @@ -162,31 +167,34 @@ SUBSYSTEM_DEF(garbage) lastlevel = level - //We do this rather then for(var/refID in queue) because that sort of for loop copies the whole list. +// 1 from the hard reference in the queue, and 1 from the variable used before this +#define REFS_WE_EXPECT 2 + + //We do this rather then for(var/list/ref_info in queue) because that sort of for loop copies the whole list. //Normally this isn't expensive, but the gc queue can grow to 40k items, and that gets costly/causes overrun. for (var/i in 1 to length(queue)) var/list/L = queue[i] - if (length(L) < 2) + if (length(L) < GC_QUEUE_ITEM_INDEX_COUNT) count++ if (MC_TICK_CHECK) return continue - var/GCd_at_time = L[GC_QUEUE_ITEM_QUEUE_TIME] - if(GCd_at_time > cut_off_time) + var/queued_at_time = L[GC_QUEUE_ITEM_QUEUE_TIME] + if(queued_at_time > cut_off_time) break // Everything else is newer, skip them count++ - var/refID = L[GC_QUEUE_ITEM_REF] - var/datum/D - D = locate(refID) - if (!D || D.gc_destroyed != GCd_at_time) // So if something else coincidently gets the same ref, it's not deleted by mistake + var/datum/D = L[GC_QUEUE_ITEM_REF] + + // If that's all we've got, send er off + if (refcount(D) == REFS_WE_EXPECT) ++gcedlasttick ++totalgcs pass_counts[level]++ #ifdef REFERENCE_TRACKING - reference_find_on_fail -= refID //It's deleted we don't care anymore. - #endif + reference_find_on_fail -= text_ref(D) //It's deleted we don't care anymore. + #endif //ifdef REFERENCE_TRACKING if (MC_TICK_CHECK) return continue @@ -201,36 +209,50 @@ SUBSYSTEM_DEF(garbage) switch (level) if (GC_QUEUE_CHECK) #ifdef REFERENCE_TRACKING - if(reference_find_on_fail[refID]) - INVOKE_ASYNC(D, TYPE_PROC_REF(/datum, find_references)) - ref_searching = TRUE - #ifdef GC_FAILURE_HARD_LOOKUP - else - INVOKE_ASYNC(D, TYPE_PROC_REF(/datum, find_references)) - ref_searching = TRUE - #endif - reference_find_on_fail -= refID - #endif + #ifdef FAST_REFERENCE_TRACKING + var/skip = GLOB.reftracker_skip_typecache[D.type] + #else + var/skip = FALSE + #endif //ifdef FAST_REFERENCE_TRACKING + if(!skip) + // Decides how many refs to look for (potentially) + // Based off the remaining and the ones we can account for + var/remaining_refs = refcount(D) - REFS_WE_EXPECT + if(reference_find_on_fail[text_ref(D)]) + INVOKE_ASYNC(D, TYPE_PROC_REF(/datum,find_references), remaining_refs) + ref_searching = TRUE + #ifdef GC_FAILURE_HARD_LOOKUP + else + INVOKE_ASYNC(D, TYPE_PROC_REF(/datum,find_references), remaining_refs) + ref_searching = TRUE + #endif //ifdef GC_FAILURE_HARD_LOOKUP + reference_find_on_fail -= text_ref(D) + #endif //ifdef REFERENCE_TRACKING var/type = D.type var/datum/qdel_item/I = items[type] + var/detail = D.dump_harddel_info() var/message = "## TESTING: GC: -- [text_ref(D)] | [type] was unable to be GC'd --" message = "[message] (ref count of [refcount(D)])" + if(detail) + message = "[message] | [detail]" + LAZYADD(I.extra_details, detail) log_world(message) + #ifdef TESTING for(var/c in GLOB.admins) //Using testing() here would fill the logs with ADMIN_VV garbage var/client/admin = c if(!check_rights_for(admin, R_ADMIN)) continue to_chat(admin, "## TESTING: GC: -- [ADMIN_VV(D)] | [type] was unable to be GC'd --") - #endif + #endif //ifdef TESTING I.failures++ if (I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG) #ifdef REFERENCE_TRACKING if(ref_searching) return //ref searching intentionally cancels all further fires while running so things that hold references don't end up getting deleted, so we want to return here instead of continue - #endif + #endif //ifdef REFERENCE_TRACKING continue if (GC_QUEUE_HARDDELETE) if(!HardDelete(D)) @@ -244,7 +266,7 @@ SUBSYSTEM_DEF(garbage) #ifdef REFERENCE_TRACKING if(ref_searching) return - #endif + #endif //ifdef REFERENCE_TRACKING if (MC_TICK_CHECK) return @@ -252,19 +274,21 @@ SUBSYSTEM_DEF(garbage) queue.Cut(1,count+1) count = 0 +#undef REFS_WE_EXPECT + /datum/controller/subsystem/garbage/proc/Queue(datum/D, level = GC_QUEUE_FILTER) if (isnull(D)) return if (level > GC_QUEUE_COUNT) HardDelete(D) return - var/gctime = world.time - var/refid = "\ref[D]" + var/queue_time = world.time - D.gc_destroyed = gctime - var/list/queue = queues[level] + if (D.gc_destroyed <= 0) + D.gc_destroyed = queue_time - queue[++queue.len] = list(gctime, refid) // not += for byond reasons + var/list/queue = queues[level] + queue[++queue.len] = list(queue_time, D, D.gc_destroyed) // not += for byond reasons //this is mainly to separate things profile wise. /datum/controller/subsystem/garbage/proc/HardDelete(datum/D, override = FALSE) @@ -278,17 +302,20 @@ SUBSYSTEM_DEF(garbage) ++delslasttick ++totaldels var/type = D.type - var/refID = "\ref[D]" + var/refID = text_ref(D) + var/datum/qdel_item/type_info = items[type] + var/detail = D.dump_harddel_info() + if(detail) + LAZYADD(type_info.extra_details, detail) var/tick_usage = TICK_USAGE del(D) tick_usage = TICK_USAGE_TO_MS(tick_usage) - var/datum/qdel_item/I = items[type] - I.hard_deletes++ - I.hard_delete_time += tick_usage - if (tick_usage > I.hard_delete_max) - I.hard_delete_max = tick_usage + type_info.hard_deletes++ + type_info.hard_delete_time += tick_usage + if (tick_usage > type_info.hard_delete_max) + type_info.hard_delete_max = tick_usage if (tick_usage > highest_del_ms) highest_del_ms = tick_usage highest_del_type_string = "[type]" @@ -297,16 +324,16 @@ SUBSYSTEM_DEF(garbage) if (time > 0.1 SECONDS) postpone(time) - var/threshold = 0.5 //CONFIG_GET(number/hard_deletes_overrun_threshold) + var/threshold = CONFIG_GET(number/hard_deletes_overrun_threshold) if (threshold && (time > threshold SECONDS)) - if (!(I.qdel_flags & QDEL_ITEM_ADMINS_WARNED)) + if (!(type_info.qdel_flags & QDEL_ITEM_ADMINS_WARNED)) log_game("Error: [type]([refID]) took longer than [threshold] seconds to delete (took [round(time/10, 0.1)] seconds to delete)") message_admins("Error: [type]([refID]) took longer than [threshold] seconds to delete (took [round(time/10, 0.1)] seconds to delete).") - I.qdel_flags |= QDEL_ITEM_ADMINS_WARNED - I.hard_deletes_over_threshold++ - var/overrun_limit = 0 // CONFIG_GET(number/hard_deletes_overrun_limit) - if (overrun_limit && I.hard_deletes_over_threshold >= overrun_limit) - I.qdel_flags |= QDEL_ITEM_SUSPENDED_FOR_LAG + type_info.qdel_flags |= QDEL_ITEM_ADMINS_WARNED + type_info.hard_deletes_over_threshold++ + var/overrun_limit = CONFIG_GET(number/hard_deletes_overrun_limit) + if (overrun_limit && type_info.hard_deletes_over_threshold >= overrun_limit) + type_info.qdel_flags |= QDEL_ITEM_SUSPENDED_FOR_LAG /datum/controller/subsystem/garbage/Recover() InitQueues() //We first need to create the queues before recovering data @@ -328,85 +355,109 @@ SUBSYSTEM_DEF(garbage) var/no_hint = 0 //!Number of times it's not even bother to give a qdel hint var/slept_destroy = 0 //!Number of times it's slept in its destroy var/qdel_flags = 0 //!Flags related to this type's trip thru qdel. + var/list/extra_details //!Lazylist of string metadata about the deleted objects /datum/qdel_item/New(mytype) name = "[mytype]" +/proc/non_datum_qdel(to_delete) + var/found_type = "unable to determine type" + var/delable = FALSE + + if(islist(to_delete)) + found_type = "list" + delable = TRUE + + if(isnum(to_delete)) + found_type = "number" + + if(ispath(to_delete)) + found_type = "typepath" + + if(delable) + del(to_delete) + + CRASH("Bad qdel ([found_type])") /// Should be treated as a replacement for the 'del' keyword. /// /// Datums passed to this will be given a chance to clean up references to allow the GC to collect them. -/proc/qdel(datum/to_delete, force=FALSE, ...) +/proc/qdel(datum/to_delete, force = FALSE) if(!istype(to_delete)) if(isnull(to_delete)) return - else if(islist(to_delete)) - stack_trace("Lists should not be directly passed to qdel! You likely want either list.Cut(), QDEL_LIST(list), QDEL_LIST_ASSOC(list), or QDEL_LIST_ASSOC_VAL(list)") - else if(to_delete != world) - stack_trace("Tried to qdel possibly invalid value: [to_delete]") - del(to_delete) +// #ifndef DISABLE_DREAMLUAU +// DREAMLUAU_CLEAR_REF_USERDATA(to_delete) +// #endif + non_datum_qdel(to_delete) return - var/datum/qdel_item/I = SSgarbage.items[to_delete.type] - if (!I) - I = SSgarbage.items[to_delete.type] = new /datum/qdel_item(to_delete.type) - I.qdels++ + var/datum/qdel_item/trash = SSgarbage.items[to_delete.type] + if (isnull(trash)) + trash = SSgarbage.items[to_delete.type] = new /datum/qdel_item(to_delete.type) + trash.qdels++ - if(isnull(to_delete.gc_destroyed)) - if (SEND_SIGNAL(to_delete, COMSIG_PREQDELETED, force)) // Give the components a chance to prevent their parent from being deleted - return - to_delete.gc_destroyed = GC_CURRENTLY_BEING_QDELETED - var/start_time = world.time - var/start_tick = world.tick_usage - SEND_SIGNAL(to_delete, COMSIG_QDELETING, force) // Let the (remaining) components know about the result of Destroy - var/hint = to_delete.Destroy(arglist(args.Copy(2))) // Let our friend know they're about to get fucked up. - if(world.time != start_time) - I.slept_destroy++ - else - I.destroy_time += TICK_USAGE_TO_MS(start_tick) - if(!to_delete) + if(!isnull(to_delete.gc_destroyed)) + if(to_delete.gc_destroyed == GC_CURRENTLY_BEING_QDELETED) + CRASH("[to_delete.type] destroy proc was called multiple times, likely due to a qdel loop in the Destroy logic") + return + + if (SEND_SIGNAL(to_delete, COMSIG_PREQDELETED, force)) // Give the components a chance to prevent their parent from being deleted + return + + to_delete.gc_destroyed = GC_CURRENTLY_BEING_QDELETED + var/start_time = world.time + var/start_tick = world.tick_usage + SEND_SIGNAL(to_delete, COMSIG_QDELETING, force) // Let the (remaining) components know about the result of Destroy + var/hint = to_delete.Destroy(force) // Let our friend know they're about to get fucked up. + + if(world.time != start_time) + trash.slept_destroy++ + else + trash.destroy_time += TICK_USAGE_TO_MS(start_tick) + + if(isnull(to_delete)) + return + + switch(hint) + if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion. + SSgarbage.Queue(to_delete) + if (QDEL_HINT_IWILLGC) + to_delete.gc_destroyed = world.time return - switch(hint) - if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion. - SSgarbage.Queue(to_delete) - if (QDEL_HINT_IWILLGC) - to_delete.gc_destroyed = world.time + if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory. + if(!force) + to_delete.gc_destroyed = null //clear the gc variable (important!) return - if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory. - if(!force) - to_delete.gc_destroyed = null //clear the gc variable (important!) - return - // Returning LETMELIVE after being told to force destroy - // indicates the objects Destroy() does not respect force - #ifdef TESTING - if(!I.no_respect_force) - testing("WARNING: [to_delete.type] has been force deleted, but is \ - returning an immortal QDEL_HINT, indicating it does \ - not respect the force flag for qdel(). It has been \ - placed in the queue, further instances of this type \ - will also be queued.") - #endif - I.no_respect_force++ - - SSgarbage.Queue(to_delete) - if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete - SSgarbage.Queue(to_delete, GC_QUEUE_HARDDELETE) - if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste. - SSgarbage.HardDelete(to_delete) - #ifdef REFERENCE_TRACKING - if (QDEL_HINT_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled, display all references to this object, then queue the object for deletion. - SSgarbage.Queue(to_delete) - to_delete.find_references() //This breaks ci. Consider it insurance against somehow pring reftracking on accident - if (QDEL_HINT_IFFAIL_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled and the object fails to collect, display all references to this object. - SSgarbage.Queue(to_delete) - SSgarbage.reference_find_on_fail["\ref[to_delete]"] = TRUE + // Returning LETMELIVE after being told to force destroy + // indicates the objects Destroy() does not respect force + #ifdef TESTING + if(!trash.no_respect_force) + testing("WARNING: [to_delete.type] has been force deleted, but is \ + returning an immortal QDEL_HINT, indicating it does \ + not respect the force flag for qdel(). It has been \ + placed in the queue, further instances of this type \ + will also be queued.") #endif - else - #ifdef TESTING - if(!I.no_hint) - testing("WARNING: [to_delete.type] is not returning a qdel hint. It is being placed in the queue. Further instances of this type will also be queued.") - #endif - I.no_hint++ - SSgarbage.Queue(to_delete) - else if(to_delete.gc_destroyed == GC_CURRENTLY_BEING_QDELETED) - CRASH("[to_delete.type] destroy proc was called multiple times, likely due to a qdel loop in the Destroy logic") + trash.no_respect_force++ + + SSgarbage.Queue(to_delete) + if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete + SSgarbage.Queue(to_delete, GC_QUEUE_HARDDELETE) + if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste. + SSgarbage.HardDelete(to_delete, override = TRUE) + #ifdef REFERENCE_TRACKING + if (QDEL_HINT_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled, display all references to this object, then queue the object for deletion. + SSgarbage.HardDelete(to_delete, override = TRUE) // Need to override enable_hard_deletes, stuff like /client uses this + INVOKE_ASYNC(to_delete, TYPE_PROC_REF(/datum, find_references)) + if (QDEL_HINT_IFFAIL_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled and the object fails to collect, display all references to this object. + SSgarbage.Queue(to_delete) + SSgarbage.reference_find_on_fail[text_ref(to_delete)] = TRUE + #endif + else + #ifdef TESTING + if(!trash.no_hint) + testing("WARNING: [to_delete.type] is not returning a qdel hint. It is being placed in the queue. Further instances of this type will also be queued.") + #endif + trash.no_hint++ + SSgarbage.Queue(to_delete) diff --git a/code/controllers/subsystems/tgui.dm b/code/controllers/subsystems/tgui.dm index e75b9c455a0..0727bf5d3e3 100644 --- a/code/controllers/subsystems/tgui.dm +++ b/code/controllers/subsystems/tgui.dm @@ -15,7 +15,7 @@ SUBSYSTEM_DEF(tgui) wait = 9 flags = SS_NO_INIT priority = FIRE_PRIORITY_TGUI - runlevels = RUNLEVEL_INIT | RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT + runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT /// A list of UIs scheduled to process var/list/current_run = list() @@ -39,6 +39,25 @@ SUBSYSTEM_DEF(tgui) basehtml = replacetextEx(basehtml, "", "Nanotrasen NeoTheology (c) 2525-[CURRENT_SHIP_YEAR]") +/datum/controller/subsystem/tgui/OnConfigLoad() + var/storage_iframe = CONFIG_GET(string/storage_cdn_iframe) + + if(storage_iframe && storage_iframe != /datum/config_entry/string/storage_cdn_iframe::default) + basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe) + return + + if(CONFIG_GET(string/asset_transport) == "webroot") + var/datum/asset_transport/webroot/webroot = SSassets.transport + + var/datum/asset_cache_item/item = webroot.register_asset("iframe.html", file("tgui/public/iframe.html")) + basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", webroot.get_asset_url("iframe.html", item)) + return + + if(!storage_iframe) + return + + basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe) + /datum/controller/subsystem/tgui/Shutdown() close_all_uis() @@ -52,14 +71,15 @@ SUBSYSTEM_DEF(tgui) 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] + var/seconds_per_tick = wait * 0.1 + while(length(current_run)) + var/datum/tgui/ui = current_run[length(current_run)] current_run.len-- // TODO: Move user/src_object check to process() if(ui?.user && ui.src_object) - ui.Process(wait * 0.1) + ui.Process(seconds_per_tick) else - ui.close(0) + ui.close(FALSE) if(MC_TICK_CHECK) return @@ -70,7 +90,7 @@ SUBSYSTEM_DEF(tgui) * Returns null if pool was exhausted. * * required user mob - * return datum/tgui_window + * return datum/tgui */ /datum/controller/subsystem/tgui/proc/request_pooled_window(mob/user) if(!user.client) @@ -96,8 +116,10 @@ SUBSYSTEM_DEF(tgui) window_found = TRUE break if(!window_found) +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(user, "Error: Pool exhausted", context = "SStgui/request_pooled_window") +#endif return null return window @@ -109,7 +131,9 @@ SUBSYSTEM_DEF(tgui) * required user mob */ /datum/controller/subsystem/tgui/proc/force_close_all_windows(mob/user) +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(user, context = "SStgui/force_close_all_windows") +#endif if(user.client) user.client.tgui_windows = list() for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT) @@ -125,11 +149,15 @@ SUBSYSTEM_DEF(tgui) * required window_id string */ /datum/controller/subsystem/tgui/proc/force_close_window(mob/user, window_id) +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(user, context = "SStgui/force_close_window") +#endif // 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) + // Unset machine just to be sure. + user.unset_machine() // Close window directly just to be sure. user << browse(null, "window=[window_id]") diff --git a/code/datums/datum.dm b/code/datums/datum.dm index f9addfad18e..05ac8324ea9 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -17,7 +17,7 @@ */ var/gc_destroyed - /// Open tguis owned by this datum + /// Open uis owned by this datum /// Lazy, since this case is semi rare var/list/open_uis @@ -29,16 +29,16 @@ /** * Components attached to this datum * - * Lazy associated list in the structure of `type:component/list of components` + * Lazy associated list in the structure of `type -> component/list of components` */ var/list/_datum_components /** * Any datum registered to receive signals from this datum is in this list * - * Lazy associated list in the structure of `[signal] = list(registered_objects)` + * Lazy associated list in the structure of `signal -> registree/list of registrees` */ var/list/_listen_lookup - /// Lazy associated list in the structure of `[target][signal] = proc)` that are run when the datum receives that signal + /// Lazy associated list in the structure of `target -> list(signal -> proctype)` that are run when the datum receives that signal var/list/list/datum/callback/_signal_procs var/signal_enabled = FALSE @@ -58,15 +58,44 @@ var/list/cooldowns + /// List for handling persistent filters. + var/list/filter_data + /// An accursed beast of a list that contains our filters. Why? Because var/list/filters on atoms/images isn't actually a list + /// but a snowflaked skinwalker pretending to be one, which doesn't support half the list procs/operations and the other half behaves weirdly + /// so we cut down on filter creation and appearance update costs by editing *this* list, and then assigning ours to it + var/list/filter_cache + #ifdef REFERENCE_TRACKING var/running_find_references + /// When was this datum last touched by a reftracker? + /// If this value doesn't match with the start of the search + /// We know this datum has never been seen before, and we should check it var/last_find_references = 0 + /// How many references we're trying to find when searching + var/references_to_clear = 0 #ifdef REFERENCE_TRACKING_DEBUG ///Stores info about where refs are found, used for sanity checks and testing var/list/found_refs #endif #endif + // If we have called dump_harddel_info already. Used to avoid duped calls (since we call it immediately in some cases on failure to process) + // Create and destroy is weird and I wanna cover my bases + var/harddel_deets_dumped = FALSE + +#ifdef DATUMVAR_DEBUGGING_MODE + var/list/cached_vars +#endif + +/** + * Called when a href for this datum is clicked + * + * Sends a [COMSIG_TOPIC] signal + */ +/datum/Topic(href, href_list[]) + ..() + SEND_SIGNAL(src, COMSIG_TOPIC, usr, href_list) + /** * Default implementation of clean-up code. * @@ -143,6 +172,33 @@ for(var/target in _signal_procs) UnregisterSignal(target, _signal_procs[target]) +#ifdef DATUMVAR_DEBUGGING_MODE +/datum/proc/save_vars() + cached_vars = list() + for(var/i in vars) + if(i == "cached_vars") + continue + cached_vars[i] = vars[i] + +/datum/proc/check_changed_vars() + . = list() + for(var/i in vars) + if(i == "cached_vars") + continue + if(cached_vars[i] != vars[i]) + .[i] = list(cached_vars[i], vars[i]) + +/datum/proc/txt_changed_vars() + var/list/l = check_changed_vars() + var/t = "[src]([REF(src)]) changed vars:" + for(var/i in l) + t += "\"[i]\" \[[l[i][1]]\] --> \[[l[i][2]]\] " + t += "." + +/datum/proc/to_chat_check_changed_vars(target = world) + to_chat(target, txt_changed_vars()) +#endif + /// Return a list of data which can be used to investigate the datum, also ensure that you set the semver in the options list /datum/proc/serialize_list(list/options, list/semvers) SHOULD_CALL_PARENT(TRUE) @@ -182,6 +238,37 @@ jsonlist["DATUM_TYPE"] = D.type return json_encode(jsonlist) +/// Convert a list of json to datum +/proc/json_deserialize_datum(list/jsonlist, list/options, target_type, strict_target_type = FALSE) + if(!islist(jsonlist)) + if(!istext(jsonlist)) + CRASH("Invalid JSON") + jsonlist = json_decode(jsonlist) + if(!islist(jsonlist)) + CRASH("Invalid JSON") + if(!jsonlist["DATUM_TYPE"]) + return + if(!ispath(jsonlist["DATUM_TYPE"])) + if(!istext(jsonlist["DATUM_TYPE"])) + return + jsonlist["DATUM_TYPE"] = text2path(jsonlist["DATUM_TYPE"]) + if(!ispath(jsonlist["DATUM_TYPE"])) + return + if(target_type) + if(!ispath(target_type)) + return + if(strict_target_type) + if(target_type != jsonlist["DATUM_TYPE"]) + return + else if(!ispath(jsonlist["DATUM_TYPE"], target_type)) + return + var/typeofdatum = jsonlist["DATUM_TYPE"] //BYOND won't directly read if this is just put in the line below, and will instead runtime because it thinks you're trying to make a new list? + var/datum/D = new typeofdatum + if(!D.deserialize_list(jsonlist, options)) + qdel(D) + else + return D + /** * Callback called by a timer to end an associative-list-indexed cooldown. * @@ -213,65 +300,22 @@ SEND_SIGNAL(source, COMSIG_CD_RESET(index), S_TIMER_COOLDOWN_TIMELEFT(source, index)) TIMER_COOLDOWN_END(source, index) -/datum/proc/CanProcCall(procname) - return TRUE - -/datum/proc/can_vv_get(var_name) - if(var_name == NAMEOF(src, vars)) - return FALSE - return TRUE - -/// Called when a var is edited with the new value to change to -/datum/proc/vv_edit_var(var_name, var_value) - if(var_name == NAMEOF(src, vars)) - return FALSE - vars[var_name] = var_value - datum_flags |= DF_VAR_EDITED - return TRUE - -/datum/proc/vv_get_var(var_name) - switch(var_name) - if (NAMEOF(src, vars)) - return debug_variable(var_name, list(), 0, src) - return debug_variable(var_name, vars[var_name], 0, src) - -/datum/proc/can_vv_mark() - return TRUE - -/** - * Gets all the dropdown options in the vv menu. - * When overriding, make sure to call . = ..() first and appent to the result, that way parent items are always at the top and child items are further down. - * Add seperators by doing VV_DROPDOWN_OPTION("", "---") - */ -/datum/proc/vv_get_dropdown() - SHOULD_CALL_PARENT(TRUE) - - . = list() - VV_DROPDOWN_OPTION("", "---") - VV_DROPDOWN_OPTION(VV_HK_CALLPROC, "Call Proc") - VV_DROPDOWN_OPTION(VV_HK_MARK, "Mark Object") - VV_DROPDOWN_OPTION(VV_HK_TAG, "Tag Datum") - VV_DROPDOWN_OPTION(VV_HK_DELETE, "Delete") - VV_DROPDOWN_OPTION(VV_HK_EXPOSE, "Show VV To Player") - VV_DROPDOWN_OPTION(VV_HK_ADDCOMPONENT, "Add Component/Element") - VV_DROPDOWN_OPTION(VV_HK_REMOVECOMPONENT, "Remove Component/Element") - VV_DROPDOWN_OPTION(VV_HK_MASS_REMOVECOMPONENT, "Mass Remove Component/Element") - -/** - * This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks! - * href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables! - * This proc is for "high level" actions like admin heal/set species/etc/etc. The low level debugging things should go in admin/view_variables/topic_basic.dm incase this runtimes. - */ -/datum/proc/vv_do_topic(list/href_list) - if(!usr || !usr.client || !usr.client.holder || !check_rights(NONE)) - return FALSE //This is VV, not to be called by anything else. - if(SEND_SIGNAL(src, COMSIG_VV_TOPIC, usr, href_list) & COMPONENT_VV_HANDLED) - return FALSE - // if(href_list[VV_HK_MODIFY_TRAITS]) - // usr.client.holder.modify_traits(src) - return TRUE - -/datum/proc/vv_get_header() - . = list() - if(("name" in vars) && !isatom(src)) - . += "[vars["name"]]
" +///Generate a tag for this /datum, if it implements one +///Should be called as early as possible, best would be in New, to avoid weakref mistargets +///Really just don't use this, you don't need it, global lists will do just fine MOST of the time +///We really only use it for mobs to make id'ing people easier +/datum/proc/GenerateTag() + datum_flags |= DF_USE_TAG + +/// Return text from this proc to provide extra context to hard deletes that happen to it +/// Optional, you should use this for cases where replication is difficult and extra context is required +/// Can be called more then once per object, use harddel_deets_dumped to avoid duplicate calls (I am so sorry) +/datum/proc/dump_harddel_info() + return + +///images are pretty generic, this should help a bit with tracking harddels related to them +/image/dump_harddel_info() + if(harddel_deets_dumped) + return + harddel_deets_dumped = TRUE + return "Image icon: [icon] - icon_state: [icon_state] [loc ? "loc: [loc] ([loc.x],[loc.y],[loc.z])" : ""]" diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm new file mode 100644 index 00000000000..7f8923082c5 --- /dev/null +++ b/code/datums/datumvars.dm @@ -0,0 +1,62 @@ +/datum/proc/CanProcCall(procname) + return TRUE + +/datum/proc/can_vv_get(var_name) + if(var_name == NAMEOF(src, vars)) + return FALSE + return TRUE + +/// Called when a var is edited with the new value to change to +/datum/proc/vv_edit_var(var_name, var_value) + if(var_name == NAMEOF(src, vars)) + return FALSE + vars[var_name] = var_value + datum_flags |= DF_VAR_EDITED + return TRUE + +/datum/proc/vv_get_var(var_name) + switch(var_name) + if (NAMEOF(src, vars)) + return debug_variable(var_name, list(), 0, src) + return debug_variable(var_name, vars[var_name], 0, src) + +/datum/proc/can_vv_mark() + return TRUE + +/** + * Gets all the dropdown options in the vv menu. + * When overriding, make sure to call . = ..() first and appent to the result, that way parent items are always at the top and child items are further down. + * Add seperators by doing VV_DROPDOWN_OPTION("", "---") + */ +/datum/proc/vv_get_dropdown() + SHOULD_CALL_PARENT(TRUE) + + . = list() + VV_DROPDOWN_OPTION("", "---") + VV_DROPDOWN_OPTION(VV_HK_CALLPROC, "Call Proc") + VV_DROPDOWN_OPTION(VV_HK_MARK, "Mark Object") + VV_DROPDOWN_OPTION(VV_HK_TAG, "Tag Datum") + VV_DROPDOWN_OPTION(VV_HK_DELETE, "Delete") + VV_DROPDOWN_OPTION(VV_HK_EXPOSE, "Show VV To Player") + VV_DROPDOWN_OPTION(VV_HK_ADDCOMPONENT, "Add Component/Element") + VV_DROPDOWN_OPTION(VV_HK_REMOVECOMPONENT, "Remove Component/Element") + VV_DROPDOWN_OPTION(VV_HK_MASS_REMOVECOMPONENT, "Mass Remove Component/Element") + +/** + * This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks! + * href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables! + * This proc is for "high level" actions like admin heal/set species/etc/etc. The low level debugging things should go in admin/view_variables/topic_basic.dm incase this runtimes. + */ +/datum/proc/vv_do_topic(list/href_list) + if(!usr || !usr.client || !usr.client.holder || !check_rights(NONE)) + return FALSE //This is VV, not to be called by anything else. + if(SEND_SIGNAL(src, COMSIG_VV_TOPIC, usr, href_list) & COMPONENT_VV_HANDLED) + return FALSE + // if(href_list[VV_HK_MODIFY_TRAITS]) + // usr.client.holder.modify_traits(src) + return TRUE + +/datum/proc/vv_get_header() + . = list() + if(("name" in vars) && !isatom(src)) + . += "[vars["name"]]
" diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 1ed733afea9..a7313c42bc9 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -194,10 +194,6 @@ update_openspace() return ..() -/// Generate a tag for this atom -/atom/proc/GenerateTag() - return - /atom/proc/reveal_blood() return diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 6e52af118aa..6a310d976d4 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -42,7 +42,7 @@ * * required user mob The mob interacting with the UI. * - * return list Static Data to be sent to the UI. + * return list Statuic Data to be sent to the UI. */ /datum/proc/ui_static_data(mob/user) return list() @@ -86,14 +86,10 @@ */ /datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) SHOULD_CALL_PARENT(TRUE) - SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action, params) + SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action) // If UI is not interactive or usr calling Topic is not the UI user, bail. if(!ui || ui.status != UI_INTERACTIVE) return TRUE - // if(action == "change_ui_state") - // var/mob/living/user = ui.user - // //write_preferences will make sure it's valid for href exploits. - // user.client.prefs.write_preference(GLOB.preference_entries[layout_prefs_used], params["new_state"]) /** * public @@ -217,9 +213,11 @@ if(window_id) window = usr.client.tgui_windows[window_id] if(!window) +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(usr, "Error: Couldn't find the window datum, force closing.", context = window_id) +#endif SStgui.force_close_window(usr, window_id) return TRUE diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 1a966a241b8..7080f45c171 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -37,6 +37,11 @@ var/datum/ui_state/state = null /// Rate limit client refreshes to prevent DoS. COOLDOWN_DECLARE(refresh_cooldown) + /// Are byond mouse events beyond the window passed in to the ui + var/mouse_hooked = FALSE + + /// The id of any ByondUi elements that we have opened + var/list/open_byondui_elements /** * public @@ -53,9 +58,11 @@ * return datum/tgui The requested UI. */ /datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y) +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(user, "new [interface] fancy [user?.client?.get_preference_value(/datum/client_preference/tgui_fancy)]", src_object = src_object) +#endif src.user = user src.src_object = src_object src.window_key = "[REF(src_object)]-main" @@ -80,7 +87,7 @@ * return bool - TRUE if a new pooled window is opened, FALSE in all other situations including if a new pooled window didn't open because one already exists. */ /datum/tgui/proc/open() - if(!user.client) + if(!user?.client) return FALSE if(window) return FALSE @@ -105,6 +112,8 @@ window.send_message("update", get_payload( with_data = TRUE, with_static_data = TRUE)) + if(mouse_hooked) + window.set_mouse_macro() SStgui.on_open(src) return TRUE @@ -142,9 +151,26 @@ window.close(can_be_suspended) src_object.ui_close(user) SStgui.on_close(src) + + if(user.client) + terminate_byondui_elements() + state = null qdel(src) +/** + * public + * + * Closes all ByondUI elements, left dangling by a forceful TGUI exit, + * such as via Alt+F4, closing in non-fancy mode, or terminating the process + * + */ +/datum/tgui/proc/terminate_byondui_elements() + set waitfor = FALSE + + for(var/byondui_element in open_byondui_elements) + winset(user.client, byondui_element, list("parent" = "")) + /** * public * @@ -155,6 +181,18 @@ /datum/tgui/proc/set_autoupdate(autoupdate) src.autoupdate = autoupdate +/** + * public + * + * Enable/disable passing through byond mouse events to the window + * + * required value bool Enable/disable hooking. + */ +/datum/tgui/proc/set_mouse_hook(value) + src.mouse_hooked = value + //Handle unhooking/hooking on already open windows ? + + /** * public * @@ -188,7 +226,7 @@ * optional force bool Send an update even if UI is not interactive. */ /datum/tgui/proc/send_full_update(custom_data, force) - if(!user.client || !initialized || closing) + if(!user?.client || !initialized || closing) return if(!COOLDOWN_FINISHED(src, refresh_cooldown)) refreshing = TRUE @@ -211,7 +249,7 @@ * optional force bool Send an update even if UI is not interactive. */ /datum/tgui/proc/send_update(custom_data, force) - if(!user.client || !initialized || closing) + if(!user?.client || !initialized || closing) return var/should_update_data = force || status >= UI_UPDATE window.send_message("update", get_payload( @@ -244,23 +282,23 @@ "scale" = user.client.get_preference_value(/datum/client_preference/ui_scale), ), "client" = list( - "ckey" = user.client.ckey, - "address" = user.client.address, - "computer_id" = user.client.computer_id, + "ckey" = user.client?.ckey, + "address" = user.client?.address, + "computer_id" = user.client?.computer_id, ), "user" = list( "name" = "[user]", "observer" = isobserver(user), ), ) - var/data = custom_data || with_data && src_object.ui_data(user) + var/data = custom_data || with_data && src_object?.ui_data(user) if(data) json_data["data"] = data - var/static_data = with_static_data && src_object.ui_static_data(user) + var/static_data = with_static_data && src_object?.ui_static_data(user) if(static_data) json_data["static_data"] = static_data - if(src_object.tgui_shared_states) - json_data["shared"] = src_object.tgui_shared_states + if(src_object?.tgui_shared_states) + json_data["shared"] = src_object?.tgui_shared_states return json_data /** @@ -279,9 +317,11 @@ return // Validate ping if(!initialized && world.time - opened_at > TGUI_PING_TIMEOUT) +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(user, "Error: Zombie window detected, closing.", window = window, src_object = src_object) +#endif close(can_be_suspended = FALSE) return // Update through a normal call to ui_interact @@ -342,6 +382,18 @@ LAZYINITLIST(src_object.tgui_shared_states) src_object.tgui_shared_states[href_list["key"]] = href_list["value"] SStgui.update_uis(src_object) + if(TGUI_MANAGED_BYONDUI_TYPE_RENDER) + var/byond_ui_id = payload[TGUI_MANAGED_BYONDUI_PAYLOAD_ID] + if(!byond_ui_id || LAZYLEN(open_byondui_elements) > TGUI_MANAGED_BYONDUI_LIMIT) + return + + LAZYOR(open_byondui_elements, byond_ui_id) + if(TGUI_MANAGED_BYONDUI_TYPE_UNMOUNT) + var/byond_ui_id = payload[TGUI_MANAGED_BYONDUI_PAYLOAD_ID] + if(!byond_ui_id) + return + + LAZYREMOVE(open_byondui_elements, byond_ui_id) /// Wrapper for behavior to potentially wait until the next tick if the server is overloaded /datum/tgui/proc/on_act_message(act_type, payload, state) diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index 38c49548b64..f12075d322f 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -25,6 +25,19 @@ var/initial_inline_html var/initial_inline_js var/initial_inline_css + var/mouse_event_macro_set = FALSE + + /** + * Static list used to map in macros that will then emit execute events to the tgui window + * A small disclaimer though I'm no tech wiz: I don't think it's possible to map in right or middle + * clicks in the current state, as they're keywords rather than modifiers. + */ + var/static/list/byondToTguiEventMap = list( + "MouseDown" = "byond/mousedown", + "MouseUp" = "byond/mouseup", + "Ctrl" = "byond/ctrldown", + "Ctrl+UP" = "byond/ctrlup", + ) var/list/oversized_payloads = list() @@ -65,10 +78,12 @@ inline_html = "", inline_js = "", inline_css = "") +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(client, context = "[id]/initialize", window = src) - if(!client) +#endif + if(QDELETED(client)) return src.initial_fancy = fancy src.initial_assets = assets @@ -119,7 +134,7 @@ // Detect whether the control is a browser is_browser = winexists(client, id) == "BROWSER" // Instruct the client to signal UI when the window is closed. - if(!is_browser) + if(!is_browser && !QDELETED(client)) // monkestation: extra anti-runtime checks winset(client, id, "on-close=\"uiclose [id]\"") /** @@ -222,17 +237,23 @@ /datum/tgui_window/proc/close(can_be_suspended = TRUE) if(!client) return + if(mouse_event_macro_set) + remove_mouse_macro() if(can_be_suspended && can_be_suspended()) +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(client, context = "[id]/close (suspending)", window = src) +#endif visible = FALSE status = TGUI_WINDOW_READY send_message("suspend") return +#ifdef EXTENDED_DEBUG_LOGGING log_tgui(client, context = "[id]/close", window = src) +#endif release_lock() visible = FALSE status = TGUI_WINDOW_CLOSED @@ -307,7 +328,6 @@ var/datum/asset/spritesheet_batched/spritesheet = asset send_message("asset/stylesheet", spritesheet.css_filename()) send_raw_message(asset.get_serialized_url_mappings()) - /** * private * @@ -397,6 +417,35 @@ /datum/tgui_window/vv_edit_var(var_name, var_value) return var_name != NAMEOF(src, id) && ..() + +/datum/tgui_window/proc/set_mouse_macro() + if(mouse_event_macro_set) + return + + for(var/mouseMacro in byondToTguiEventMap) + var/command_template = ".output CONTROL PAYLOAD" + var/event_message = TGUI_CREATE_MESSAGE(byondToTguiEventMap[mouseMacro], null) + var target_control = is_browser \ + ? "[id]:update" \ + : "[id].browser:update" + var/with_id = replacetext(command_template, "CONTROL", target_control) + var/full_command = replacetext(with_id, "PAYLOAD", event_message) + + var/list/params = list() + params["parent"] = "default" //Technically this is external to tgui but whatever + params["name"] = mouseMacro + params["command"] = full_command + + winset(client, "[mouseMacro]Window[id]Macro", params) + mouse_event_macro_set = TRUE + +/datum/tgui_window/proc/remove_mouse_macro() + if(!mouse_event_macro_set) + stack_trace("Unsetting mouse macro on tgui window that has none") + for(var/mouseMacro in byondToTguiEventMap) + winset(client, null, "[mouseMacro]Window[id]Macro.parent=null") + mouse_event_macro_set = FALSE + /datum/tgui_window/proc/create_oversized_payload(payload_id, message_type, chunk_count) if(oversized_payloads[payload_id]) stack_trace("Attempted to create oversized tgui payload with duplicate ID.") @@ -414,7 +463,7 @@ return var/list/chunks = payload["chunks"] chunks += chunk - if(length(chunks) >= payload["count"]) + if(chunks.len >= payload["count"]) deltimer(payload["timeout"]) var/message_type = payload["type"] var/final_payload = chunks.Join() diff --git a/code/modules/tgui_panel/telemetry.dm b/code/modules/tgui_panel/telemetry.dm index aceccc7fa9d..5b30d26ebe9 100644 --- a/code/modules/tgui_panel/telemetry.dm +++ b/code/modules/tgui_panel/telemetry.dm @@ -12,15 +12,14 @@ /** * Maximum time allocated for sending a telemetry packet. */ -#define TGUI_TELEMETRY_RESPONSE_WINDOW 30 SECONDS +#define TGUI_TELEMETRY_RESPONSE_WINDOW (30 SECONDS) /// Time of telemetry request -/datum/tgui_panel - var/telemetry_requested_at - /// Time of telemetry analysis completion - var/telemetry_analyzed_at - /// List of previous client connections - var/list/telemetry_connections +/datum/tgui_panel/var/telemetry_requested_at +/// Time of telemetry analysis completion +/datum/tgui_panel/var/telemetry_analyzed_at +/// List of previous client connections +/datum/tgui_panel/var/list/telemetry_connections /** * private @@ -109,9 +108,7 @@ if(found) var/msg = "[key_name(client)] has a banned account in connection history! (Matched: [found["ckey"]], [found["address"]], [found["computer_id"]])" message_admins(msg) - // log_admin_private(msg) - log_admin(msg) - log_suspicious_login(msg, access_log_mirror = FALSE) + log_admin_private(msg) log_suspicious_login(msg, access_log_mirror = FALSE) // Only log them all at the end, since it's not as important as reporting an evader @@ -131,7 +128,7 @@ :computer_id, :round_id, :round_id - ) ON DUPLICATE KEY UPDATE [format_table_name("latest_round_id")] = :round_id + ) ON DUPLICATE KEY UPDATE latest_round_id = :round_id "}, list( "ckey" = ckey, "telemetry_ckey" = one_query["telemetry_ckey"], @@ -141,3 +138,6 @@ )) query.Execute() qdel(query) + +#undef TGUI_TELEMETRY_MAX_CONNECTIONS +#undef TGUI_TELEMETRY_RESPONSE_WINDOW diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index 4baf7f2e18f..899337a1654 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -62,7 +62,7 @@ */ /datum/tgui_panel/proc/on_initialize_timed_out() // Currently does nothing but sending a message to old chat. - SEND_TEXT(client, "Failed to load fancy chat, click HERE to attempt to reload it.") + SEND_TEXT(client, "Failed to load fancy chat, click HERE to attempt to reload it.") /** * private diff --git a/config/example/resources.txt b/config/example/resources.txt index e20ab1cf518..3e3dac38e5b 100644 --- a/config/example/resources.txt +++ b/config/example/resources.txt @@ -67,3 +67,12 @@ SMART_CACHE_ASSETS ## Tgui payloads larger than the 2kb limit for BYOND topic requests are split into roughly 1kb chunks and sent in sequence. ## This config option limits the maximum chunk count for which the server will accept a payload, default is 32 TGUI_MAX_CHUNK_COUNT 32 + +# If configured, this allows server operators to define the persistent origin used for clientside +# storage. This must host the same file as available in tgui/public/iframe.html. This is also hosted +# on the GitHub Pages site for the /tg/station repository, so does not need to be configured. +# If multiple servers use the same domain name, clientside features such as message saving +# and chat tabs will be persistent across both. +# If this setting is not configured, but the webroot CDN is, that will be used instead of GitHub Pages. +# If this setting is mpty, and the webroot CDN is disabled, byondstorage will be used. +# STORAGE_CDN_IFRAME https://tgstation.github.io/byond-client-storage/iframe.html diff --git a/tgui/global.d.ts b/tgui/global.d.ts index cfc7920e820..0579f0d58b8 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -63,6 +63,11 @@ type ByondType = { */ strictMode: boolean; + /** + * The external URL for the IndexedDB IFrame to use as the origin + */ + storageCdn: string; + /** * Makes a BYOND call. * diff --git a/tgui/package.json b/tgui/package.json index 788a4c5240b..a971b8e9c73 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -36,8 +36,8 @@ "jsdom": "^26.0.0", "prettier": "^3.2.5", "sass": "^1.80.6", - "sass-embedded": "^1.85.1", - "sass-loader": "^16.0.3", + "sass-embedded": "^1.89.2", + "sass-loader": "^16.0.5", "typescript": "^5.6.3", "url-loader": "^4.1.1" }, diff --git a/tgui/packages/common/storage.ts b/tgui/packages/common/storage.ts index 427e8b8ffce..b67b115a9f3 100644 --- a/tgui/packages/common/storage.ts +++ b/tgui/packages/common/storage.ts @@ -6,10 +6,12 @@ * @license MIT */ -export const IMPL_MEMORY = 0; export const IMPL_HUB_STORAGE = 1; +export const IMPL_IFRAME_INDEXED_DB = 2; -type StorageImplementation = typeof IMPL_MEMORY | typeof IMPL_HUB_STORAGE; +type StorageImplementation = + | typeof IMPL_HUB_STORAGE + | typeof IMPL_IFRAME_INDEXED_DB; type StorageBackend = { impl: StorageImplementation; @@ -31,57 +33,104 @@ const testHubStorage = testGeneric( () => window.hubStorage && !!window.hubStorage.getItem, ); -class MemoryBackend implements StorageBackend { - private store: Record; +class HubStorageBackend implements StorageBackend { public impl: StorageImplementation; constructor() { - this.impl = IMPL_MEMORY; - this.store = {}; + this.impl = IMPL_HUB_STORAGE; } async get(key: string): Promise { - return this.store[key]; + const value = await window.hubStorage.getItem(key); + if (typeof value === 'string') { + return JSON.parse(value); + } + return undefined; } async set(key: string, value: any): Promise { - this.store[key] = value; + window.hubStorage.setItem(key, JSON.stringify(value)); } async remove(key: string): Promise { - this.store[key] = undefined; + window.hubStorage.removeItem(key); } async clear(): Promise { - this.store = {}; + window.hubStorage.clear(); } } -class HubStorageBackend implements StorageBackend { +class IFrameIndexedDbBackend implements StorageBackend { public impl: StorageImplementation; + private documentElement: HTMLIFrameElement; + private iframeWindow: Window; + constructor() { - this.impl = IMPL_HUB_STORAGE; + this.impl = IMPL_IFRAME_INDEXED_DB; + } + + async ready(): Promise { + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = Byond.storageCdn; + + const completePromise: Promise = new Promise((resolve) => { + fetch(Byond.storageCdn, { method: 'HEAD' }) + .then((response) => { + if (response.status !== 200) { + resolve(false); + } + }) + .catch(() => { + resolve(false); + }); + + window.addEventListener('message', (message) => { + if (message.data === 'ready') { + resolve(true); + } + }); + }); + + this.documentElement = document.body.appendChild(iframe); + if (!this.documentElement.contentWindow) { + return new Promise((res) => res(false)); + } + + this.iframeWindow = this.documentElement.contentWindow; + + return completePromise; } async get(key: string): Promise { - const value = await window.hubStorage.getItem(key); - if (typeof value === 'string') { - return JSON.parse(value); - } - return undefined; + const promise = new Promise((resolve) => { + window.addEventListener('message', (message) => { + if (message.data.key && message.data.key === key) { + resolve(message.data.value); + } + }); + }); + + this.iframeWindow.postMessage({ type: 'get', key: key }, '*'); + return promise; } async set(key: string, value: any): Promise { - window.hubStorage.setItem(key, JSON.stringify(value)); + this.iframeWindow.postMessage({ type: 'set', key: key, value: value }, '*'); } async remove(key: string): Promise { - window.hubStorage.removeItem(key); + this.iframeWindow.postMessage({ type: 'remove', key: key }, '*'); } async clear(): Promise { - window.hubStorage.clear(); + this.iframeWindow.postMessage({ type: 'clear' }, '*'); + } + + async destroy(): Promise { + document.body.removeChild(this.documentElement); } } @@ -91,19 +140,71 @@ class HubStorageBackend implements StorageBackend { */ class StorageProxy implements StorageBackend { private backendPromise: Promise; - public impl: StorageImplementation = IMPL_MEMORY; + public impl: StorageImplementation = IMPL_IFRAME_INDEXED_DB; constructor() { this.backendPromise = (async () => { - if (testHubStorage()) { - return new HubStorageBackend(); + // If we have not enabled byondstorage yet, we need to check + // if we can use the IFrame, or if we need to enable byondstorage + if (!testHubStorage()) { + // If we have an IFrame URL we can use, and we haven't already enabled + // byondstorage, we should use the IFrame backend + if (Byond.storageCdn) { + const iframe = new IFrameIndexedDbBackend(); + + if ((await iframe.ready()) === true) { + if (await iframe.get('byondstorage-migrated')) return iframe; + + Byond.winset(null, 'browser-options', '+byondstorage'); + + await new Promise((resolve) => { + document.addEventListener('byondstorageupdated', async () => { + setTimeout(() => { + const hub = new HubStorageBackend(); + + // Migrate these existing settings from byondstorage to the IFrame + for (const setting of [ + 'panel-settings', + 'chat-state', + 'chat-messages', + ]) { + hub + .get(setting) + .then((settings) => iframe.set(setting, settings)); + } + + iframe.set('byondstorage-migrated', true); + Byond.winset(null, 'browser-options', '-byondstorage'); + + resolve(); + }, 1); + }); + }); + + return iframe; + } + + iframe.destroy(); + } + + // IFrame hasn't worked out for us, we'll need to enable byondstorage + Byond.winset(null, 'browser-options', '+byondstorage'); + + return new Promise((resolve) => { + const listener = () => { + document.removeEventListener('byondstorageupdated', listener); + + // This event is emitted *before* byondstorage is actually created + // so we have to wait a little bit before we can use it + setTimeout(() => resolve(new HubStorageBackend()), 1); + }; + + document.addEventListener('byondstorageupdated', listener); + }); } - console.warn( - 'No supported storage backend found. Using in-memory storage.', - ); - - return new MemoryBackend(); + // byondstorage is already enabled, we can use it straight away + return new HubStorageBackend(); })(); } diff --git a/tgui/packages/tgui-panel/Panel.tsx b/tgui/packages/tgui-panel/Panel.tsx index d818f0351b8..d79fd985fdb 100644 --- a/tgui/packages/tgui-panel/Panel.tsx +++ b/tgui/packages/tgui-panel/Panel.tsx @@ -33,7 +33,7 @@ export const Panel = (props) => {
- + diff --git a/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx b/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx index 53f3d5d7048..7b9afea69da 100644 --- a/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx +++ b/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx @@ -46,7 +46,7 @@ export const NowPlayingWidget = (props) => {
{URL !== 'Song Link Hidden' && ( - URL: {URL} + URL: {URL} )} @@ -99,7 +99,8 @@ export const NowPlayingWidget = (props) => { step={0.0025} stepPixelSize={1} format={(value) => toFixed(value * 100) + '%'} - onDrag={(e, value) => + tickWhileDragging + onChange={(value) => settings.update({ adminMusicVolume: value, }) diff --git a/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx b/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx index a4402fc7bf2..463ec0c2dd5 100644 --- a/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx +++ b/tgui/packages/tgui-panel/chat/ChatPageSettings.jsx @@ -24,9 +24,10 @@ import { import { MESSAGE_TYPES } from './constants'; import { selectCurrentChatPage } from './selectors'; -export const ChatPageSettings = (props) => { +export function ChatPageSettings(props) { const page = useSelector(selectCurrentChatPage); const dispatch = useDispatch(); + return (
@@ -48,9 +49,9 @@ export const ChatPageSettings = (props) => { )} + onBlur={(value) => dispatch( updateChatPage({ pageId: page.id, @@ -112,7 +113,7 @@ export const ChatPageSettings = (props) => { )} -
+
{MESSAGE_TYPES.filter( (typeDef) => !typeDef.important && !typeDef.admin, ).map((typeDef) => ( @@ -154,4 +155,4 @@ export const ChatPageSettings = (props) => {
); -}; +} diff --git a/tgui/packages/tgui-panel/chat/ChatPanel.jsx b/tgui/packages/tgui-panel/chat/ChatPanel.tsx similarity index 84% rename from tgui/packages/tgui-panel/chat/ChatPanel.jsx rename to tgui/packages/tgui-panel/chat/ChatPanel.tsx index 92d07635834..b42086ccd56 100644 --- a/tgui/packages/tgui-panel/chat/ChatPanel.jsx +++ b/tgui/packages/tgui-panel/chat/ChatPanel.tsx @@ -10,7 +10,19 @@ import { shallowDiffers } from 'tgui-core/react'; import { chatRenderer } from './renderer'; -export class ChatPanel extends Component { +type Props = { + fontSize?: string; + lineHeight: string; +}; + +type State = { + scrollTracking: boolean; +}; + +export class ChatPanel extends Component { + ref: React.RefObject; + handleScrollTrackingChange: (value: boolean) => void; + constructor(props) { super(props); this.ref = createRef(); @@ -29,7 +41,7 @@ export class ChatPanel extends Component { 'scrollTrackingChanged', this.handleScrollTrackingChange, ); - this.componentDidUpdate(); + this.componentDidUpdate(null); } componentWillUnmount() { diff --git a/tgui/packages/tgui-panel/chat/ChatTabs.jsx b/tgui/packages/tgui-panel/chat/ChatTabs.tsx similarity index 75% rename from tgui/packages/tgui-panel/chat/ChatTabs.jsx rename to tgui/packages/tgui-panel/chat/ChatTabs.tsx index 9614266daea..d20c5f999ca 100644 --- a/tgui/packages/tgui-panel/chat/ChatTabs.jsx +++ b/tgui/packages/tgui-panel/chat/ChatTabs.tsx @@ -5,24 +5,25 @@ */ import { useDispatch, useSelector } from 'tgui/backend'; -import { Box, Button, Flex, Tabs } from 'tgui-core/components'; +import { Box, Button, Stack, Tabs } from 'tgui-core/components'; import { openChatSettings } from '../settings/actions'; import { addChatPage, changeChatPage } from './actions'; import { selectChatPages, selectCurrentChatPage } from './selectors'; -const UnreadCountWidget = ({ value }) => ( - {Math.min(value, 99)} -); +function UnreadCountWidget({ value }: { value: number }) { + return {Math.min(value, 99)}; +} -export const ChatTabs = (props) => { +export function ChatTabs(props) { const pages = useSelector(selectChatPages); const currentPage = useSelector(selectCurrentChatPage); const dispatch = useDispatch(); + return ( - - - + + + {pages.map((page) => ( { ))} - - + +
diff --git a/tgui/packages/tgui/stories/Input.stories.tsx b/tgui/packages/tgui/stories/Input.stories.tsx index a232a62bee3..8ed8e51023d 100644 --- a/tgui/packages/tgui/stories/Input.stories.tsx +++ b/tgui/packages/tgui/stories/Input.stories.tsx @@ -29,10 +29,10 @@ function Story() {
- setText(value)} /> + setText(value)} /> - setText(value)} /> + setText(value)} /> setNumber(value)} /> - + setNumber(value)} + tickWhileDragging + onChange={(value) => setNumber(value)} /> - + setNumber(value)} + tickWhileDragging + onChange={(e, value) => setNumber(value)} /> - + setNumber(value)} + tickWhileDragging + onChange={(e, value) => setNumber(value)} /> setNumber(value)} + tickWhileDragging + onChange={(e, value) => setNumber(value)} /> @@ -101,7 +105,8 @@ function Story() { dragMatrix={[0, -1]} step={1} stepPixelSize={5} - onDrag={(e, value) => setNumber(value)} + tickWhileDragging + onChange={(e, value) => setNumber(value)} > {(control) => ( diff --git a/tgui/packages/tgui/stories/ProgressBar.stories.tsx b/tgui/packages/tgui/stories/ProgressBar.stories.tsx index 3b8d8ac2b3c..510dedb7f89 100644 --- a/tgui/packages/tgui/stories/ProgressBar.stories.tsx +++ b/tgui/packages/tgui/stories/ProgressBar.stories.tsx @@ -45,7 +45,7 @@ function Story() { - setColor(value)} /> + setColor(value)} /> diff --git a/tgui/packages/tgui/stories/Storage.stories.tsx b/tgui/packages/tgui/stories/Storage.stories.tsx index 5c63cf82be4..c5234a1e31d 100644 --- a/tgui/packages/tgui/stories/Storage.stories.tsx +++ b/tgui/packages/tgui/stories/Storage.stories.tsx @@ -6,7 +6,6 @@ import { storage } from 'common/storage'; import { Button, LabeledList, NoticeBox, Section } from 'tgui-core/components'; -import { formatSiUnit } from 'tgui-core/format'; export const meta = { title: 'Storage', @@ -37,9 +36,6 @@ const Story = (props) => { {localStorage.length} - - {formatSiUnit(localStorage.remainingSpace, 0, 'B')} -
); diff --git a/tgui/packages/tgui/stories/Themes.stories.tsx b/tgui/packages/tgui/stories/Themes.stories.tsx index 9cb1106c4a4..357d5d06e49 100644 --- a/tgui/packages/tgui/stories/Themes.stories.tsx +++ b/tgui/packages/tgui/stories/Themes.stories.tsx @@ -23,7 +23,7 @@ function Story() { setTheme(value)} + onChange={(value) => setTheme(value)} /> diff --git a/tgui/public/helpers.min.js b/tgui/public/helpers.min.js index 872eebc8ee2..6f11d9a35cc 100644 --- a/tgui/public/helpers.min.js +++ b/tgui/public/helpers.min.js @@ -1 +1 @@ -(function(){var hasOwn=Object.prototype.hasOwnProperty;var assign=function(target){for(var i=1;i0){url+="&"}var value=params[key];if(value===null||value===undefined){value=""}url+=encodeURIComponent(key)+"="+encodeURIComponent(value)}}}if(window.cef_to_byond){cef_to_byond("byond://"+url);return}if(url.length<2048){location.href="byond://"+url;return}var xhr=new XMLHttpRequest;xhr.open("GET",url);xhr.send()};Byond.callAsync=function(path,params){if(!window.Promise){throw new Error("Async calls require API level of ES2015 or later.")}var index=Byond.__callbacks__.length;var promise=new window.Promise((function(resolve){Byond.__callbacks__.push(resolve)}));Byond.call(path,assign({},params,{callback:"Byond.__callbacks__["+index+"]"}));return promise};Byond.topic=function(params){return Byond.call("",params)};Byond.command=function(command){return Byond.call("winset",{command:command})};Byond.winget=function(id,propName){if(id===null){id=""}var isArray=propName instanceof Array;var isSpecific=propName&&propName!=="*"&&!isArray;var promise=Byond.callAsync("winget",{id:id,property:isArray&&propName.join(",")||propName||"*"});if(isSpecific){promise=promise.then((function(props){return props[propName]}))}return promise};Byond.winset=function(id,propName,propValue){if(id===null){id=""}else if(typeof id==="object"){return Byond.call("winset",id)}var props={};if(typeof propName==="string"){props[propName]=propValue}else{assign(props,propName)}props.id=id;return Byond.call("winset",props)};Byond.parseJson=function(json){try{return JSON.parse(json,byondJsonReviver)}catch(err){throw new Error("JSON parsing error: "+(err&&err.message))}};Byond.sendMessage=function(type,payload){var message=typeof type==="string"?{type:type,payload:payload}:type;if(message.payload!==null&&message.payload!==undefined){message.payload=JSON.stringify(message.payload)}assign(message,{tgui:1,window_id:Byond.windowId});Byond.topic(message)};Byond.injectMessage=function(type,payload){window.update(JSON.stringify({type:type,payload:payload}))};Byond.subscribe=function(listener){window.update.flushQueue(listener);window.update.listeners.push(listener)};Byond.subscribeTo=function(type,listener){var _listener=function(_type,payload){if(_type===type){listener(payload)}};window.update.flushQueue(_listener);window.update.listeners.push(_listener)};var RETRY_ATTEMPTS=5;var RETRY_WAIT_INITIAL=500;var RETRY_WAIT_INCREMENT=500;var loadedAssetByUrl={};var isStyleSheetLoaded=function(node,url){var styleSheet=node.sheet;if(styleSheet){return styleSheet.rules.length>0}return false};var injectNode=function(node){if(!document.body){setTimeout((function(){injectNode(node)}));return}var refs=document.body.childNodes;var ref=refs[refs.length-1];ref.parentNode.insertBefore(node,ref.nextSibling)};var loadAsset=function(options){var url=options.url;var type=options.type;var sync=options.sync;var attempt=options.attempt||0;if(loadedAssetByUrl[url]){return}loadedAssetByUrl[url]=options;var retry=function(){if(attempt>=RETRY_ATTEMPTS){var errorMessage="Error: Failed to load the asset "+"'"+url+"' after several attempts.";if(type==="css"){errorMessage+=+"\nStylesheet was either not found, "+"or you're trying to load an empty stylesheet "+"that has no CSS rules in it."}throw new Error(errorMessage)}setTimeout((function(){loadedAssetByUrl[url]=null;options.attempt+=1;loadAsset(options)}),RETRY_WAIT_INITIAL+attempt*RETRY_WAIT_INCREMENT)};if(type==="js"){var node=document.createElement("script");node.type="text/javascript";node.crossOrigin="anonymous";node.src=url;if(sync){node.defer=true}else{node.async=true}node.onerror=function(){node.onerror=null;node.parentNode.removeChild(node);node=null;retry()};injectNode(node);return}if(type==="css"){var node=document.createElement("link");node.type="text/css";node.rel="stylesheet";node.crossOrigin="anonymous";node.href=url;if(!sync){node.media="only x"}var removeNodeAndRetry=function(){node.parentNode.removeChild(node);node=null;retry()};node.onerror=function(){node.onerror=null;removeNodeAndRetry()};node.onload=function(){node.onload=null;if(isStyleSheetLoaded(node,url)){node.media="all";return}removeNodeAndRetry()};injectNode(node);return}};Byond.loadJs=function(url,sync){loadAsset({url:url,sync:sync,type:"js"})};Byond.loadCss=function(url,sync){loadAsset({url:url,sync:sync,type:"css"})};Byond.saveBlob=function(blob,filename,ext){if(window.navigator.msSaveBlob){window.navigator.msSaveBlob(blob,filename)}else if(window.showSaveFilePicker){var accept={};accept[blob.type]=[ext];var opts={suggestedName:filename,types:[{description:"SS13 file",accept:accept}]};window.showSaveFilePicker(opts).then((function(file){return file.createWritable()})).then((function(file){return file.write(blob).then((function(){return file.close()}))})).catch((function(){}))}};Byond.iconRefMap={}})();window.onerror=function(msg,url,line,col,error){window.onerror.errorCount=(window.onerror.errorCount||0)+1;var stack=error&&error.stack;if(!stack){stack=msg+"\n at "+url+":"+line;if(col){stack+=":"+col}}stack=window.__augmentStack__(stack,error);if(Byond.strictMode){var errorRoot=document.getElementById("FatalError");var errorStack=document.getElementById("FatalError__stack");if(errorRoot){errorRoot.className="FatalError FatalError--visible";if(window.onerror.__stack__){window.onerror.__stack__+="\n\n"+stack}else{window.onerror.__stack__=stack}var textProp="textContent";errorStack[textProp]=window.onerror.__stack__}var setFatalErrorGeometry=function(){Byond.winset(Byond.windowId,{titlebar:true,"is-visible":true,"can-resize":true})};setFatalErrorGeometry();setInterval(setFatalErrorGeometry,1e3)}if(Byond.strictMode){Byond.sendMessage({type:"log",fatal:1,message:stack})}else if(window.onerror.errorCount<=1){stack+="\nWindow is in non-strict mode, future errors are suppressed.";Byond.sendMessage({type:"log",message:stack})}if(Byond.strictMode){window.update=function(){};window.update.queue=[]}return true};window.onunhandledrejection=function(e){var msg="UnhandledRejection";if(e.reason){msg+=": "+(e.reason.message||e.reason.description||e.reason);if(e.reason.stack){e.reason.stack="UnhandledRejection: "+e.reason.stack}}window.onerror(msg,null,null,null,e.reason)};window.__augmentStack__=function(stack,error){return stack+"\nUser Agent: "+navigator.userAgent};window.update=function(rawMessage){if(window.update.queueActive){window.update.queue.push(rawMessage);return}var message=Byond.parseJson(rawMessage);var listeners=window.update.listeners;for(var i=0;i0){url+="&"}var value=params[key];if(value===null||value===undefined){value=""}url+=encodeURIComponent(key)+"="+encodeURIComponent(value)}}}if(window.cef_to_byond){cef_to_byond("byond://"+url);return}if(url.length<2048){location.href="byond://"+url;return}var xhr=new XMLHttpRequest;xhr.open("GET",url);xhr.send()};Byond.callAsync=function(path,params){if(!window.Promise){throw new Error("Async calls require API level of ES2015 or later.")}var index=Byond.__callbacks__.length;var promise=new window.Promise(function(resolve){Byond.__callbacks__.push(resolve)});Byond.call(path,assign({},params,{callback:"Byond.__callbacks__["+index+"]"}));return promise};Byond.topic=function(params){return Byond.call("",params)};Byond.command=function(command){return Byond.call("winset",{command:command})};Byond.winget=function(id,propName){if(id===null){id=""}var isArray=propName instanceof Array;var isSpecific=propName&&propName!=="*"&&!isArray;var promise=Byond.callAsync("winget",{id:id,property:isArray&&propName.join(",")||propName||"*"});if(isSpecific){promise=promise.then(function(props){return props[propName]})}return promise};Byond.winset=function(id,propName,propValue){if(id===null){id=""}else if(typeof id==="object"){return Byond.call("winset",id)}var props={};if(typeof propName==="string"){props[propName]=propValue}else{assign(props,propName)}props.id=id;return Byond.call("winset",props)};Byond.parseJson=function(json){try{return JSON.parse(json,byondJsonReviver)}catch(err){throw new Error("JSON parsing error: "+(err&&err.message))}};Byond.sendMessage=function(type,payload){var message=typeof type==="string"?{type:type,payload:payload}:type;if(message.payload!==null&&message.payload!==undefined){message.payload=JSON.stringify(message.payload)}assign(message,{tgui:1,window_id:Byond.windowId});Byond.topic(message)};Byond.injectMessage=function(type,payload){window.update(JSON.stringify({type:type,payload:payload}))};Byond.subscribe=function(listener){window.update.flushQueue(listener);window.update.listeners.push(listener)};Byond.subscribeTo=function(type,listener){var _listener=function(_type,payload){if(_type===type){listener(payload)}};window.update.flushQueue(_listener);window.update.listeners.push(_listener)};var RETRY_ATTEMPTS=5;var RETRY_WAIT_INITIAL=500;var RETRY_WAIT_INCREMENT=500;var loadedAssetByUrl={};var isStyleSheetLoaded=function(node,url){var styleSheet=node.sheet;if(styleSheet){return styleSheet.rules.length>0}return false};var injectNode=function(node){if(!document.body){setTimeout(function(){injectNode(node)});return}var refs=document.body.childNodes;var ref=refs[refs.length-1];ref.parentNode.insertBefore(node,ref.nextSibling)};var loadAsset=function(options){var url=options.url;var type=options.type;var sync=options.sync;var attempt=options.attempt||0;if(loadedAssetByUrl[url]){return}loadedAssetByUrl[url]=options;var retry=function(){if(attempt>=RETRY_ATTEMPTS){var errorMessage="Error: Failed to load the asset "+"'"+url+"' after several attempts.";if(type==="css"){errorMessage+=+"\nStylesheet was either not found, "+"or you're trying to load an empty stylesheet "+"that has no CSS rules in it."}throw new Error(errorMessage)}setTimeout(function(){loadedAssetByUrl[url]=null;options.attempt+=1;loadAsset(options)},RETRY_WAIT_INITIAL+attempt*RETRY_WAIT_INCREMENT)};if(type==="js"){var node=document.createElement("script");node.type="text/javascript";node.crossOrigin="anonymous";node.src=url;if(sync){node.defer=true}else{node.async=true}node.onerror=function(){node.onerror=null;node.parentNode.removeChild(node);node=null;retry()};injectNode(node);return}if(type==="css"){var node=document.createElement("link");node.type="text/css";node.rel="stylesheet";node.crossOrigin="anonymous";node.href=url;if(!sync){node.media="only x"}var removeNodeAndRetry=function(){node.parentNode.removeChild(node);node=null;retry()};node.onerror=function(){node.onerror=null;removeNodeAndRetry()};node.onload=function(){node.onload=null;if(isStyleSheetLoaded(node,url)){node.media="all";return}removeNodeAndRetry()};injectNode(node);return}};Byond.loadJs=function(url,sync){loadAsset({url:url,sync:sync,type:"js"})};Byond.loadCss=function(url,sync){loadAsset({url:url,sync:sync,type:"css"})};Byond.saveBlob=function(blob,filename,ext){if(window.navigator.msSaveBlob){window.navigator.msSaveBlob(blob,filename)}else if(window.showSaveFilePicker){var accept={};accept[blob.type]=[ext];var opts={suggestedName:filename,types:[{description:"SS13 file",accept:accept}]};window.showSaveFilePicker(opts).then(function(file){return file.createWritable()}).then(function(file){return file.write(blob).then(function(){return file.close()})}).catch(function(){})}};Byond.iconRefMap={}})();window.onerror=function(msg,url,line,col,error){window.onerror.errorCount=(window.onerror.errorCount||0)+1;var stack=error&&error.stack;if(!stack){stack=msg+"\n at "+url+":"+line;if(col){stack+=":"+col}}stack=window.__augmentStack__(stack,error);if(Byond.strictMode){var errorRoot=document.getElementById("FatalError");var errorStack=document.getElementById("FatalError__stack");if(errorRoot){errorRoot.className="FatalError FatalError--visible";if(window.onerror.__stack__){window.onerror.__stack__+="\n\n"+stack}else{window.onerror.__stack__=stack}var textProp="textContent";errorStack[textProp]=window.onerror.__stack__}var setFatalErrorGeometry=function(){Byond.winset(Byond.windowId,{titlebar:true,"is-visible":true,"can-resize":true})};setFatalErrorGeometry();setInterval(setFatalErrorGeometry,1e3)}if(Byond.strictMode){Byond.sendMessage({type:"log",fatal:1,message:stack})}else if(window.onerror.errorCount<=1){stack+="\nWindow is in non-strict mode, future errors are suppressed.";Byond.sendMessage({type:"log",message:stack})}if(Byond.strictMode){window.update=function(){};window.update.queue=[]}return true};window.onunhandledrejection=function(e){var msg="UnhandledRejection";if(e.reason){msg+=": "+(e.reason.message||e.reason.description||e.reason);if(e.reason.stack){e.reason.stack="UnhandledRejection: "+e.reason.stack}}window.onerror(msg,null,null,null,e.reason)};window.__augmentStack__=function(stack,error){return stack+"\nUser Agent: "+navigator.userAgent};window.update=function(rawMessage){if(window.update.queueActive){window.update.queue.push(rawMessage);return}var message=Byond.parseJson(rawMessage);var listeners=window.update.listeners;for(var i=0;i + + + + + + + + diff --git a/tgui/public/tgui.html b/tgui/public/tgui.html index 5dbeb390d10..78ff04ee8b8 100644 --- a/tgui/public/tgui.html +++ b/tgui/public/tgui.html @@ -1,19 +1,19 @@ - + - + diff --git a/tgui/yarn.lock b/tgui/yarn.lock index 23bef86ddbd..0d4d0e63f2b 100644 --- a/tgui/yarn.lock +++ b/tgui/yarn.lock @@ -1633,10 +1633,10 @@ __metadata: languageName: node linkType: hard -"@bufbuild/protobuf@npm:^2.0.0": - version: 2.2.3 - resolution: "@bufbuild/protobuf@npm:2.2.3" - checksum: 10c0/546c38b924c4a8dd79ec457928cfb99a5aab2945d11f15f4f06894bdc148ea37a1ae8b78cf17de9bb5354cbb896f6af4099073690b5b2089e1b196cec963d6ec +"@bufbuild/protobuf@npm:^2.5.0": + version: 2.10.1 + resolution: "@bufbuild/protobuf@npm:2.10.1" + checksum: 10c0/47fde36740663e8b45c8290dc9107780a579971fae9664ece6ed0da24add458b46492cc1ff4847db218ec918a9cdf76b8e74ab0b21d79912163f3c4cb0ceda22 languageName: node linkType: hard @@ -2023,55 +2023,55 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.6.0": - version: 1.6.9 - resolution: "@floating-ui/core@npm:1.6.9" +"@floating-ui/core@npm:^1.7.3": + version: 1.7.3 + resolution: "@floating-ui/core@npm:1.7.3" dependencies: - "@floating-ui/utils": "npm:^0.2.9" - checksum: 10c0/77debdfc26bc36c6f5ae1f26ab3c15468215738b3f5682af4e1915602fa21ba33ad210273f31c9d2da1c531409929e1afb1138b1608c6b54a0f5853ee84c340d + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10c0/edfc23800122d81df0df0fb780b7328ae6c5f00efbb55bd48ea340f4af8c5b3b121ceb4bb81220966ab0f87b443204d37105abdd93d94846468be3243984144c languageName: node linkType: hard -"@floating-ui/dom@npm:^1.0.0": - version: 1.6.13 - resolution: "@floating-ui/dom@npm:1.6.13" +"@floating-ui/dom@npm:^1.7.4": + version: 1.7.4 + resolution: "@floating-ui/dom@npm:1.7.4" dependencies: - "@floating-ui/core": "npm:^1.6.0" - "@floating-ui/utils": "npm:^0.2.9" - checksum: 10c0/272242d2eb6238ffcee0cb1f3c66e0eafae804d5d7b449db5ecf904bc37d31ad96cf575a9e650b93c1190f64f49a684b1559d10e05ed3ec210628b19116991a9 + "@floating-ui/core": "npm:^1.7.3" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10c0/da6166c25f9b0729caa9f498685a73a0e28251613b35d27db8de8014bc9d045158a23c092b405321a3d67c2064909b6e2a7e6c1c9cc0f62967dca5779f5aef30 languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.1.2": - version: 2.1.2 - resolution: "@floating-ui/react-dom@npm:2.1.2" +"@floating-ui/react-dom@npm:^2.1.6": + version: 2.1.6 + resolution: "@floating-ui/react-dom@npm:2.1.6" dependencies: - "@floating-ui/dom": "npm:^1.0.0" + "@floating-ui/dom": "npm:^1.7.4" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 10c0/e855131c74e68cab505f7f44f92cd4e2efab1c125796db3116c54c0859323adae4bf697bf292ee83ac77b9335a41ad67852193d7aeace90aa2e1c4a640cafa60 + checksum: 10c0/6654834a8e73ecbdbc6cad2ad8f7abc698ac7c1800ded4d61113525c591c03d2e3b59d3cf9205859221465ea38c87af4f9e6e204703c5b7a7e85332d1eef2e18 languageName: node linkType: hard -"@floating-ui/react@npm:^0.27.6": - version: 0.27.7 - resolution: "@floating-ui/react@npm:0.27.7" +"@floating-ui/react@npm:^0.27.16, @floating-ui/react@npm:^0.27.6": + version: 0.27.16 + resolution: "@floating-ui/react@npm:0.27.16" dependencies: - "@floating-ui/react-dom": "npm:^2.1.2" - "@floating-ui/utils": "npm:^0.2.9" + "@floating-ui/react-dom": "npm:^2.1.6" + "@floating-ui/utils": "npm:^0.2.10" tabbable: "npm:^6.0.0" peerDependencies: react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10c0/bb79c89a63dfb44917f1583548905437adb7ba3fc9ece27c45b12532a60423425f8ae9fde8a54b10711e6449585ffde0951aa70a4f13f99eaeacd5006d7d69e5 + checksum: 10c0/a026266d8875e69de1ac1e1a00588660c8ee299c1e7d067c5c5fd1d69a46fd10acff5dd6cb66c3fe40a3347b443234309ba95f5b33d49059d0cda121f558f566 languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.9": - version: 0.2.9 - resolution: "@floating-ui/utils@npm:0.2.9" - checksum: 10c0/48bbed10f91cb7863a796cc0d0e917c78d11aeb89f98d03fc38d79e7eb792224a79f538ed8a2d5d5584511d4ca6354ef35f1712659fd569868e342df4398ad6f +"@floating-ui/utils@npm:^0.2.10": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: 10c0/e9bc2a1730ede1ee25843937e911ab6e846a733a4488623cd353f94721b05ec2c9ec6437613a2ac9379a94c2fd40c797a2ba6fa1df2716f5ce4aa6ddb1cf9ea4 languageName: node linkType: hard @@ -2806,6 +2806,13 @@ __metadata: languageName: node linkType: hard +"@nozbe/microfuzz@npm:^1.0.0": + version: 1.0.0 + resolution: "@nozbe/microfuzz@npm:1.0.0" + checksum: 10c0/16ce1b36b521f3990b83b08d2a6d1f6eb43fe240d0ebfb600e8f469187a1303c6aa576925b6c0bebee8a8df2f8e8e768e12b1c67d4ac50133468b7a02c46efa9 + languageName: node + linkType: hard + "@npmcli/agent@npm:^2.0.0": version: 2.2.2 resolution: "@npmcli/agent@npm:2.2.2" @@ -17833,185 +17840,173 @@ __metadata: languageName: node linkType: hard -"sass-embedded-android-arm64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-android-arm64@npm:1.85.1" - conditions: os=android & cpu=arm64 +"sass-embedded-all-unknown@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-all-unknown@npm:1.93.3" + dependencies: + sass: "npm:1.93.3" + conditions: (!cpu=arm | !cpu=arm64 | !cpu=riscv64 | !cpu=x64) languageName: node linkType: hard -"sass-embedded-android-arm@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-android-arm@npm:1.85.1" - conditions: os=android & cpu=arm +"sass-embedded-android-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-arm64@npm:1.93.3" + conditions: os=android & cpu=arm64 languageName: node linkType: hard -"sass-embedded-android-ia32@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-android-ia32@npm:1.85.1" - conditions: os=android & cpu=ia32 +"sass-embedded-android-arm@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-arm@npm:1.93.3" + conditions: os=android & cpu=arm languageName: node linkType: hard -"sass-embedded-android-riscv64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-android-riscv64@npm:1.85.1" +"sass-embedded-android-riscv64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-riscv64@npm:1.93.3" conditions: os=android & cpu=riscv64 languageName: node linkType: hard -"sass-embedded-android-x64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-android-x64@npm:1.85.1" +"sass-embedded-android-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-android-x64@npm:1.93.3" conditions: os=android & cpu=x64 languageName: node linkType: hard -"sass-embedded-darwin-arm64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-darwin-arm64@npm:1.85.1" +"sass-embedded-darwin-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-darwin-arm64@npm:1.93.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"sass-embedded-darwin-x64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-darwin-x64@npm:1.85.1" +"sass-embedded-darwin-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-darwin-x64@npm:1.93.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"sass-embedded-linux-arm64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-arm64@npm:1.85.1" +"sass-embedded-linux-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-arm64@npm:1.93.3" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"sass-embedded-linux-arm@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-arm@npm:1.85.1" +"sass-embedded-linux-arm@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-arm@npm:1.93.3" conditions: os=linux & cpu=arm languageName: node linkType: hard -"sass-embedded-linux-ia32@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-ia32@npm:1.85.1" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"sass-embedded-linux-musl-arm64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-musl-arm64@npm:1.85.1" +"sass-embedded-linux-musl-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-arm64@npm:1.93.3" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"sass-embedded-linux-musl-arm@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-musl-arm@npm:1.85.1" +"sass-embedded-linux-musl-arm@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-arm@npm:1.93.3" conditions: os=linux & cpu=arm languageName: node linkType: hard -"sass-embedded-linux-musl-ia32@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-musl-ia32@npm:1.85.1" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"sass-embedded-linux-musl-riscv64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-musl-riscv64@npm:1.85.1" +"sass-embedded-linux-musl-riscv64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-riscv64@npm:1.93.3" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"sass-embedded-linux-musl-x64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-musl-x64@npm:1.85.1" +"sass-embedded-linux-musl-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-musl-x64@npm:1.93.3" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"sass-embedded-linux-riscv64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-riscv64@npm:1.85.1" +"sass-embedded-linux-riscv64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-riscv64@npm:1.93.3" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"sass-embedded-linux-x64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-linux-x64@npm:1.85.1" +"sass-embedded-linux-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-linux-x64@npm:1.93.3" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"sass-embedded-win32-arm64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-win32-arm64@npm:1.85.1" - conditions: os=win32 & cpu=arm64 +"sass-embedded-unknown-all@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-unknown-all@npm:1.93.3" + dependencies: + sass: "npm:1.93.3" + conditions: (!os=android | !os=darwin | !os=linux | !os=win32) languageName: node linkType: hard -"sass-embedded-win32-ia32@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-win32-ia32@npm:1.85.1" - conditions: os=win32 & cpu=ia32 +"sass-embedded-win32-arm64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-win32-arm64@npm:1.93.3" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"sass-embedded-win32-x64@npm:1.85.1": - version: 1.85.1 - resolution: "sass-embedded-win32-x64@npm:1.85.1" +"sass-embedded-win32-x64@npm:1.93.3": + version: 1.93.3 + resolution: "sass-embedded-win32-x64@npm:1.93.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"sass-embedded@npm:^1.85.1": - version: 1.85.1 - resolution: "sass-embedded@npm:1.85.1" +"sass-embedded@npm:^1.89.2": + version: 1.93.3 + resolution: "sass-embedded@npm:1.93.3" dependencies: - "@bufbuild/protobuf": "npm:^2.0.0" + "@bufbuild/protobuf": "npm:^2.5.0" buffer-builder: "npm:^0.2.0" colorjs.io: "npm:^0.5.0" immutable: "npm:^5.0.2" rxjs: "npm:^7.4.0" - sass-embedded-android-arm: "npm:1.85.1" - sass-embedded-android-arm64: "npm:1.85.1" - sass-embedded-android-ia32: "npm:1.85.1" - sass-embedded-android-riscv64: "npm:1.85.1" - sass-embedded-android-x64: "npm:1.85.1" - sass-embedded-darwin-arm64: "npm:1.85.1" - sass-embedded-darwin-x64: "npm:1.85.1" - sass-embedded-linux-arm: "npm:1.85.1" - sass-embedded-linux-arm64: "npm:1.85.1" - sass-embedded-linux-ia32: "npm:1.85.1" - sass-embedded-linux-musl-arm: "npm:1.85.1" - sass-embedded-linux-musl-arm64: "npm:1.85.1" - sass-embedded-linux-musl-ia32: "npm:1.85.1" - sass-embedded-linux-musl-riscv64: "npm:1.85.1" - sass-embedded-linux-musl-x64: "npm:1.85.1" - sass-embedded-linux-riscv64: "npm:1.85.1" - sass-embedded-linux-x64: "npm:1.85.1" - sass-embedded-win32-arm64: "npm:1.85.1" - sass-embedded-win32-ia32: "npm:1.85.1" - sass-embedded-win32-x64: "npm:1.85.1" + sass-embedded-all-unknown: "npm:1.93.3" + sass-embedded-android-arm: "npm:1.93.3" + sass-embedded-android-arm64: "npm:1.93.3" + sass-embedded-android-riscv64: "npm:1.93.3" + sass-embedded-android-x64: "npm:1.93.3" + sass-embedded-darwin-arm64: "npm:1.93.3" + sass-embedded-darwin-x64: "npm:1.93.3" + sass-embedded-linux-arm: "npm:1.93.3" + sass-embedded-linux-arm64: "npm:1.93.3" + sass-embedded-linux-musl-arm: "npm:1.93.3" + sass-embedded-linux-musl-arm64: "npm:1.93.3" + sass-embedded-linux-musl-riscv64: "npm:1.93.3" + sass-embedded-linux-musl-x64: "npm:1.93.3" + sass-embedded-linux-riscv64: "npm:1.93.3" + sass-embedded-linux-x64: "npm:1.93.3" + sass-embedded-unknown-all: "npm:1.93.3" + sass-embedded-win32-arm64: "npm:1.93.3" + sass-embedded-win32-x64: "npm:1.93.3" supports-color: "npm:^8.1.1" sync-child-process: "npm:^1.0.2" varint: "npm:^6.0.0" dependenciesMeta: + sass-embedded-all-unknown: + optional: true sass-embedded-android-arm: optional: true sass-embedded-android-arm64: optional: true - sass-embedded-android-ia32: - optional: true sass-embedded-android-riscv64: optional: true sass-embedded-android-x64: @@ -18024,14 +18019,10 @@ __metadata: optional: true sass-embedded-linux-arm64: optional: true - sass-embedded-linux-ia32: - optional: true sass-embedded-linux-musl-arm: optional: true sass-embedded-linux-musl-arm64: optional: true - sass-embedded-linux-musl-ia32: - optional: true sass-embedded-linux-musl-riscv64: optional: true sass-embedded-linux-musl-x64: @@ -18040,15 +18031,15 @@ __metadata: optional: true sass-embedded-linux-x64: optional: true - sass-embedded-win32-arm64: + sass-embedded-unknown-all: optional: true - sass-embedded-win32-ia32: + sass-embedded-win32-arm64: optional: true sass-embedded-win32-x64: optional: true bin: sass: dist/bin/sass.js - checksum: 10c0/be087fd67cc2563d17fad6254ec512fc37849490bc131e227e00baacc9f40d19dec77efc0d03ae6acfc4b19b34c1625bcaeabc1af7f9d3b92eee03b37891dc8d + checksum: 10c0/fdaecf90af7f131494f2800f0db6212133200ff18939e504aeca93c7ce6230a5366c514fce14585d766623e9feace7d521e499b66f2116a98b55821c81ee9250 languageName: node linkType: hard @@ -18077,9 +18068,9 @@ __metadata: languageName: node linkType: hard -"sass-loader@npm:^16.0.3": - version: 16.0.3 - resolution: "sass-loader@npm:16.0.3" +"sass-loader@npm:^16.0.5": + version: 16.0.6 + resolution: "sass-loader@npm:16.0.6" dependencies: neo-async: "npm:^2.6.2" peerDependencies: @@ -18099,7 +18090,24 @@ __metadata: optional: true webpack: optional: true - checksum: 10c0/2dc188dd0d5276ed0251eee7f245848ccf9df6ec121227462403f322c17a3dbe100fb60d47968f078e585e4aced452eb7fa1a8e55b415d5de3151fa1bbf2d561 + checksum: 10c0/a66df6ecc01c80011a2bc9356d2b262753ad425382171d120ec5d4b5015d5131e919384a22cd148d48ecc1cb4fa598acaaa6308b260f8951f3558b5785816bb4 + languageName: node + linkType: hard + +"sass@npm:1.93.3": + version: 1.93.3 + resolution: "sass@npm:1.93.3" + dependencies: + "@parcel/watcher": "npm:^2.4.1" + chokidar: "npm:^4.0.0" + immutable: "npm:^5.0.2" + source-map-js: "npm:>=0.6.2 <2.0.0" + dependenciesMeta: + "@parcel/watcher": + optional: true + bin: + sass: sass.js + checksum: 10c0/b9facc64de10c9d1514272c1dcbb48ca99d5f591a1cd43fd27d641275d9d95948f1299107ab5b309e2abb552667cca84a38a1a3df5116eb72eba4144bf850b6a languageName: node linkType: hard @@ -19722,6 +19730,19 @@ __metadata: languageName: node linkType: hard +"tgui-core@npm:^5.3.2": + version: 5.3.2 + resolution: "tgui-core@npm:5.3.2" + dependencies: + "@floating-ui/react": "npm:^0.27.16" + "@nozbe/microfuzz": "npm:^1.0.0" + peerDependencies: + react: ^19.1.0 + react-dom: ^19.1.0 + checksum: 10c0/3fb36f837ee97732152fbba73b822fb846ebff39645fd2a376aad9a5a284bc55a17cfdcef99d582343e99e66097ca90dd95698f72b5f3d405b87179dc9846fa8 + languageName: node + linkType: hard + "tgui-dev-server@workspace:*, tgui-dev-server@workspace:packages/tgui-dev-server": version: 0.0.0-use.local resolution: "tgui-dev-server@workspace:packages/tgui-dev-server" @@ -19746,7 +19767,7 @@ __metadata: react: "npm:^19.1.0" react-dom: "npm:^19.1.0" tgui: "workspace:*" - tgui-core: "npm:^2.1.1" + tgui-core: "npm:^5.3.2" tgui-dev-server: "workspace:*" languageName: unknown linkType: soft @@ -19761,7 +19782,7 @@ __metadata: react: "npm:^19.1.0" react-dom: "npm:^19.1.0" tgui: "workspace:*" - tgui-core: "npm:^2.1.1" + tgui-core: "npm:^5.3.2" vitest: "npm:^3.1.1" languageName: unknown linkType: soft @@ -19796,8 +19817,8 @@ __metadata: jsdom: "npm:^26.0.0" prettier: "npm:^3.2.5" sass: "npm:^1.80.6" - sass-embedded: "npm:^1.85.1" - sass-loader: "npm:^16.0.3" + sass-embedded: "npm:^1.89.2" + sass-loader: "npm:^16.0.5" typescript: "npm:^5.6.3" url-loader: "npm:^4.1.1" vitest: "npm:^3.1.1" @@ -19821,7 +19842,7 @@ __metadata: marked: "npm:^4.3.0" react: "npm:^19.1.0" react-dom: "npm:^19.1.0" - tgui-core: "npm:^2.1.1" + tgui-core: "npm:^5.3.2" tgui-dev-server: "workspace:*" vitest: "npm:^3.1.1" languageName: unknown