From 10ba80973bfc977b17aa116f3b630862e9e70a7c Mon Sep 17 00:00:00 2001 From: Kylerace Date: Sun, 23 Jan 2022 00:46:46 -0800 Subject: [PATCH] makes most statpanel tabs update a tenth or so as often (>= 4 seconds instead of 4 deciseconds) because theyre wastful of cpu (#63991) makes most updating stat panel tabs update once every 4 seconds instead of 4 deciseconds, but switching tabs instantly updates statpanel data for you. also makes examining a turf make flat icons for a maximum of 10 contents instead of 30 because its ridiculous to call getFuckingFlatIcon() wrappers that many times. also makes SSfluids not have SS_TICKER and updates its wait accordingly because theres no reason for it to be a ticker subsystem the mc tab updates every 2 seconds unless someone has the pref enabled to refresh it quickly because SOME UNILLUMINATED LEMONS absolutely must watch overtime spikes in real time statpanels can take between 1-3% of masters total processing time at very high pop, which is silly considering theres no need for someone to know any of the data updated accurate to less than half of a second. The only reason it needed to update so fast was because it looked awful when switching tabs, which will only be updated on the next fire. now switching tabs updates data instantly so theres no need to update the rest of the data quickly. also makes each stat tab update into its own proc so we can tell how much each tab update costs --- .github/CONTRIBUTING.md | 2 + .github/guides/MC_tab.md | 36 +++ .github/guides/STANDARDS.md | 2 +- .../subsystem/processing/fluids.dm | 4 +- code/controllers/subsystem/statpanel.dm | 285 ++++++++++++------ code/modules/atmospherics/Atmospherics.md | 8 +- code/modules/client/client_procs.dm | 6 +- code/modules/client/preferences/statpanel.dm | 11 + tgstation.dme | 1 + .../features/game_preferences/admin.tsx | 7 + 10 files changed, 250 insertions(+), 112 deletions(-) create mode 100644 .github/guides/MC_tab.md create mode 100644 code/modules/client/preferences/statpanel.dm diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b60bced464e45..8c59153529bde 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -111,6 +111,8 @@ Things you **CAN'T** do: [AI Datums](../code/datums/ai/making_your_ai.md) +[MC Tab Guide](./guides/MC_tab.md) + ## Pull Request Process There is no strict process when it comes to merging pull requests. Pull requests will sometimes take a while before they are looked at by a maintainer; the bigger the change, the more time it will take before they are accepted into the code. Every team member is a volunteer who is giving up their own time to help maintain and contribute, so please be courteous and respectful. Here are some helpful ways to make it easier for you and for the maintainers when making a pull request. diff --git a/.github/guides/MC_tab.md b/.github/guides/MC_tab.md new file mode 100644 index 0000000000000..01ad1e6aa174c --- /dev/null +++ b/.github/guides/MC_tab.md @@ -0,0 +1,36 @@ +The MC tab hold information on how the game is performing. Here's a crash course on what the most important of those numbers mean. + +If you already know what these numbers mean and you want to see them update faster than the default refresh rate of once every 2 seconds, you can enable the admin pref to make the MC tab refresh every 4 deciseconds. Please don't do this unless you actually need that information at a faster refresh rate since updating every subsystems information is expensive. + +# Main Entries: + + * CPU: What percentage of a tick the game is using before starting the next tick. If this is above 100 it means we are over budget. + + * TickCount: How many ticks should have elapsed since the start of the game if no ticks were ever delayed from starting. + + * TickDrift: How many ticks since the game started that have been delayed. Essentially this is how many ticks the game is running behind. If this is increasing then the game is currently not able to keep up with demand. + + * Internal Tick Usage: You might have heard of this referred to as "maptick". It's how much of the tick that an internal byond function called SendMaps() has taken recently. The higher this is the less time our code has to run. SendMaps() deals with sending players updates of their view of the game world so it has to run every tick but it's expensive so ideally this is optimized as much as possible. You can see a more detailed breakdown of the cost of SendMaps by looking at the profiler in the debug tab -> "Send Maps Profile". + +# Master Controller Entry: + + * TickRate: How many Byond ticks go between each master controller iteration. By default this is 1 meaning the MC runs once every byond tick. But certain configurations can increase this slightly. + + * Iteration: How many times the MC has ran since starting. + + * TickLimit: This SHOULD be what percentage of the tick the MC can use when it starts a run, however currently it just represents how much of the tick the MC can use by the time that SSstatpanels fires. Someone should fix that. + +# Subsystem Entries: + +Subsystems will typically have a base stat entry of the form: +[ ] Name 12ms|28%(2%)|3 + +The brackets hold a letter if the subsystem is in a state other than idle. + +The first numbered entry is the cost of the subsystem, which is a running average of how many milliseconds the subsystem takes to complete a full run. This is increased every time the subsystem resumes an uncompleted run or starts a new run and decays when runs take less time. If this balloons to huge values then it means that the amount of work the subsystem needs to complete in a run is far greater than the amount of time it actually has to execute in whenever it is its turn to fire. + +The second numbered entry is like cost, but in percentage of an ideal tick this subsystem takes to complete a run. They both represent the same data. + +The third entry (2%) is how much time this subsystem spent executing beyond the time it was allocated by the MC. This is bad, it means that this subsystem doesn't yield when it's taking too much time and makes the job of the MC harder. The MC will attempt to account for this but it is better for all subsystems to be able to correctly yield when their turn is done. + +The fourth entry represents how many times this subsystem fires before it completes a run. diff --git a/.github/guides/STANDARDS.md b/.github/guides/STANDARDS.md index 2ce126ba752ea..71668005e3195 100644 --- a/.github/guides/STANDARDS.md +++ b/.github/guides/STANDARDS.md @@ -343,7 +343,7 @@ It's listed here in the hope that it will prevent fruitless debugging in future. #### Icon hell -The ‘transparent’ icon state causes fucked visual behavior when used on turfs, something to do with underlays and overlays. +Due to how they are internally represented as part of appearance, overlays and underlays which have an icon_state named the same as an icon_state on the parent object will use the parent's icon_state and look completely wrong. This has caused two bugs with underlay lighting whenever a turf had the icon_state of "transparent" or "dark" and their lighting objects also had those states - because when the lighting underlays were in those modes they would be rendered by the client to look like the icons the floor used. When adding something as an overlay or an underlay make sure it can't match icon_state names with whatever you're adding it to. ## SQL diff --git a/code/controllers/subsystem/processing/fluids.dm b/code/controllers/subsystem/processing/fluids.dm index c4fa13d693995..70903e0088c79 100644 --- a/code/controllers/subsystem/processing/fluids.dm +++ b/code/controllers/subsystem/processing/fluids.dm @@ -1,5 +1,5 @@ PROCESSING_SUBSYSTEM_DEF(fluids) name = "Fluids" - wait = 20 + wait = 10 stat_tag = "FD" //its actually Fluid Ducts - flags = SS_NO_INIT | SS_TICKER + flags = SS_NO_INIT diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index 8b97c14d3114d..0dcaff6435d0c 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -9,8 +9,18 @@ SUBSYSTEM_DEF(statpanels) var/mc_data_encoded 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 + ///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. + 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( "Map: [SSmapping.config?.map_name || "Loading..."]", @@ -29,123 +39,162 @@ SUBSYSTEM_DEF(statpanels) encoded_global_data = url_encode(json_encode(global_data)) src.currentrun = GLOB.clients.Copy() mc_data_encoded = null + var/list/currentrun = src.currentrun while(length(currentrun)) var/client/target = currentrun[length(currentrun)] currentrun.len-- + if(!target.statbrowser_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") else target << output("[!!(target.prefs.toggles & SPLIT_ADMIN_TABS)]", "statbrowser:update_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") + + if(target.stat_tab == "MC" && ((num_fires % mc_wait == 0) || target?.prefs.read_preference(/datum/preference/toggle/fast_mc_refresh))) + 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") + + 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 + 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 % default_wait == 0) if(!target_mob.TurfAdjacent(target_mob.listed_turf)) target << output("", "statbrowser: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, /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]", 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(!encoded_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") + +/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) + generate_mc_data() + target << output("[mc_data_encoded];[coord_entry]", "statbrowser:update_mc") + +/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") + 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 << output("[url_encode(json_encode(data))];", "statbrowser:update_interviews") + +/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 << output(url_encode(json_encode(sdql2A)), "statbrowser:update_sdql2") + +/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] + + 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") + +/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 = url_encode(json_encode(turfitems)) + target << output("[turfitems];", "statbrowser:update_listedturf") /datum/controller/subsystem/statpanels/proc/generate_mc_data() var/list/mc_data = list( @@ -159,12 +208,51 @@ 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)) +///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) + return FALSE + + if(target.stat_tab == "Status") + set_status_tab(target) + return 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 + + if(target_mob?.listed_turf) + if(!target_mob.TurfAdjacent(target_mob.listed_turf)) + target << output("", "statbrowser: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)) + set_turf_examine_tab(target, target_mob) + return TRUE + + if(!target.holder) + return FALSE + + if(target.stat_tab == "MC") + set_MC_tab(target) + return TRUE + + if(target.stat_tab == "Tickets") + set_tickets_tab(target) + return TRUE + + 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") + set_SDQL2_tab(target) + /atom/proc/remove_from_cache() SIGNAL_HANDLER SSstatpanels.cached_images -= REF(src) @@ -175,6 +263,7 @@ SUBSYSTEM_DEF(statpanels) set hidden = TRUE stat_tab = tab + SSstatpanels.immediate_send_stat_data(src) /client/verb/send_tabs(tabs as text|null) set name = "Send Tabs" diff --git a/code/modules/atmospherics/Atmospherics.md b/code/modules/atmospherics/Atmospherics.md index a71bd4887c134..6d61a8cf16bcc 100644 --- a/code/modules/atmospherics/Atmospherics.md +++ b/code/modules/atmospherics/Atmospherics.md @@ -336,13 +336,7 @@ The MC entry for SSAir is very helpful for debugging, and it is good to understa *Figure 6.1: SSAir sitting doing little to nothing turf wise, only processing pipenets and atmos machines* -As you can see here, SSAir is a bit of a jumble, don't worry, it'll make sense in a second. The first line is in this order: cost, tick_usage, tick_overrun, ticks. -All of these are averages by the way. - -* *`cost`* Cost is the raw time spent running the subsystem in milliseconds -* *`tick_usage`* The percent of each byond tick the last fire() took. Tends to be twice cost, good for comparing with overrun. -* *`tick_overrun`* A percentage of how far past our allotted time we ran. This is what causes Time Dilation, it's bad. -* *`ticks`* The amount of subsystem fires it takes to run through all the subprocesses once. +If you aren't familiar with the default subsystem stats, you can see them explained here: [][http://codedocs.tgstation13.org/.github/guides/MC_tab.md] The second line is the cost each subprocess contributed per full cycle, this is a rolling average. It'll give you a good feel for what is misbehaving. (The only exception to this is pipenet rebuilds, the last entry. Because of its nature as something that can happen at any time, it doesn't have a rolling average, instead it just displays the time it used last process) diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 44f4568097905..5611b817f0baa 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1075,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) diff --git a/code/modules/client/preferences/statpanel.dm b/code/modules/client/preferences/statpanel.dm new file mode 100644 index 0000000000000..4c9e3c51b262b --- /dev/null +++ b/code/modules/client/preferences/statpanel.dm @@ -0,0 +1,11 @@ +/datum/preference/toggle/fast_mc_refresh + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "fast_mc_refresh" + savefile_identifier = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/fast_mc_refresh/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return is_admin(preferences.parent) diff --git a/tgstation.dme b/tgstation.dme index 6069750929987..e4ec6c175acbd 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2319,6 +2319,7 @@ #include "code\modules\client\preferences\security_department.dm" #include "code\modules\client\preferences\skin_tone.dm" #include "code\modules\client\preferences\species.dm" +#include "code\modules\client\preferences\statpanel.dm" #include "code\modules\client\preferences\tgui.dm" #include "code\modules\client\preferences\tooltips.dm" #include "code\modules\client\preferences\ui_style.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx index bee211699484b..acc6707e4fb67 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx @@ -20,3 +20,10 @@ export const bypass_deadmin_in_centcom: FeatureToggle = { description: "Whether or not to always remain an admin when spawned in CentCom.", component: CheckboxInput, }; + +export const fast_mc_refreshes: FeatureToggle = { + name: "Enable fast MC stat panel refreshes", + category: "ADMIN", + description: "Whether or not the MC tab of the Stat Panel refreshes fast. This is expensive so make sure you need it.", + component: CheckboxInput, +};