diff --git a/code/__DEFINES/statpanel.dm b/code/__DEFINES/statpanel.dm
deleted file mode 100644
index 7988b9b5c83..00000000000
--- a/code/__DEFINES/statpanel.dm
+++ /dev/null
@@ -1,8 +0,0 @@
-/// Bare minimum required verbs for stat panel operation
-GLOBAL_LIST_INIT(stat_panel_verbs, list(
- /client/verb/set_tab,
- /client/verb/send_tabs,
- /client/verb/remove_tabs,
- /client/verb/reset_tabs,
- /client/verb/panel_ready
-))
diff --git a/code/__HELPERS/verbs.dm b/code/__HELPERS/verbs.dm
index 5a3df642c7d..d042929f118 100644
--- a/code/__HELPERS/verbs.dm
+++ b/code/__HELPERS/verbs.dm
@@ -43,9 +43,8 @@
for(var/thing in verbs_list)
var/procpath/verb_to_add = thing
output_list[++output_list.len] = list(verb_to_add.category, verb_to_add.name)
- output_list = url_encode(json_encode(output_list))
- target << output("[output_list];", "statbrowser:add_verb_list")
+ target.stat_panel.send_message("add_verb_list", output_list)
/**
* handles removing verb and sending it to browser to update, use this for removing verbs
@@ -91,6 +90,5 @@
for(var/thing in verbs_list)
var/procpath/verb_to_remove = thing
output_list[++output_list.len] = list(verb_to_remove.category, verb_to_remove.name)
- output_list = url_encode(json_encode(output_list))
- target << output("[output_list];", "statbrowser:remove_verb_list")
+ target.stat_panel.send_message("remove_verb_list", output_list)
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index 325df268dd3..b6f668549ad 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -412,7 +412,7 @@
var/turf/T = get_turf(src)
if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T))
user.listed_turf = T
- user.client << output("[url_encode(json_encode(T.name))];", "statbrowser:create_listedturf")
+ user.client.stat_panel.send_message("create_listedturf", T.name)
///The base proc of when something is right clicked on when alt is held - generally use alt_click_secondary instead
/atom/proc/alt_click_on_secondary(atom/A)
@@ -436,7 +436,7 @@
var/turf/T = get_turf(A)
if(T && user.TurfAdjacent(T))
user.listed_turf = T
- user.client << output("[url_encode(json_encode(T.name))];", "statbrowser:create_listedturf")
+ user.client.stat_panel.send_message("create_listedturf", T.name)
/mob/proc/TurfAdjacent(turf/T)
return T.Adjacent(src)
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index 1e33af2cd25..eb4b3b2245e 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -6,14 +6,14 @@ SUBSYSTEM_DEF(statpanels)
priority = FIRE_PRIORITY_STATPANEL
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
var/list/currentrun = list()
- var/encoded_global_data
- var/mc_data_encoded
+ var/list/global_data
+ var/list/mc_data
var/list/cached_images = list()
///how many subsystem fires between most tab updates
var/default_wait = 10
///how many subsystem fires between updates of the status tab
- var/status_wait = 12
+ var/status_wait = 6
///how many subsystem fires between updates of the MC tab
var/mc_wait = 5
///how many full runs this subsystem has completed. used for variable rate refreshes.
@@ -24,7 +24,7 @@ SUBSYSTEM_DEF(statpanels)
num_fires++
var/datum/map_config/cached = SSmapping.next_map_config
/* SKYRAT EDIT CHANGE
- var/list/global_data = list(
+ global_data = list(
"Map: [SSmapping.config?.map_name || "Loading..."]",
cached ? "Next Map: [cached.map_name]" : null,
"Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]",
@@ -56,28 +56,27 @@ SUBSYSTEM_DEF(statpanels)
var/ETA = SSshuttle.emergency.getModeStr()
if(ETA)
global_data += "[ETA] [SSshuttle.emergency.getTimerStr()]"
- encoded_global_data = url_encode(json_encode(global_data))
src.currentrun = GLOB.clients.Copy()
- mc_data_encoded = null
+ mc_data = null
var/list/currentrun = src.currentrun
while(length(currentrun))
var/client/target = currentrun[length(currentrun)]
currentrun.len--
- if(!target.statbrowser_ready)
+ if(!target.stat_panel.is_ready())
continue
if(target.stat_tab == "Status" && num_fires % status_wait == 0)
set_status_tab(target)
if(!target.holder)
- target << output("", "statbrowser:remove_admin_tabs")
+ target.stat_panel.send_message("remove_admin_tabs")
else
- target << output("[!!(target.prefs.toggles & SPLIT_ADMIN_TABS)]", "statbrowser:update_split_admin_tabs")
+ target.stat_panel.send_message("update_split_admin_tabs", !!(target.prefs.toggles & SPLIT_ADMIN_TABS))
if(!("MC" in target.panel_tabs) || !("Tickets" in target.panel_tabs))
- target << output("[url_encode(target.holder.href_token)]", "statbrowser:add_admin_tabs")
+ target.stat_panel.send_message("add_admin_tabs", target.holder.href_token)
if(target.stat_tab == "MC" && ((num_fires % mc_wait == 0) || target?.prefs.read_preference(/datum/preference/toggle/fast_mc_refresh)))
set_MC_tab(target)
@@ -86,7 +85,7 @@ SUBSYSTEM_DEF(statpanels)
set_tickets_tab(target)
if(!length(GLOB.sdql2_queries) && ("SDQL2" in target.panel_tabs))
- target << output("", "statbrowser:remove_sdql2")
+ target.stat_panel.send_message("remove_sdql2")
else if(length(GLOB.sdql2_queries) && (target.stat_tab == "SDQL2" || !("SDQL2" in target.panel_tabs)) && num_fires % default_wait == 0)
set_SDQL2_tab(target)
@@ -97,9 +96,10 @@ SUBSYSTEM_DEF(statpanels)
if(num_fires % default_wait == 0)
set_spells_tab(target, target_mob)
+
if(target_mob?.listed_turf && num_fires % default_wait == 0)
- if(!target_mob.TurfAdjacent(target_mob.listed_turf))
- target << output("", "statbrowser:remove_listedturf")
+ if(!target_mob.TurfAdjacent(target_mob.listed_turf) || isnull(target_mob.listed_turf))
+ target.stat_panel.send_message("remove_listedturf")
target_mob.listed_turf = null
else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs))
@@ -109,23 +109,25 @@ SUBSYSTEM_DEF(statpanels)
return
/datum/controller/subsystem/statpanels/proc/set_status_tab(client/target)
- if(!encoded_global_data)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data()
+ if(!global_data)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data()
return
- var/ping_str = url_encode("Ping: [round(target.lastping, 1)]ms (Average: [round(target.avgping, 1)]ms)")
- var/other_str = url_encode(json_encode(target.mob?.get_status_tab_items()))
- target << output("[encoded_global_data];[ping_str];[other_str]", "statbrowser:update")
+ target.stat_panel.send_message("update_stat", list(
+ global_data = global_data,
+ ping_str = "Ping: [round(target.lastping, 1)]ms (Average: [round(target.avgping, 1)]ms)",
+ other_str = target.mob?.get_status_tab_items(),
+ ))
/datum/controller/subsystem/statpanels/proc/set_MC_tab(client/target)
var/turf/eye_turf = get_turf(target.eye)
- var/coord_entry = url_encode(COORD(eye_turf))
- if(!mc_data_encoded)
+ var/coord_entry = COORD(eye_turf)
+ if(!mc_data)
generate_mc_data()
- target << output("[mc_data_encoded];[coord_entry]", "statbrowser:update_mc")
+ target.stat_panel.send_message("update_mc", list(mc_data = mc_data, coord_entry = coord_entry))
/datum/controller/subsystem/statpanels/proc/set_tickets_tab(client/target)
var/list/ahelp_tickets = GLOB.ahelp_tickets.stat_entry()
- target << output("[url_encode(json_encode(ahelp_tickets))];", "statbrowser:update_tickets")
+ target.stat_panel.send_message("update_tickets", ahelp_tickets)
var/datum/interview_manager/m = GLOB.interviews
// get open interview count
@@ -153,7 +155,7 @@ SUBSYSTEM_DEF(statpanels)
)
// Push update
- target << output("[url_encode(json_encode(data))];", "statbrowser:update_interviews")
+ target.stat_panel.send_message("update_interviews", data)
/datum/controller/subsystem/statpanels/proc/set_SDQL2_tab(client/target)
var/list/sdql2A = list()
@@ -163,7 +165,7 @@ SUBSYSTEM_DEF(statpanels)
sdql2B = query.generate_stat()
sdql2A += sdql2B
- target << output(url_encode(json_encode(sdql2A)), "statbrowser:update_sdql2")
+ target.stat_panel.send_message("update_sdql2", sdql2A)
/datum/controller/subsystem/statpanels/proc/set_spells_tab(client/target, mob/target_mob)
var/list/proc_holders = target_mob.get_proc_holders()
@@ -172,11 +174,7 @@ SUBSYSTEM_DEF(statpanels)
for(var/proc_holder_list as anything in proc_holders)
target.spell_tabs |= proc_holder_list[1]
- var/proc_holders_encoded = ""
- if(length(proc_holders))
- proc_holders_encoded = url_encode(json_encode(proc_holders))
-
- target << output("[url_encode(json_encode(target.spell_tabs))];[proc_holders_encoded]", "statbrowser:update_spells")
+ target.stat_panel.send_message("update_spells", list(spell_tabs = target.spell_tabs, proc_holders_encoded = proc_holders))
/datum/controller/subsystem/statpanels/proc/set_turf_examine_tab(client/target, mob/target_mob)
var/list/overrides = list()
@@ -213,11 +211,11 @@ SUBSYSTEM_DEF(statpanels)
else
turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content))
- turfitems = url_encode(json_encode(turfitems))
- target << output("[turfitems];", "statbrowser:update_listedturf")
+ turfitems = turfitems
+ target.stat_panel.send_message("update_listedturf", turfitems)
/datum/controller/subsystem/statpanels/proc/generate_mc_data()
- var/list/mc_data = list(
+ mc_data = list(
list("CPU:", world.cpu),
list("Instances:", "[num2text(world.contents.len, 10)]"),
list("World Time:", "[world.time]"),
@@ -231,11 +229,10 @@ SUBSYSTEM_DEF(statpanels)
for(var/datum/controller/subsystem/sub_system as anything in Master.subsystems)
mc_data[++mc_data.len] = list("\[[sub_system.state_letter()]][sub_system.name]", sub_system.stat_entry(), "\ref[sub_system]")
mc_data[++mc_data.len] = list("Camera Net", "Cameras: [GLOB.cameranet.cameras.len] | Chunks: [GLOB.cameranet.chunks.len]", "\ref[GLOB.cameranet]")
- mc_data_encoded = url_encode(json_encode(mc_data))
///immediately update the active statpanel tab of the target client
/datum/controller/subsystem/statpanels/proc/immediate_send_stat_data(client/target)
- if(!target.statbrowser_ready)
+ if(!target.stat_panel.is_ready())
return FALSE
if(target.stat_tab == "Status")
@@ -249,7 +246,7 @@ SUBSYSTEM_DEF(statpanels)
if(target_mob?.listed_turf)
if(!target_mob.TurfAdjacent(target_mob.listed_turf))
- target << output("", "statbrowser:remove_listedturf")
+ target.stat_panel.send_message("removed_listedturf")
target_mob.listed_turf = null
else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs))
@@ -268,7 +265,7 @@ SUBSYSTEM_DEF(statpanels)
return TRUE
if(!length(GLOB.sdql2_queries) && ("SDQL2" in target.panel_tabs))
- target << output("", "statbrowser:remove_sdql2")
+ target.stat_panel.send_message("remove_sdql2")
else if(length(GLOB.sdql2_queries) && target.stat_tab == "SDQL2")
set_SDQL2_tab(target)
@@ -277,41 +274,5 @@ SUBSYSTEM_DEF(statpanels)
SIGNAL_HANDLER
SSstatpanels.cached_images -= REF(src)
-/// verbs that send information from the browser UI
-/client/verb/set_tab(tab as text|null)
- set name = "Set Tab"
- set hidden = TRUE
-
- stat_tab = tab
- SSstatpanels.immediate_send_stat_data(src)
-
-/client/verb/send_tabs(tabs as text|null)
- set name = "Send Tabs"
- set hidden = TRUE
-
- panel_tabs |= tabs
-
-/client/verb/remove_tabs(tabs as text|null)
- set name = "Remove Tabs"
- set hidden = TRUE
-
- panel_tabs -= tabs
-
-/client/verb/reset_tabs()
- set name = "Reset Tabs"
- set hidden = TRUE
-
- panel_tabs = list()
-
-/client/verb/panel_ready()
- set name = "Panel Ready"
- set hidden = TRUE
-
- statbrowser_ready = TRUE
- init_verbs()
-
-/client/verb/update_verbs()
- set name = "Update Verbs"
- set hidden = TRUE
-
- init_verbs()
+/// Stat panel window declaration
+/client/var/datum/tgui_window/stat_panel
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 38563d8519a..2dbcaf4a05a 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -819,7 +819,7 @@
if(istype(S, spell))
spell_list -= S
qdel(S)
- current?.client << output(null, "statbrowser:check_spells")
+ current?.client.stat_panel.send_message("check_spells")
/datum/mind/proc/RemoveAllSpells()
for(var/obj/effect/proc_holder/S in spell_list)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 913512fddbe..0edf9951aa8 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -862,7 +862,7 @@ GLOBAL_PROTECT(admin_verbs_hideable)
set name = "Debug Stat Panel"
set category = "Debug"
- src << output("", "statbrowser:create_debug")
+ src.stat_panel.send_message("create_debug")
/client/proc/admin_2fa_verify()
set name = "Verify Admin"
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index d2e0a3086d4..cf3cd8ee37b 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -181,9 +181,6 @@
/// our current tab
var/stat_tab
- /// whether our browser is ready or not yet
- var/statbrowser_ready = FALSE
-
/// list of all tabs
var/list/panel_tabs = list()
/// list of tabs containing spells and abilities
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 04953a71253..d7de1b5315d 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -87,7 +87,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if(href_list["reload_tguipanel"])
nuke_chat()
if(href_list["reload_statbrowser"])
- src << browse(file('html/statbrowser.html'), "window=statbrowser")
+ stat_panel.reinitialize()
// Log all hrefs
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
@@ -213,8 +213,12 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
GLOB.clients += src
GLOB.directory[ckey] = src
+ // Instantiate stat panel
+ stat_panel = new(src, "statbrowser")
+ stat_panel.subscribe(src, .proc/on_stat_panel_message)
+
// Instantiate tgui panel
- tgui_panel = new(src)
+ tgui_panel = new(src, "browseroutput")
set_right_click_menu_mode(TRUE)
@@ -344,9 +348,15 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if(SSinput.initialized)
set_macros()
- // Initialize tgui panel
- src << browse(file('html/statbrowser.html'), "window=statbrowser")
+ // Initialize stat panel
+ stat_panel.initialize(
+ inline_html = file2text('html/statbrowser.html'),
+ inline_js = file2text('html/statbrowser.js'),
+ inline_css = file2text('html/statbrowser.css'),
+ )
addtimer(CALLBACK(src, .proc/check_panel_loaded), 30 SECONDS)
+
+ // Initialize tgui panel
tgui_panel.initialize()
if(alert_mob_dupe_login && !holder)
@@ -1129,10 +1139,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
continue
panel_tabs |= verb_to_init.category
verblist[++verblist.len] = list(verb_to_init.category, verb_to_init.name)
- src << output("[url_encode(json_encode(panel_tabs))];[url_encode(json_encode(verblist))]", "statbrowser:init_verbs")
+ src.stat_panel.send_message("init_verbs", list(panel_tabs = panel_tabs, verblist = verblist))
/client/proc/check_panel_loaded()
- if(statbrowser_ready)
+ if(stat_panel.is_ready())
return
to_chat(src, span_userdanger("Statpanel failed to load, click here to reload the panel "))
@@ -1182,6 +1192,23 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
else
SSambience.remove_ambience_client(src)
+/**
+ * Handles incoming messages from the stat-panel TGUI.
+ */
+/client/proc/on_stat_panel_message(type, payload)
+ switch(type)
+ if("Update-Verbs")
+ init_verbs()
+ if("Remove-Tabs")
+ panel_tabs -= payload["tab"]
+ if("Send-Tabs")
+ panel_tabs |= payload["tab"]
+ if("Reset-Tabs")
+ panel_tabs = list()
+ if("Set-Tab")
+ stat_tab = payload["tab"]
+ SSstatpanels.immediate_send_stat_data(src)
+
/// Checks if this client has met the days requirement passed in, or if
/// they are exempt from it.
/// Returns the number of days left, or 0.
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index f7a82ef383b..92ba7aa51ec 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -472,14 +472,12 @@
// First we detain them by removing all the verbs they have on client
for (var/v in client.verbs)
var/procpath/verb_path = v
- if (!(verb_path in GLOB.stat_panel_verbs))
- remove_verb(client, verb_path)
+ remove_verb(client, verb_path)
// Then remove those on their mob as well
for (var/v in verbs)
var/procpath/verb_path = v
- if (!(verb_path in GLOB.stat_panel_verbs))
- remove_verb(src, verb_path)
+ remove_verb(src, verb_path)
// Then we create the interview form and show it to the client
var/datum/interview/I = GLOB.interviews.interview_for_client(client)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 2a1d15ce77f..fb9118ea448 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -924,7 +924,7 @@
LAZYREMOVE(mob_spell_list, S)
qdel(S)
if(client)
- client << output(null, "statbrowser:check_spells")
+ client.stat_panel.send_message("check_spells")
/**
* Checks to see if the mob can cast normal magic spells.
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index 52ff34d08b9..dff46365f48 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -94,6 +94,7 @@
window.acquire_lock(src)
if(!window.is_ready())
window.initialize(
+ strict_mode = TRUE,
fancy = user.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy),
assets = list(
get_asset_datum(/datum/asset/simple/tgui),
diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm
index c437ce5817c..d0fb91db0cc 100644
--- a/code/modules/tgui/tgui_window.dm
+++ b/code/modules/tgui/tgui_window.dm
@@ -18,6 +18,7 @@
var/message_queue
var/sent_assets = list()
// Vars passed to initialize proc (and saved for later)
+ var/initial_strict_mode
var/initial_fancy
var/initial_assets
var/initial_inline_html
@@ -47,11 +48,15 @@
* state. You can begin sending messages right after initializing. Messages
* will be put into the queue until the window finishes loading.
*
- * optional assets list List of assets to inline into the html.
- * optional inline_html string Custom HTML to inject.
- * optional fancy bool If TRUE, will hide the window titlebar.
+ * optional strict_mode bool - Enables strict error handling and BSOD.
+ * optional fancy bool - If TRUE and if this is NOT a panel, will hide the window titlebar.
+ * optional assets list - List of assets to load during initialization.
+ * optional inline_html string - Custom HTML to inject.
+ * optional inline_js string - Custom JS to inject.
+ * optional inline_css string - Custom CSS to inject.
*/
/datum/tgui_window/proc/initialize(
+ strict_mode = FALSE,
fancy = FALSE,
assets = list(),
inline_html = "",
@@ -79,6 +84,7 @@
// Generate page html
var/html = SStgui.basehtml
html = replacetextEx(html, "\[tgui:windowId]", id)
+ html = replacetextEx(html, "\[tgui:strictMode]", strict_mode)
// Inject assets
var/inline_assets_str = ""
for(var/datum/asset/asset in assets)
@@ -99,7 +105,7 @@
html = replacetextEx(html, "", inline_html)
// Inject inline JS
if (inline_js)
- inline_js = ""
+ inline_js = ""
html = replacetextEx(html, "", inline_js)
// Inject inline CSS
if (inline_css)
@@ -113,6 +119,20 @@
if(!is_browser)
winset(client, id, "on-close=\"uiclose [id]\"")
+/**
+ * public
+ *
+ * Reinitializes the panel with previous data used for initialization.
+ */
+/datum/tgui_window/proc/reinitialize()
+ initialize(
+ strict_mode = initial_strict_mode,
+ fancy = initial_fancy,
+ assets = initial_assets,
+ inline_html = initial_inline_html,
+ inline_js = initial_inline_js,
+ inline_css = initial_inline_css)
+
/**
* public
*
@@ -346,12 +366,7 @@
client << link(href_list["url"])
if("cacheReloaded")
// Reinitialize
- initialize(
- fancy = initial_fancy,
- assets = initial_assets,
- inline_html = initial_inline_html,
- inline_js = initial_inline_js,
- inline_css = initial_inline_css)
+ reinitialize()
// Resend the assets
for(var/asset in sent_assets)
send_asset(asset)
diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm
index 8ec88f0be1f..45b6725c893 100644
--- a/code/modules/tgui_panel/tgui_panel.dm
+++ b/code/modules/tgui_panel/tgui_panel.dm
@@ -13,9 +13,9 @@
var/broken = FALSE
var/initialized_at
-/datum/tgui_panel/New(client/client)
+/datum/tgui_panel/New(client/client, id)
src.client = client
- window = new(client, "browseroutput")
+ window = new(client, id)
window.subscribe(src, .proc/on_message)
/datum/tgui_panel/Del()
@@ -42,9 +42,11 @@
sleep(1)
initialized_at = world.time
// Perform a clean initialization
- window.initialize(assets = list(
- get_asset_datum(/datum/asset/simple/tgui_panel),
- ))
+ window.initialize(
+ strict_mode = TRUE,
+ assets = list(
+ get_asset_datum(/datum/asset/simple/tgui_panel),
+ ))
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat))
// Other setup
diff --git a/html/statbrowser.css b/html/statbrowser.css
new file mode 100644
index 00000000000..dc693f42f75
--- /dev/null
+++ b/html/statbrowser.css
@@ -0,0 +1,227 @@
+body {
+ font-family: Verdana, Geneva, Tahoma, sans-serif;
+ font-size: 12px !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+
+body.dark {
+ background-color: #131313;
+ color: #b2c4dd;
+ scrollbar-base-color: #1c1c1c;
+ scrollbar-face-color: #3b3b3b;
+ scrollbar-3dlight-color: #252525;
+ scrollbar-highlight-color: #252525;
+ scrollbar-track-color: #1c1c1c;
+ scrollbar-arrow-color: #929292;
+ scrollbar-shadow-color: #3b3b3b;
+}
+
+#menu {
+ background-color: #F0F0F0;
+ position: fixed;
+ width: 100%;
+ z-index: 100;
+}
+
+.dark #menu {
+ background-color: #202020;
+}
+
+#statcontent {
+ padding: 7px 7px 7px 7px;
+}
+
+a {
+ color: black;
+ text-decoration: none
+}
+
+.dark a {
+ color: #b2c4dd;
+}
+
+a:hover,
+.dark a:hover {
+ text-decoration: underline;
+}
+
+ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ background-color: #333;
+}
+
+li {
+ float: left;
+}
+
+li a {
+ display: block;
+ color: white;
+ text-align: center;
+ padding: 14px 16px;
+ text-decoration: none;
+}
+
+li a:hover:not(.active) {
+ background-color: #111;
+}
+
+.button-container {
+ display: inline-flex;
+ flex-wrap: wrap-reverse;
+ flex-direction: row;
+ align-items: flex-start;
+ overflow-x: hidden;
+ white-space: pre-wrap;
+ padding: 0 4px;
+}
+
+.button {
+ background-color: #dfdfdf;
+ border: 1px solid #cecece;
+ border-bottom-width: 2px;
+ color: rgba(0, 0, 0, 0.7);
+ padding: 6px 4px 4px;
+ text-align: center;
+ text-decoration: none;
+ font-size: 12px;
+ margin: 0;
+ cursor: pointer;
+ transition-duration: 100ms;
+ order: 3;
+ min-width: 40px;
+}
+
+.dark button {
+ background-color: #222222;
+ border-color: #343434;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.button:hover {
+ background-color: #ececec;
+ transition-duration: 0;
+}
+
+.dark button:hover {
+ background-color: #2e2e2e;
+}
+
+.button:active,
+.button.active {
+ background-color: #ffffff;
+ color: black;
+ border-top-color: #cecece;
+ border-left-color: #cecece;
+ border-right-color: #cecece;
+ border-bottom-color: #ffffff;
+}
+
+.dark .button:active,
+.dark .button.active {
+ background-color: #444444;
+ color: white;
+ border-top-color: #343434;
+ border-left-color: #343434;
+ border-right-color: #343434;
+ border-bottom-color: #ffffff;
+}
+
+.grid-container {
+ margin: -2px;
+ margin-right: -15px;
+}
+
+.grid-item {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ box-sizing: border-box;
+ overflow: visible;
+ padding: 3px 2px;
+ text-decoration: none;
+}
+
+@media only screen and (min-width: 300px) {
+ .grid-item {
+ width: 50%;
+ }
+}
+
+@media only screen and (min-width: 430px) {
+ .grid-item {
+ width: 33%;
+ }
+}
+
+@media only screen and (min-width: 560px) {
+ .grid-item {
+ width: 25%;
+ }
+}
+
+@media only screen and (min-width: 770px) {
+ .grid-item {
+ width: 20%;
+ }
+}
+
+.grid-item:hover {
+ z-index: 1;
+}
+
+.grid-item:hover .grid-item-text {
+ width: auto;
+ text-decoration: underline;
+}
+
+.grid-item-text {
+ display: inline-block;
+ width: 100%;
+ background-color: #ffffff;
+ margin: 0 -6px;
+ padding: 0 6px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ pointer-events: none;
+}
+
+.dark .grid-item-text {
+ background-color: #131313;
+}
+
+.link {
+ display: inline;
+ background: none;
+ border: none;
+ padding: 7px 14px;
+ color: black;
+ text-decoration: none;
+ cursor: pointer;
+ font-size: 13px;
+ margin: 2px 2px;
+}
+
+.dark .link {
+ color: #abc6ec;
+}
+
+.link:hover {
+ text-decoration: underline;
+}
+
+img {
+ -ms-interpolation-mode: nearest-neighbor;
+ image-rendering: pixelated;
+}
+
+.interview_panel_controls,
+.interview_panel_stats {
+ margin-bottom: 10px;
+}
diff --git a/html/statbrowser.html b/html/statbrowser.html
index 714c45b77e0..1aea8811d58 100644
--- a/html/statbrowser.html
+++ b/html/statbrowser.html
@@ -1,1272 +1,3 @@
-
-
-
-Stat Browser
-
-
-
-
-
-
-
-
-
-
diff --git a/html/statbrowser.js b/html/statbrowser.js
new file mode 100644
index 00000000000..d024d50b8c3
--- /dev/null
+++ b/html/statbrowser.js
@@ -0,0 +1,1003 @@
+// Polyfills and compatibility ------------------------------------------------
+var decoder = decodeURIComponent || unescape;
+if (!Array.prototype.includes) {
+ Array.prototype.includes = function (thing) {
+ for (var i = 0; i < this.length; i++) {
+ if (this[i] == thing) return true;
+ }
+ return false;
+ }
+}
+if (!String.prototype.trim) {
+ String.prototype.trim = function () {
+ return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
+ };
+}
+
+// Status panel implementation ------------------------------------------------
+var status_tab_parts = ["Loading..."];
+var current_tab = null;
+var mc_tab_parts = [["Loading...", ""]];
+var href_token = null;
+var spells = [];
+var spell_tabs = [];
+var verb_tabs = [];
+var verbs = [["", ""]]; // list with a list inside
+var tickets = [];
+var interviewManager = { status: "", interviews: [] };
+var sdql2 = [];
+var permanent_tabs = []; // tabs that won't be cleared by wipes
+var turfcontents = [];
+var turfname = "";
+var imageRetryDelay = 500;
+var imageRetryLimit = 50;
+var menu = document.getElementById('menu');
+var under_menu = document.getElementById('under_menu');
+var statcontentdiv = document.getElementById('statcontent');
+var storedimages = [];
+var split_admin_tabs = false;
+
+// Any BYOND commands that could result in the client's focus changing go through this
+// to ensure that when we relinquish our focus, we don't do it after the result of
+// a command has already taken focus for itself.
+function run_after_focus(callback) {
+ setTimeout(callback, 0);
+}
+
+function createStatusTab(name) {
+ if (name.indexOf(".") != -1) {
+ var splitName = name.split(".");
+ if (split_admin_tabs && splitName[0] === "Admin")
+ name = splitName[1];
+ else
+ name = splitName[0];
+ }
+ if (document.getElementById(name) || name.trim() == "") {
+ return;
+ }
+ if (!verb_tabs.includes(name) && !permanent_tabs.includes(name)) {
+ return;
+ }
+ var B = document.createElement("BUTTON");
+ B.onclick = function () {
+ tab_change(name);
+ this.blur();
+ };
+ B.id = name;
+ B.textContent = name;
+ B.className = "button";
+ //ORDERING ALPHABETICALLY
+ B.style.order = name.charCodeAt(0);
+ if (name == "Status" || name == "MC") {
+ B.style.order = name == "Status" ? 1 : 2;
+ }
+ //END ORDERING
+ menu.appendChild(B);
+ SendTabToByond(name);
+ under_menu.style.height = menu.clientHeight + 'px';
+}
+
+function removeStatusTab(name) {
+ if (!document.getElementById(name) || permanent_tabs.includes(name)) {
+ return;
+ }
+ for (var i = verb_tabs.length - 1; i >= 0; --i) {
+ if (verb_tabs[i] == name) {
+ verb_tabs.splice(i, 1);
+ }
+ }
+ menu.removeChild(document.getElementById(name));
+ TakeTabFromByond(name);
+ under_menu.style.height = menu.clientHeight + 'px';
+}
+
+function sortVerbs() {
+ verbs.sort(function (a, b) {
+ var selector = a[0] == b[0] ? 1 : 0;
+ if (a[selector].toUpperCase() < b[selector].toUpperCase()) {
+ return 1;
+ }
+ else if (a[selector].toUpperCase() > b[selector].toUpperCase()) {
+ return -1;
+ }
+ return 0;
+ })
+}
+
+window.onresize = function () {
+ under_menu.style.height = menu.clientHeight + 'px';
+}
+
+function addPermanentTab(name) {
+ if (!permanent_tabs.includes(name)) {
+ permanent_tabs.push(name);
+ }
+ createStatusTab(name);
+}
+
+function removePermanentTab(name) {
+ for (var i = permanent_tabs.length - 1; i >= 0; --i) {
+ if (permanent_tabs[i] == name) {
+ permanent_tabs.splice(i, 1);
+ }
+ }
+ removeStatusTab(name);
+}
+
+function checkStatusTab() {
+ for (var i = 0; i < menu.children.length; i++) {
+ if (!verb_tabs.includes(menu.children[i].id) && !permanent_tabs.includes(menu.children[i].id)) {
+ menu.removeChild(menu.children[i]);
+ }
+ }
+}
+
+function remove_verb(v) {
+ var verb_to_remove = v; // to_remove = [verb:category, verb:name]
+ for (var i = verbs.length - 1; i >= 0; i--) {
+ var part_to_remove = verbs[i];
+ if (part_to_remove[1] == verb_to_remove[1]) {
+ verbs.splice(i, 1)
+ }
+ }
+}
+
+function check_verbs() {
+ for (var v = verb_tabs.length - 1; v >= 0; v--) {
+ verbs_cat_check(verb_tabs[v]);
+ }
+}
+
+function verbs_cat_check(cat) {
+ var tabCat = cat;
+ if (cat.indexOf(".") != -1) {
+ var splitName = cat.split(".");
+ if (split_admin_tabs && splitName[0] === "Admin")
+ tabCat = splitName[1];
+ else
+ tabCat = splitName[0];
+ }
+ var verbs_in_cat = 0;
+ var verbcat = "";
+ if (!verb_tabs.includes(tabCat)) {
+ removeStatusTab(tabCat);
+ return;
+ }
+ for (var v = 0; v < verbs.length; v++) {
+ var part = verbs[v];
+ verbcat = part[0];
+ if (verbcat.indexOf(".") != -1) {
+ var splitName = verbcat.split(".");
+ if (split_admin_tabs && splitName[0] === "Admin")
+ verbcat = splitName[1];
+ else
+ verbcat = splitName[0];
+ }
+ if (verbcat != tabCat || verbcat.trim() == "") {
+ continue;
+ }
+ else {
+ verbs_in_cat = 1;
+ break; // we only need one
+ }
+ }
+ if (verbs_in_cat != 1) {
+ removeStatusTab(tabCat);
+ if (current_tab == tabCat)
+ tab_change("Status");
+ }
+}
+
+function findVerbindex(name, verblist) {
+ for (var i = 0; i < verblist.length; i++) {
+ var part = verblist[i];
+ if (part[1] == name)
+ return i;
+ }
+}
+function wipe_verbs() {
+ verbs = [["", ""]];
+ verb_tabs = [];
+ checkStatusTab(); // remove all empty verb tabs
+}
+
+function update_verbs() {
+ wipe_verbs();
+ Byond.sendMessage("Update-Verbs");
+}
+
+function SendTabsToByond() {
+ var tabstosend = [];
+ tabstosend = tabstosend.concat(permanent_tabs, verb_tabs);
+ for (var i = 0; i < tabstosend.length; i++) {
+ SendTabToByond(tabstosend[i]);
+ }
+}
+
+function SendTabToByond(tab) {
+ Byond.sendMessage("Send-Tabs", {tab: tab});
+}
+
+//Byond can't have this tab anymore since we're removing it
+function TakeTabFromByond(tab) {
+ Byond.sendMessage("Remove-Tabs", {tab: tab});
+}
+
+function spell_cat_check(cat) {
+ var spells_in_cat = 0;
+ var spellcat = "";
+ for (var s = 0; s < spells.length; s++) {
+ var spell = spells[s];
+ spellcat = spell[0];
+ if (spellcat == cat) {
+ spells_in_cat++;
+ }
+ }
+ if (spells_in_cat < 1) {
+ removeStatusTab(cat);
+ }
+}
+
+function tab_change(tab) {
+ if (tab == current_tab) return;
+ if (document.getElementById(current_tab))
+ document.getElementById(current_tab).className = "button"; // disable active on last button
+ current_tab = tab;
+ set_byond_tab(tab);
+ if (document.getElementById(tab))
+ document.getElementById(tab).className = "button active"; // make current button active
+ var spell_tabs_thingy = (spell_tabs.includes(tab));
+ var verb_tabs_thingy = (verb_tabs.includes(tab));
+ if (tab == "Status") {
+ draw_status();
+ } else if (tab == "MC") {
+ draw_mc();
+ } else if (spell_tabs_thingy) {
+ draw_spells(tab);
+ } else if (verb_tabs_thingy) {
+ draw_verbs(tab);
+ } else if (tab == "Debug Stat Panel") {
+ draw_debug();
+ } else if (tab == "Tickets") {
+ draw_tickets();
+ draw_interviews();
+ } else if (tab == "SDQL2") {
+ draw_sdql2();
+ } else if (tab == turfname) {
+ draw_listedturf();
+ } else {
+ statcontentdiv.textContext = "Loading...";
+ }
+ Byond.winset(Byond.windowId, {
+ 'is-visible': true,
+ });
+}
+
+function set_byond_tab(tab) {
+ Byond.sendMessage("Set-Tab", {tab: tab});
+}
+
+function draw_debug() {
+ statcontentdiv.textContent = "";
+ var wipeverbstabs = document.createElement("div");
+ var link = document.createElement("a");
+ link.onclick = function () { wipe_verbs() };
+ link.textContent = "Wipe All Verbs";
+ wipeverbstabs.appendChild(link);
+ document.getElementById("statcontent").appendChild(wipeverbstabs);
+ var wipeUpdateVerbsTabs = document.createElement("div");
+ var updateLink = document.createElement("a");
+ updateLink.onclick = function () { update_verbs() };
+ updateLink.textContent = "Wipe and Update All Verbs";
+ wipeUpdateVerbsTabs.appendChild(updateLink);
+ document.getElementById("statcontent").appendChild(wipeUpdateVerbsTabs);
+ var text = document.createElement("div");
+ text.textContent = "Verb Tabs:";
+ document.getElementById("statcontent").appendChild(text);
+ var table1 = document.createElement("table");
+ for (var i = 0; i < verb_tabs.length; i++) {
+ var part = verb_tabs[i];
+ // Hide subgroups except admin subgroups if they are split
+ if (verb_tabs[i].lastIndexOf(".") != -1) {
+ var splitName = verb_tabs[i].split(".");
+ if (split_admin_tabs && splitName[0] === "Admin")
+ part = splitName[1];
+ else
+ continue;
+ }
+ var tr = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.textContent = part;
+ var a = document.createElement("a");
+ a.onclick = function (part) {
+ return function () { removeStatusTab(part) };
+ }(part);
+ a.textContent = " Delete Tab " + part;
+ td1.appendChild(a);
+ tr.appendChild(td1);
+ table1.appendChild(tr);
+ }
+ document.getElementById("statcontent").appendChild(table1);
+ var header2 = document.createElement("div");
+ header2.textContent = "Verbs:";
+ document.getElementById("statcontent").appendChild(header2);
+ var table2 = document.createElement("table");
+ for (var v = 0; v < verbs.length; v++) {
+ var part2 = verbs[v];
+ var trr = document.createElement("tr");
+ var tdd1 = document.createElement("td");
+ tdd1.textContent = part2[0];
+ var tdd2 = document.createElement("td");
+ tdd2.textContent = part2[1];
+ trr.appendChild(tdd1);
+ trr.appendChild(tdd2);
+ table2.appendChild(trr);
+ }
+ document.getElementById("statcontent").appendChild(table2);
+ var text3 = document.createElement("div");
+ text3.textContent = "Permanent Tabs:";
+ document.getElementById("statcontent").appendChild(text3);
+ var table3 = document.createElement("table");
+ for (var i = 0; i < permanent_tabs.length; i++) {
+ var part3 = permanent_tabs[i];
+ var trrr = document.createElement("tr");
+ var tddd1 = document.createElement("td");
+ tddd1.textContent = part3;
+ trrr.appendChild(tddd1);
+ table3.appendChild(trrr);
+ }
+ document.getElementById("statcontent").appendChild(table3);
+
+}
+function draw_status() {
+ if (!document.getElementById("Status")) {
+ createStatusTab("Status");
+ current_tab = "Status";
+ }
+ statcontentdiv.textContent = '';
+ for (var i = 0; i < status_tab_parts.length; i++) {
+ if (status_tab_parts[i].trim() == "") {
+ document.getElementById("statcontent").appendChild(document.createElement("br"));
+ } else {
+ var div = document.createElement("div");
+ div.textContent = status_tab_parts[i];
+ document.getElementById("statcontent").appendChild(div);
+ }
+ }
+ if (verb_tabs.length == 0 || !verbs) {
+ Byond.command("Fix-Stat-Panel");
+ }
+}
+
+function draw_mc() {
+ statcontentdiv.textContent = "";
+ var table = document.createElement("table");
+ for (var i = 0; i < mc_tab_parts.length; i++) {
+ var part = mc_tab_parts[i];
+ var tr = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.textContent = part[0];
+ var td2 = document.createElement("td");
+ if (part[2]) {
+ var a = document.createElement("a");
+ a.href = "?_src_=vars;admin_token=" + href_token + ";Vars=" + part[2];
+ a.textContent = part[1];
+ td2.appendChild(a);
+ } else {
+ td2.textContent = part[1];
+ }
+ tr.appendChild(td1);
+ tr.appendChild(td2);
+ table.appendChild(tr);
+ }
+ document.getElementById("statcontent").appendChild(table);
+}
+
+function remove_tickets() {
+ if (tickets) {
+ tickets = [];
+ removePermanentTab("Tickets");
+ if (current_tab == "Tickets")
+ tab_change("Status");
+ }
+ checkStatusTab();
+}
+
+function remove_sdql2() {
+ if (sdql2) {
+ sdql2 = [];
+ removePermanentTab("SDQL2");
+ if (current_tab == "SDQL2")
+ tab_change("Status");
+ }
+ checkStatusTab();
+}
+
+function remove_interviews() {
+ if (tickets) {
+ tickets = [];
+ }
+ checkStatusTab();
+}
+
+function iconError(e) {
+ if(current_tab != turfname) {
+ return;
+ }
+ setTimeout(function () {
+ var node = e.target;
+ var current_attempts = Number(node.getAttribute("data-attempts")) || 0
+ if (current_attempts > imageRetryLimit) {
+ return;
+ }
+ var src = node.src;
+ node.src = null;
+ node.src = src + '#' + current_attempts;
+ node.setAttribute("data-attempts", current_attempts + 1)
+ draw_listedturf();
+ }, imageRetryDelay);
+}
+
+function draw_listedturf() {
+ statcontentdiv.textContent = "";
+ var table = document.createElement("table");
+ for (var i = 0; i < turfcontents.length; i++) {
+ var part = turfcontents[i];
+ if (storedimages[part[1]] == null && part[2]) {
+ var img = document.createElement("img");
+ img.src = part[2];
+ img.id = part[1];
+ storedimages[part[1]] = part[2];
+ img.onerror = iconError;
+ table.appendChild(img);
+ } else {
+ var img = document.createElement("img");
+ img.onerror = iconError;
+ img.src = storedimages[part[1]];
+ img.id = part[1];
+ table.appendChild(img);
+ }
+ var b = document.createElement("div");
+ var clickcatcher = "";
+ b.className = "link";
+ b.onmousedown = function (part) {
+ // The outer function is used to close over a fresh "part" variable,
+ // rather than every onmousedown getting the "part" of the last entry.
+ return function (e) {
+ e.preventDefault();
+ clickcatcher = "?src=" + part[1];
+ switch (e.button) {
+ case 1:
+ clickcatcher += ";statpanel_item_click=middle"
+ break;
+ case 2:
+ clickcatcher += ";statpanel_item_click=right"
+ break;
+ default:
+ clickcatcher += ";statpanel_item_click=left"
+ }
+ if (e.shiftKey) {
+ clickcatcher += ";statpanel_item_shiftclick=1";
+ }
+ if (e.ctrlKey) {
+ clickcatcher += ";statpanel_item_ctrlclick=1";
+ }
+ if (e.altKey) {
+ clickcatcher += ";statpanel_item_altclick=1";
+ }
+ window.location.href = clickcatcher;
+ }
+ }(part);
+ b.textContent = part[0];
+ table.appendChild(b);
+ table.appendChild(document.createElement("br"));
+ }
+ document.getElementById("statcontent").appendChild(table);
+}
+
+function remove_listedturf() {
+ removePermanentTab(turfname);
+ checkStatusTab();
+ if (current_tab == turfname) {
+ tab_change("Status");
+ }
+}
+
+function remove_mc() {
+ removeStatusTab("MC");
+ if (current_tab == "MC") {
+ tab_change("Status");
+ }
+};
+
+function draw_sdql2() {
+ statcontentdiv.textContent = "";
+ var table = document.createElement("table");
+ for (var i = 0; i < sdql2.length; i++) {
+ var part = sdql2[i];
+ var tr = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.textContent = part[0];
+ var td2 = document.createElement("td");
+ if (part[2]) {
+ var a = document.createElement("a");
+ a.href = "?src=" + part[2] + ";statpanel_item_click=left";
+ a.textContent = part[1];
+ td2.appendChild(a);
+ } else {
+ td2.textContent = part[1];
+ }
+ tr.appendChild(td1);
+ tr.appendChild(td2);
+ table.appendChild(tr);
+ }
+ document.getElementById("statcontent").appendChild(table);
+}
+
+function draw_tickets() {
+ statcontentdiv.textContent = "";
+ var table = document.createElement("table");
+ if (!tickets) {
+ return;
+ }
+ for (var i = 0; i < tickets.length; i++) {
+ var part = tickets[i];
+ var tr = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.textContent = part[0];
+ var td2 = document.createElement("td");
+ if (part[2]) {
+ var a = document.createElement("a");
+ a.href = "?_src_=holder;admin_token=" + href_token + ";ahelp=" + part[2] + ";ahelp_action=ticket;statpanel_item_click=left;action=ticket";
+ a.textContent = part[1];
+ td2.appendChild(a);
+ } else if (part[3]) {
+ var a = document.createElement("a");
+ a.href = "?src=" + part[3] + ";statpanel_item_click=left";
+ a.textContent = part[1];
+ td2.appendChild(a);
+ } else {
+ td2.textContent = part[1];
+ }
+ tr.appendChild(td1);
+ tr.appendChild(td2);
+ table.appendChild(tr);
+ }
+ document.getElementById("statcontent").appendChild(table);
+}
+
+function draw_interviews() {
+ var body = document.createElement("div");
+ var header = document.createElement("h3");
+ header.textContent = "Interviews";
+ body.appendChild(header);
+ var manDiv = document.createElement("div");
+ manDiv.className = "interview_panel_controls"
+ var manLink = document.createElement("a");
+ manLink.textContent = "Open Interview Manager Panel";
+ manLink.href = "?_src_=holder;admin_token=" + href_token + ";interview_man=1;statpanel_item_click=left";
+ manDiv.appendChild(manLink);
+ body.appendChild(manDiv);
+
+ // List interview stats
+ var statsDiv = document.createElement("table");
+ statsDiv.className = "interview_panel_stats";
+ for (var key in interviewManager.status) {
+ var d = document.createElement("div");
+ var tr = document.createElement("tr");
+ var stat_name = document.createElement("td");
+ var stat_text = document.createElement("td");
+ stat_name.textContent = key;
+ stat_text.textContent = interviewManager.status[key];
+ tr.appendChild(stat_name);
+ tr.appendChild(stat_text);
+ statsDiv.appendChild(tr);
+ }
+ body.appendChild(statsDiv);
+ document.getElementById("statcontent").appendChild(body);
+
+ // List interviews if any are open
+ var table = document.createElement("table");
+ table.className = "interview_panel_table";
+ if (!interviewManager) {
+ return;
+ }
+ for (var i = 0; i < interviewManager.interviews.length; i++) {
+ var part = interviewManager.interviews[i];
+ var tr = document.createElement("tr");
+ var td = document.createElement("td");
+ var a = document.createElement("a");
+ a.textContent = part["status"];
+ a.href = "?_src_=holder;admin_token=" + href_token + ";interview=" + part["ref"] + ";statpanel_item_click=left";
+ td.appendChild(a);
+ tr.appendChild(td);
+ table.appendChild(tr);
+ }
+ document.getElementById("statcontent").appendChild(table);
+}
+
+function draw_spells(cat) {
+ statcontentdiv.textContent = "";
+ var table = document.createElement("table");
+ for (var i = 0; i < spells.length; i++) {
+ var part = spells[i];
+ if (part[0] != cat) continue;
+ var tr = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.textContent = part[1];
+ var td2 = document.createElement("td");
+ if (part[3]) {
+ var a = document.createElement("a");
+ a.href = "?src=" + part[3] + ";statpanel_item_click=left";
+ a.textContent = part[2];
+ td2.appendChild(a);
+ } else {
+ td2.textContent = part[2];
+ }
+ tr.appendChild(td1);
+ tr.appendChild(td2);
+ table.appendChild(tr);
+ }
+ document.getElementById("statcontent").appendChild(table);
+}
+
+function make_verb_onclick(command) {
+ return function () {
+ run_after_focus(function () {
+ Byond.command(command);
+ });
+ };
+}
+
+function draw_verbs(cat) {
+ statcontentdiv.textContent = "";
+ var table = document.createElement("div");
+ var additions = {}; // additional sub-categories to be rendered
+ table.className = "grid-container";
+ sortVerbs();
+ if (split_admin_tabs && cat.lastIndexOf(".") != -1) {
+ var splitName = cat.split(".");
+ if (splitName[0] === "Admin")
+ cat = splitName[1];
+ }
+ verbs.reverse(); // sort verbs backwards before we draw
+ for (var i = 0; i < verbs.length; ++i) {
+ var part = verbs[i];
+ var name = part[0];
+ if (split_admin_tabs && name.lastIndexOf(".") != -1) {
+ var splitName = name.split(".");
+ if (splitName[0] === "Admin")
+ name = splitName[1];
+ }
+ var command = part[1];
+
+ if (command && name.lastIndexOf(cat, 0) != -1 && (name.length == cat.length || name.charAt(cat.length) == ".")) {
+ var subCat = name.lastIndexOf(".") != -1 ? name.split(".")[1] : null;
+ if (subCat && !additions[subCat]) {
+ var newTable = document.createElement("div");
+ newTable.className = "grid-container";
+ additions[subCat] = newTable;
+ }
+
+ var a = document.createElement("a");
+ a.href = "#";
+ a.onclick = make_verb_onclick(command.replace(/\s/g, "-"));
+ a.className = "grid-item";
+ var t = document.createElement("span");
+ t.textContent = command;
+ t.className = "grid-item-text";
+ a.appendChild(t);
+ (subCat ? additions[subCat] : table).appendChild(a);
+ }
+ }
+
+ // Append base table to view
+ var content = document.getElementById("statcontent");
+ content.appendChild(table);
+
+ // Append additional sub-categories if relevant
+ for (var cat in additions) {
+ if (additions.hasOwnProperty(cat)) {
+ // do addition here
+ var header = document.createElement("h3");
+ header.textContent = cat;
+ content.appendChild(header);
+ content.appendChild(additions[cat]);
+ }
+ }
+}
+
+function set_theme(which) {
+ if (which == "light") {
+ document.body.className = "";
+ set_style_sheet("browserOutput_white");
+ } else if (which == "dark") {
+ document.body.className = "dark";
+ set_style_sheet("browserOutput");
+ }
+}
+
+function set_style_sheet(sheet) {
+ if (document.getElementById("goonStyle")) {
+ var currentSheet = document.getElementById("goonStyle");
+ currentSheet.parentElement.removeChild(currentSheet);
+ }
+ var head = document.getElementsByTagName('head')[0];
+ var sheetElement = document.createElement("link");
+ sheetElement.id = "goonStyle";
+ sheetElement.rel = "stylesheet";
+ sheetElement.type = "text/css";
+ sheetElement.href = sheet + ".css";
+ sheetElement.media = 'all';
+ head.appendChild(sheetElement);
+}
+
+function restoreFocus() {
+ run_after_focus(function () {
+ Byond.winset('map', {
+ focus: true,
+ });
+ });
+}
+
+function getCookie(cname) {
+ var name = cname + '=';
+ var ca = document.cookie.split(';');
+ for (var i = 0; i < ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0) == ' ') c = c.substring(1);
+ if (c.indexOf(name) === 0) {
+ return decoder(c.substring(name.length, c.length));
+ }
+ }
+ return '';
+}
+
+function add_verb_list(payload) {
+ var to_add = payload; // list of a list with category and verb inside it
+ to_add.sort(); // sort what we're adding
+ for (var i = 0; i < to_add.length; i++) {
+ var part = to_add[i];
+ if (!part[0])
+ continue;
+ var category = part[0];
+ if (category.indexOf(".") != -1) {
+ var splitName = category.split(".");
+ if (split_admin_tabs && splitName[0] === "Admin")
+ category = splitName[1];
+ else
+ category = splitName[0];
+ }
+ if (findVerbindex(part[1], verbs))
+ continue;
+ if (verb_tabs.includes(category)) {
+ verbs.push(part);
+ if (current_tab == category) {
+ draw_verbs(category); // redraw if we added a verb to the tab we're currently in
+ }
+ } else if (category) {
+ verb_tabs.push(category);
+ verbs.push(part);
+ createStatusTab(category);
+ }
+ }
+};
+
+function init_spells() {
+ var cat = "";
+ for (var i = 0; i < spell_tabs.length; i++) {
+ cat = spell_tabs[i];
+ if (cat.length > 0) {
+ verb_tabs.push(cat);
+ createStatusTab(cat);
+ }
+ }
+}
+
+document.addEventListener("mouseup", restoreFocus);
+document.addEventListener("keyup", restoreFocus);
+
+if (!current_tab) {
+ addPermanentTab("Status");
+ tab_change("Status");
+}
+
+window.onload = function () {
+ Byond.command("Update-Verbs");
+};
+
+Byond.subscribeTo('update_spells', function (payload) {
+ spell_tabs = payload.spell_tabs;
+ var do_update = false;
+ if (spell_tabs.includes(current_tab)) {
+ do_update = true;
+ }
+ init_spells();
+ if (payload.verblist) {
+ spells = payload.verblist;
+ if (do_update) {
+ draw_spells(current_tab);
+ }
+ } else {
+ remove_spells();
+ }
+});
+
+Byond.subscribeTo('remove_verb_list', function (v) {
+ var to_remove = v;
+ for (var i = 0; i < to_remove.length; i++) {
+ remove_verb(to_remove[i]);
+ }
+ check_verbs();
+ sortVerbs();
+ if (verb_tabs.includes(current_tab))
+ draw_verbs(current_tab);
+});
+
+// passes a 2D list of (verbcategory, verbname) creates tabs and adds verbs to respective list
+// example (IC, Say)
+Byond.subscribeTo('init_verbs', function (payload) {
+ wipe_verbs(); // remove all verb categories so we can replace them
+ checkStatusTab(); // remove all status tabs
+ verb_tabs = payload.panel_tabs;
+ verb_tabs.sort(); // sort it
+ var do_update = false;
+ var cat = "";
+ for (var i = 0; i < verb_tabs.length; i++) {
+ cat = verb_tabs[i];
+ createStatusTab(cat); // create a category if the verb doesn't exist yet
+ }
+ if (verb_tabs.includes(current_tab)) {
+ do_update = true;
+ }
+ if (payload.verblist) {
+ add_verb_list(payload.verblist);
+ sortVerbs(); // sort them
+ if (do_update) {
+ draw_verbs(current_tab);
+ }
+ }
+ SendTabsToByond();
+});
+
+Byond.subscribeTo('update_stat', function (payload) {
+ status_tab_parts = [payload.ping_str];
+ var parsed = payload.global_data;
+
+ for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]);
+
+ parsed = payload.other_str;
+
+ for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]);
+
+ if (current_tab == "Status") {
+ draw_status();
+ } else if (current_tab == "Debug Stat Panel") {
+ draw_debug();
+ }
+});
+
+Byond.subscribeTo('update_mc', function (payload) {
+ mc_tab_parts = payload.mc_data;
+ mc_tab_parts.splice(0, 0, ["Location:", payload.coord_entry]);
+
+ if (!verb_tabs.includes("MC")) {
+ verb_tabs.push("MC");
+ }
+
+ createStatusTab("MC");
+
+ if (current_tab == "MC") {
+ draw_mc();
+ }
+});
+
+Byond.subscribeTo('remove_spells', function () {
+ for (var s = 0; s < spell_tabs.length; s++) {
+ removeStatusTab(spell_tabs[s]);
+ }
+});
+
+Byond.subscribeTo('init_spells', function () {
+ var cat = "";
+ for (var i = 0; i < spell_tabs.length; i++) {
+ cat = spell_tabs[i];
+ if (cat.length > 0) {
+ verb_tabs.push(cat);
+ createStatusTab(cat);
+ }
+ }
+});
+
+Byond.subscribeTo('check_spells', function () {
+ for (var v = 0; v < spell_tabs.length; v++) {
+ spell_cat_check(spell_tabs[v]);
+ }
+});
+
+Byond.subscribeTo('create_debug', function () {
+ if (!document.getElementById("Debug Stat Panel")) {
+ addPermanentTab("Debug Stat Panel");
+ } else {
+ removePermanentTab("Debug Stat Panel");
+ }
+});
+
+Byond.subscribeTo('create_listedturf', function (TN) {
+ remove_listedturf(); // remove the last one if we had one
+ turfname = TN;
+ addPermanentTab(turfname);
+ tab_change(turfname);
+});
+
+Byond.subscribeTo('remove_admin_tabs', function () {
+ href_token = null;
+ remove_mc();
+ remove_tickets();
+ remove_sdql2();
+ remove_interviews();
+});
+
+Byond.subscribeTo('update_listedturf', function (TC) {
+ turfcontents = TC;
+ if (current_tab == turfname) {
+ draw_listedturf();
+ }
+});
+
+Byond.subscribeTo('update_interviews', function (I) {
+ interviewManager = I;
+ if (current_tab == "Tickets") {
+ draw_interviews();
+ }
+});
+
+Byond.subscribeTo('update_split_admin_tabs', function (status) {
+ status = (status == true);
+
+ if (split_admin_tabs !== status) {
+ if (split_admin_tabs === true) {
+ removeStatusTab("Events");
+ removeStatusTab("Fun");
+ removeStatusTab("Game");
+ }
+ update_verbs();
+ }
+ split_admin_tabs = status;
+});
+
+Byond.subscribeTo('add_admin_tabs', function (ht) {
+ href_token = ht;
+ addPermanentTab("MC");
+ addPermanentTab("Tickets");
+});
+
+Byond.subscribeTo('update_sdql2', function (S) {
+ sdql2 = S;
+ if (sdql2.length > 0 && !verb_tabs.includes("SDQL2")) {
+ verb_tabs.push("SDQL2");
+ addPermanentTab("SDQL2");
+ }
+ if (current_tab == "SDQL2") {
+ draw_sdql2();
+ }
+});
+
+Byond.subscribeTo('update_tickets', function (T) {
+ tickets = T;
+ if (!verb_tabs.includes("Tickets")) {
+ verb_tabs.push("Tickets");
+ addPermanentTab("Tickets");
+ }
+ if (current_tab == "Tickets") {
+ draw_tickets();
+ }
+});
+
+Byond.subscribeTo('remove_listedturf', remove_listedturf);
+
+Byond.subscribeTo('remove_sdql2', remove_sdql2);
+
+Byond.subscribeTo('remove_mc', remove_mc);
+
+Byond.subscribeTo('add_verb_list', add_verb_list);
diff --git a/tgstation.dme b/tgstation.dme
index 59657335006..a66d7762953 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -165,7 +165,6 @@
#include "code\__DEFINES\stat.dm"
#include "code\__DEFINES\stat_tracking.dm"
#include "code\__DEFINES\station.dm"
-#include "code\__DEFINES\statpanel.dm"
#include "code\__DEFINES\status_effects.dm"
#include "code\__DEFINES\storage.dm"
#include "code\__DEFINES\strippable.dm"
diff --git a/tgui/docs/tgui-for-custom-html-popups.md b/tgui/docs/tgui-for-custom-html-popups.md
index c93bbc2de80..5fe0e8ff78c 100644
--- a/tgui/docs/tgui-for-custom-html-popups.md
+++ b/tgui/docs/tgui-for-custom-html-popups.md
@@ -261,3 +261,21 @@ Byond.winget(null, 'url').then((serverUrl) => {
Byond.command('.quit');
});
```
+
+## Strict Mode
+
+Strict mode is a flag that you can set on tgui window.
+
+```dm
+window.initialize(strict_mode = TRUE)
+```
+
+If `TRUE`, unhandled errors and common mistakes result in a blue screen of death with a stack trace of the error, which you can use to debug it. Bluescreened window stops handling incoming messages and closes the active instance of tgui datum if there was one, to avoid a massive spam of errors and help to deal with them one by one.
+
+It can be defined in `window.initialize()` in DM, as shown above, or changed in runtime at runtime via `Byond.strictMode` to `true` or `false`.
+
+```js
+Byond.strictMode = true;
+```
+
+It is recommended that you keep this **ON** to detect hard to find bugs.
diff --git a/tgui/global.d.ts b/tgui/global.d.ts
index 73660d5b76e..c2e7b5ad00b 100644
--- a/tgui/global.d.ts
+++ b/tgui/global.d.ts
@@ -71,6 +71,18 @@ type ByondType = {
*/
IS_LTE_IE11: boolean;
+ /**
+ * If `true`, unhandled errors and common mistakes result in a blue screen
+ * of death, which stops this window from handling incoming messages and
+ * closes the active instance of tgui datum if there was one.
+ *
+ * It can be defined in window.initialize() in DM, or changed in runtime
+ * here via this property to `true` or `false`.
+ *
+ * It is recommended that you keep this ON to detect hard to find bugs.
+ */
+ strictMode: boolean;
+
/**
* Makes a BYOND call.
*
diff --git a/tgui/public/tgui.html b/tgui/public/tgui.html
index d7c1b4b5d97..58bbbb0c683 100644
--- a/tgui/public/tgui.html
+++ b/tgui/public/tgui.html
@@ -6,6 +6,7 @@
+