From c1f1a0d70965f8d0bdd1bf51e499595d3e336405 Mon Sep 17 00:00:00 2001
From: manofpepsi <71612753+manofpepsi@users.noreply.github.com>
Date: Mon, 9 Jun 2025 01:15:18 +0300
Subject: [PATCH] fixes statpanel by porting #66971 and #63991 from tg
---
code/__DEFINES/statpanel.dm | 8 -
code/__HELPERS/verbs.dm | 6 +-
code/_onclick/click.dm | 4 +-
code/controllers/subsystem/statpanel.dm | 336 +++--
code/datums/mind.dm | 2 +-
code/modules/admin/admin.dm | 32 +-
code/modules/admin/admin_verbs.dm | 2 +-
code/modules/client/client_defines.dm | 3 -
code/modules/client/client_procs.dm | 43 +-
.../modules/mob/dead/new_player/new_player.dm | 6 +-
code/modules/mob/mob.dm | 2 +-
html/statbrowser.css | 227 +++
html/statbrowser.html | 1269 -----------------
html/statbrowser.js | 1003 +++++++++++++
tgstation.dme | 1 -
15 files changed, 1485 insertions(+), 1459 deletions(-)
delete mode 100644 code/__DEFINES/statpanel.dm
create mode 100644 html/statbrowser.css
create mode 100644 html/statbrowser.js
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 005c8467053..97466b9c851 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -403,7 +403,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)
@@ -425,7 +425,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 a5aaad9f3a4..b57b383f067 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -5,14 +5,26 @@ 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 = 6
+ ///how many subsystem fires between updates of the MC tab
+ var/mc_wait = 5
+ /// how many subsystem fires between updates of the turf examine tab
+ var/turf_wait = 2
+ ///how many full runs this subsystem has completed. used for variable rate refreshes.
+ var/num_fires = 0
+
/datum/controller/subsystem/statpanels/fire(resumed = FALSE)
if (!resumed)
+ num_fires++
var/datum/map_config/cached = SSmapping.next_map_config
- 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"]",
@@ -26,129 +38,166 @@ 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")
- 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")
+
+ 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")
- if(target.stat_tab == "MC")
- var/turf/eye_turf = get_turf(target.eye)
- var/coord_entry = url_encode(COORD(eye_turf))
- if(!mc_data_encoded)
- generate_mc_data()
- target << output("[mc_data_encoded];[coord_entry]", "statbrowser:update_mc")
- if(target.stat_tab == "Tickets")
- var/list/ahelp_tickets = GLOB.ahelp_tickets.stat_entry()
- target << output("[url_encode(json_encode(ahelp_tickets))];", "statbrowser:update_tickets")
- var/datum/interview_manager/m = GLOB.interviews
-
- // get open interview count
- var/dc = 0
- for (var/ckey in m.open_interviews)
- var/datum/interview/I = m.open_interviews[ckey]
- if (I && !I.owner)
- dc++
- var/stat_string = "([m.open_interviews.len - dc] online / [dc] disconnected)"
-
- // Prepare each queued interview
- var/list/queued = list()
- for (var/datum/interview/I in m.interview_queue)
- queued += list(list(
- "ref" = REF(I),
- "status" = "\[[I.pos_in_queue]\]: [I.owner_ckey][!I.owner ? " (DC)": ""] \[INT-[I.id]\]"
- ))
-
- var/list/data = list(
- "status" = list(
- "Active:" = "[m.open_interviews.len] [stat_string]",
- "Queued:" = "[m.interview_queue.len]",
- "Closed:" = "[m.closed_interviews.len]"),
- "interviews" = queued
- )
-
- // Push update
- target << output("[url_encode(json_encode(data))];", "statbrowser:update_interviews")
+ target.stat_panel.send_message("add_admin_tabs", target.holder.href_token)
+
+ if(target.stat_tab == "MC" && ((num_fires % mc_wait == 0)))
+ set_MC_tab(target)
+
+ if(target.stat_tab == "Tickets" && num_fires % default_wait == 0)
+ set_tickets_tab(target)
+
if(!length(GLOB.sdql2_queries) && ("SDQL2" in target.panel_tabs))
- target << output("", "statbrowser:remove_sdql2")
- else if(length(GLOB.sdql2_queries) && (target.stat_tab == "SDQL2" || !("SDQL2" in target.panel_tabs)))
- var/list/sdql2A = list()
- sdql2A[++sdql2A.len] = list("", "Access Global SDQL2 List", REF(GLOB.sdql2_vv_statobj))
- var/list/sdql2B = list()
- for(var/i in GLOB.sdql2_queries)
- var/datum/sdql2_query/Q = i
- sdql2B = Q.generate_stat()
- sdql2A += sdql2B
- target << output(url_encode(json_encode(sdql2A)), "statbrowser:update_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)
+
if(target.mob)
- var/mob/M = target.mob
- if((target.stat_tab in target.spell_tabs) || !length(target.spell_tabs) && (length(M.mob_spell_list) || length(M.mind?.spell_list)))
- var/list/proc_holders = M.get_proc_holders()
- target.spell_tabs.Cut()
- for(var/phl in proc_holders)
- var/list/proc_holder_list = phl
- 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")
- if(M?.listed_turf)
- var/mob/target_mob = M
- if(!target_mob.TurfAdjacent(target_mob.listed_turf))
- target << output("", "statbrowser:remove_listedturf")
+ var/mob/target_mob = target.mob
+ if((target.stat_tab in target.spell_tabs) || !length(target.spell_tabs) && (length(target_mob.mob_spell_list) || length(target_mob.mind?.spell_list)))
+ if(num_fires % default_wait == 0)
+ set_spells_tab(target, target_mob)
+
+
+ if(target_mob?.listed_turf && num_fires % turf_wait == 0)
+ 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 == M?.listed_turf.name || !(M?.listed_turf.name in target.panel_tabs))
- var/list/overrides = list()
- var/list/turfitems = list()
- for(var/img in target.images)
- var/image/target_image = img
- if(!target_image.loc || target_image.loc.loc != target_mob.listed_turf || !target_image.override)
- continue
- overrides += target_image.loc
- turfitems[++turfitems.len] = list("[target_mob.listed_turf]", REF(target_mob.listed_turf), icon2html(target_mob.listed_turf, target, sourceonly=TRUE))
- for(var/tc in target_mob.listed_turf)
- var/atom/movable/turf_content = tc
- if(turf_content.mouse_opacity == MOUSE_OPACITY_TRANSPARENT)
- continue
- if(turf_content.invisibility > target_mob.see_invisible)
- continue
- if(turf_content in overrides)
- continue
- if(turf_content.IsObscured())
- continue
- if(length(turfitems) < 30) // only create images for the first 30 items on the turf, for performance reasons
- if(!(REF(turf_content) in cached_images))
- cached_images += REF(turf_content)
- turf_content.RegisterSignal(turf_content, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/atom, remove_from_cache)) // we reset cache if anything in it gets deleted
- if(ismob(turf_content) || length(turf_content.overlays) > 2)
- turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content), costly_icon2html(turf_content, target, sourceonly=TRUE))
- else
- turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content), icon2html(turf_content, target, sourceonly=TRUE))
- else
- turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content))
- else
- turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content))
- turfitems = url_encode(json_encode(turfitems))
- target << output("[turfitems];", "statbrowser:update_listedturf")
+
+ else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs))
+ set_turf_examine_tab(target, target_mob)
+
if(MC_TICK_CHECK)
return
+/datum/controller/subsystem/statpanels/proc/set_status_tab(client/target)
+ if(!global_data)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data()
+ return
+
+ 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 = COORD(eye_turf)
+ if(!mc_data)
+ generate_mc_data()
+ 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.stat_panel.send_message("update_tickets", ahelp_tickets)
+ var/datum/interview_manager/m = GLOB.interviews
+
+ // get open interview count
+ var/dc = 0
+ for (var/ckey in m.open_interviews)
+ var/datum/interview/current_interview = m.open_interviews[ckey]
+ if (current_interview && !current_interview.owner)
+ dc++
+ var/stat_string = "([m.open_interviews.len - dc] online / [dc] disconnected)"
+
+ // Prepare each queued interview
+ var/list/queued = list()
+ for (var/datum/interview/queued_interview in m.interview_queue)
+ queued += list(list(
+ "ref" = REF(queued_interview),
+ "status" = "\[[queued_interview.pos_in_queue]\]: [queued_interview.owner_ckey][!queued_interview.owner ? " (DC)": ""] \[INT-[queued_interview.id]\]"
+ ))
+
+ var/list/data = list(
+ "status" = list(
+ "Active:" = "[m.open_interviews.len] [stat_string]",
+ "Queued:" = "[m.interview_queue.len]",
+ "Closed:" = "[m.closed_interviews.len]"),
+ "interviews" = queued
+ )
+
+ // Push update
+ target.stat_panel.send_message("update_interviews", data)
+
+/datum/controller/subsystem/statpanels/proc/set_SDQL2_tab(client/target)
+ var/list/sdql2A = list()
+ sdql2A[++sdql2A.len] = list("", "Access Global SDQL2 List", REF(GLOB.sdql2_vv_statobj))
+ var/list/sdql2B = list()
+ for(var/datum/sdql2_query/query as anything in GLOB.sdql2_queries)
+ sdql2B = query.generate_stat()
+
+ sdql2A += sdql2B
+ 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()
+ target.spell_tabs.Cut()
+
+ for(var/proc_holder_list as anything in proc_holders)
+ target.spell_tabs |= proc_holder_list[1]
+
+ 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()
+ var/list/turfitems = list()
+ for(var/image/target_image as anything in target.images)
+ if(!target_image.loc || target_image.loc.loc != target_mob.listed_turf || !target_image.override)
+ continue
+ overrides += target_image.loc
+
+ turfitems[++turfitems.len] = list("[target_mob.listed_turf]", REF(target_mob.listed_turf), icon2html(target_mob.listed_turf, target, sourceonly=TRUE))
+
+ for(var/atom/movable/turf_content as anything in target_mob.listed_turf)
+ if(turf_content.mouse_opacity == MOUSE_OPACITY_TRANSPARENT)
+ continue
+ if(turf_content.invisibility > target_mob.see_invisible)
+ continue
+ if(turf_content in overrides)
+ continue
+ if(turf_content.IsObscured())
+ continue
+
+ if(length(turfitems) < 10) // only create images for the first 10 items on the turf, for performance reasons
+ var/turf_content_ref = REF(turf_content)
+ if(!(turf_content_ref in cached_images))
+ cached_images += turf_content_ref
+ turf_content.RegisterSignal(turf_content, COMSIG_PARENT_QDELETING, /atom/.proc/remove_from_cache) // we reset cache if anything in it gets deleted
+
+ if(ismob(turf_content) || length(turf_content.overlays) > 2)
+ turfitems[++turfitems.len] = list("[turf_content.name]", turf_content_ref, costly_icon2html(turf_content, target, sourceonly=TRUE))
+ else
+ turfitems[++turfitems.len] = list("[turf_content.name]", turf_content_ref, icon2html(turf_content, target, sourceonly=TRUE))
+ else
+ turfitems[++turfitems.len] = list("[turf_content.name]", turf_content_ref)
+ else
+ turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content))
+
+ 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]"),
@@ -159,50 +208,53 @@ SUBSYSTEM_DEF(statpanels)
list("Failsafe Controller:", Failsafe.stat_entry(), "\ref[Failsafe]"),
list("","")
)
- for(var/ss in Master.subsystems)
- var/datum/controller/subsystem/sub_system = ss
+ 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))
-/atom/proc/remove_from_cache()
- 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
+///immediately update the active statpanel tab of the target client
+/datum/controller/subsystem/statpanels/proc/immediate_send_stat_data(client/target)
+ if(!target.stat_panel.is_ready())
+ return FALSE
- stat_tab = tab
+ if(target.stat_tab == "Status")
+ set_status_tab(target)
+ return TRUE
-/client/verb/send_tabs(tabs as text|null)
- set name = "Send Tabs"
- set hidden = TRUE
+ var/mob/target_mob = target.mob
+ if((target.stat_tab in target.spell_tabs) || !length(target.spell_tabs) && (length(target_mob.mob_spell_list) || length(target_mob.mind?.spell_list)))
+ set_spells_tab(target, target_mob)
+ return TRUE
- panel_tabs |= tabs
+ if(target_mob?.listed_turf)
+ if(!target_mob.TurfAdjacent(target_mob.listed_turf))
+ target.stat_panel.send_message("removed_listedturf")
+ target_mob.listed_turf = null
-/client/verb/remove_tabs(tabs as text|null)
- set name = "Remove Tabs"
- set hidden = TRUE
+ else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs))
+ set_turf_examine_tab(target, target_mob)
+ return TRUE
- panel_tabs -= tabs
+ if(!target.holder)
+ return FALSE
-/client/verb/reset_tabs()
- set name = "Reset Tabs"
- set hidden = TRUE
+ if(target.stat_tab == "MC")
+ set_MC_tab(target)
+ return TRUE
- panel_tabs = list()
+ if(target.stat_tab == "Tickets")
+ set_tickets_tab(target)
+ return TRUE
-/client/verb/panel_ready()
- set name = "Panel Ready"
- set hidden = TRUE
+ if(!length(GLOB.sdql2_queries) && ("SDQL2" in target.panel_tabs))
+ target.stat_panel.send_message("remove_sdql2")
- statbrowser_ready = TRUE
- init_verbs()
+ else if(length(GLOB.sdql2_queries) && target.stat_tab == "SDQL2")
+ set_SDQL2_tab(target)
-/client/verb/update_verbs()
- set name = "Update Verbs"
- set hidden = TRUE
+/atom/proc/remove_from_cache()
+ SIGNAL_HANDLER
+ SSstatpanels.cached_images -= REF(src)
- 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 e3e3cde53fd..37f0596f2af 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -739,7 +739,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.dm b/code/modules/admin/admin.dm
index 41413dd2771..17e3bcf7583 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -19,29 +19,33 @@
if(!check_rights(0))
return
- var/dat = "
Game Panel
"
+ var/dat
if(SSticker.current_state <= GAME_STATE_PREGAME)
- dat += "(Force Roundstart Rulesets)
"
+ dat += "(Manage Dynamic Rulesets)
"
+ dat += "(Force Roundstart Rulesets)
"
if (GLOB.dynamic_forced_roundstart_ruleset.len > 0)
for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset)
- dat += {"-> [rule.name] <-
"}
- dat += "(Clear Rulesets)
"
- dat += "(Dynamic mode options)
"
- dat += "
"
+ dat += {"-> [rule.name] <-
"}
+ dat += "(Clear Rulesets)
"
+ dat += "(Dynamic mode options)
"
+ dat += "
"
if(SSticker.IsRoundInProgress())
- dat += "(Game Mode Panel)
"
+ dat += "(Game Mode Panel)
"
+ dat += "(Manage Dynamic Rulesets)
"
+ dat += "
"
dat += {"
-
- Create Object
- Quick Create Object
- Create Turf
- Create Mob
+ Create Object
+ Quick Create Object
+ Create Turf
+ Create Mob
"}
if(marked_datum && istype(marked_datum, /atom))
- dat += "Duplicate Marked Datum
"
+ dat += "Duplicate Marked Datum
"
- usr << browse(dat, "window=admin2;size=240x280")
+ var/datum/browser/browser = new(usr, "admin2", "Game Panel", 240, 280)
+ browser.set_content(dat)
+ browser.open()
return
////////////////////////////////////////////////////////////////////////////////////////////////ADMIN HELPER PROCS
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 863964ed3ef..61b0ede486e 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -807,7 +807,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 cc1177759d3..83988fc9f54 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -153,9 +153,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 7fa8139f90e..844b53545be 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -83,7 +83,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]")
@@ -208,6 +208,10 @@ 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)
@@ -324,9 +328,15 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if(SSinput.initialized)
set_macros()
+ // 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
- src << browse(file('html/statbrowser.html'), "window=statbrowser")
- addtimer(CALLBACK(src, PROC_REF(check_panel_loaded)), 30 SECONDS)
tgui_panel.initialize()
if(alert_mob_dupe_login && !holder)
@@ -1065,12 +1075,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
var/list/verbstoprocess = verbs.Copy()
if(mob)
verbstoprocess += mob.verbs
- for(var/AM in mob.contents)
- var/atom/movable/thing = AM
+ for(var/atom/movable/thing as anything in mob.contents)
verbstoprocess += thing.verbs
panel_tabs.Cut() // panel_tabs get reset in init_verbs on JS side anyway
- for(var/thing in verbstoprocess)
- var/procpath/verb_to_init = thing
+ for(var/procpath/verb_to_init as anything in verbstoprocess)
if(!verb_to_init)
continue
if(verb_to_init.hidden)
@@ -1079,10 +1087,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 "))
@@ -1135,6 +1143,23 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
else
SSambience.ambience_listening_clients -= 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 5b12f751b01..62d77d3e5d9 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -510,14 +510,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 d1ddce7b409..cb65108d854 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -954,7 +954,7 @@
LAZYREMOVE(mob_spell_list, S)
qdel(S)
if(client)
- client << output(null, "statbrowser:check_spells")
+ client.stat_panel.send_message("check_spells")
///Return any anti magic atom on this mob that matches the magic type
/mob/proc/anti_magic_check(magic = TRUE, holy = FALSE, tinfoil = FALSE, chargecost = 1, self = FALSE)
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 b32c657679d..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 35de42d4377..d1393920ed9 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -138,7 +138,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"