From 9ae7e2fac5ae65e565645c2b08a1a9bc06931758 Mon Sep 17 00:00:00 2001 From: Iajret Creature <122297233+Steals-The-PRs@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:33:03 +0300 Subject: [PATCH] [MIRROR] Controller Overview UI (#2076) (#2972) * Controller Overview UI (#82739) * Controller Overview UI --------- Co-authored-by: NovaBot <154629622+NovaBot13@users.noreply.github.com> Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> --- code/controllers/master.dm | 79 +++++- .../tgui/interfaces/ControllerOverview.tsx | 245 ++++++++++++++++++ 2 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 tgui/packages/tgui/interfaces/ControllerOverview.tsx diff --git a/code/controllers/master.dm b/code/controllers/master.dm index 32093c12745..0ba5914da41 100644 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -76,6 +76,9 @@ GLOBAL_REAL(Master, /datum/controller/master) ///used by CHECK_TICK as well so that the procs subsystems call can obey that SS's tick limits var/static/current_ticklimit = TICK_LIMIT_RUNNING + /// Whether the Overview UI will update as fast as possible for viewers. + var/overview_fast_update = FALSE + /datum/controller/master/New() if(!config) config = new @@ -135,6 +138,78 @@ GLOBAL_REAL(Master, /datum/controller/master) ss.Shutdown() log_world("Shutdown complete") +ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "View the current states of the Subsystem Controllers.", ADMIN_CATEGORY_SERVER) + Master.ui_interact(user.mob) + +/datum/controller/master/ui_status(mob/user, datum/ui_state/state) + if(!user.client?.holder?.check_for_rights(R_SERVER|R_DEBUG)) + return UI_CLOSE + return UI_INTERACTIVE + +/datum/controller/master/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(isnull(ui)) + ui = new /datum/tgui(user, src, "ControllerOverview") + ui.open() + +/datum/controller/master/ui_data(mob/user) + var/list/data = list() + + var/list/subsystem_data = list() + for(var/datum/controller/subsystem/subsystem as anything in subsystems) + subsystem_data += list(list( + "name" = subsystem.name, + "ref" = REF(subsystem), + "init_order" = subsystem.init_order, + "last_fire" = subsystem.last_fire, + "next_fire" = subsystem.next_fire, + "can_fire" = subsystem.can_fire, + "doesnt_fire" = !!(subsystem.flags & SS_NO_FIRE), + "cost_ms" = subsystem.cost, + "tick_usage" = subsystem.tick_usage, + "tick_overrun" = subsystem.tick_overrun, + "initialized" = subsystem.initialized, + "initialization_failure_message" = subsystem.initialization_failure_message, + )) + data["subsystems"] = subsystem_data + data["world_time"] = world.time + data["map_cpu"] = world.map_cpu + data["fast_update"] = overview_fast_update + + return data + +/datum/controller/master/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return TRUE + + switch(action) + if("toggle_fast_update") + overview_fast_update = !overview_fast_update + return TRUE + + if("view_variables") + var/datum/controller/subsystem/subsystem = locate(params["ref"]) in subsystems + if(isnull(subsystem)) + to_chat(ui.user, span_warning("Failed to locate subsystem.")) + return + SSadmin_verbs.dynamic_invoke_verb(ui.user, /datum/admin_verb/debug_variables, subsystem) + return TRUE + +/datum/controller/master/proc/check_and_perform_fast_update() + PRIVATE_PROC(TRUE) + set waitfor = FALSE + + + if(!overview_fast_update) + return + + var/static/already_updating = FALSE + if(already_updating) + return + already_updating = TRUE + SStgui.update_uis(src) + already_updating = FALSE + // Returns 1 if we created a new mc, 0 if we couldn't due to a recent restart, // -1 if we encountered a runtime trying to recreate it /proc/Recreate_MC() @@ -582,11 +657,9 @@ GLOBAL_REAL(Master, /datum/controller/master) if (processing * sleep_delta <= world.tick_lag) current_ticklimit -= (TICK_LIMIT_RUNNING * 0.25) //reserve the tail 1/4 of the next tick for the mc if we plan on running next tick + check_and_perform_fast_update() sleep(world.tick_lag * (processing * sleep_delta)) - - - // This is what decides if something should run. /datum/controller/master/proc/CheckQueue(list/subsystemstocheck) . = 0 //so the mc knows if we runtimed diff --git a/tgui/packages/tgui/interfaces/ControllerOverview.tsx b/tgui/packages/tgui/interfaces/ControllerOverview.tsx new file mode 100644 index 00000000000..5b5c210b7bb --- /dev/null +++ b/tgui/packages/tgui/interfaces/ControllerOverview.tsx @@ -0,0 +1,245 @@ +import { BooleanLike } from 'common/react'; +import { createSearch } from 'common/string'; +import { useMemo, useState } from 'react'; + +import { useBackend } from '../backend'; +import { + Button, + Collapsible, + Dropdown, + Input, + LabeledList, + Section, + Stack, +} from '../components'; +import { Window } from '../layouts'; + +type SubsystemData = { + name: string; + ref: string; + init_order: number; + last_fire: number; + next_fire: number; + can_fire: BooleanLike; + doesnt_fire: BooleanLike; + cost_ms: number; + tick_usage: number; + tick_overrun: number; + initialized: BooleanLike; + initialization_failure_message: string | undefined; +}; + +type ControllerData = { + world_time: number; + fast_update: BooleanLike; + map_cpu: number; + subsystems: SubsystemData[]; +}; + +const SubsystemView = (props: { data: SubsystemData }) => { + const { act } = useBackend(); + const { data } = props; + const { + name, + ref, + init_order, + last_fire, + next_fire, + can_fire, + doesnt_fire, + cost_ms, + tick_usage, + tick_overrun, + initialized, + initialization_failure_message, + } = data; + + let icon = 'play'; + if (!initialized) { + icon = 'circle-exclamation'; + } else if (doesnt_fire) { + icon = 'check'; + } else if (!can_fire) { + icon = 'pause'; + } + + return ( + { + act('view_variables', { ref: ref }); + }} + /> + } + > + + {init_order} + {last_fire} + {next_fire} + {cost_ms}ms + + {(tick_usage * 0.01).toFixed(2)}% + + + {(tick_overrun * 0.01).toFixed(2)}% + + {initialization_failure_message ? ( + + {initialization_failure_message} + + ) : undefined} + + + ); +}; + +enum SubsystemSortBy { + INIT_ORDER = 'Init Order', + NAME = 'Name', + LAST_FIRE = 'Last Fire', + NEXT_FIRE = 'Next Fire', + TICK_USAGE = 'Tick Usage', + TICK_OVERRUN = 'Tick Overrun', + COST = 'Cost', +} + +const sortSubsystemBy = ( + subsystems: SubsystemData[], + sortBy: SubsystemSortBy, + asending: boolean = true, +) => { + let sorted = subsystems.sort((left, right) => { + switch (sortBy) { + case SubsystemSortBy.INIT_ORDER: + return left.init_order - right.init_order; + case SubsystemSortBy.NAME: + return left.name.localeCompare(right.name); + case SubsystemSortBy.LAST_FIRE: + return left.last_fire - right.last_fire; + case SubsystemSortBy.NEXT_FIRE: + return left.next_fire - right.next_fire; + case SubsystemSortBy.TICK_USAGE: + return left.tick_usage - right.tick_usage; + case SubsystemSortBy.TICK_OVERRUN: + return left.tick_overrun - right.tick_overrun; + case SubsystemSortBy.COST: + return left.cost_ms - right.cost_ms; + } + }); + if (!asending) { + sorted.reverse(); + } + return sorted; +}; + +export const ControllerOverview = () => { + const { act, data } = useBackend(); + const { world_time, map_cpu, subsystems, fast_update } = data; + + const [filterName, setFilterName] = useState(''); + const [sortBy, setSortBy] = useState(SubsystemSortBy.NAME); + const [sortAscending, setSortAscending] = useState(true); + + let filteredSubsystems = useMemo(() => { + if (!filterName) { + return subsystems; + } + + return subsystems.filter(() => + createSearch(filterName, (subsystem: SubsystemData) => subsystem.name), + ); + }, [filterName, subsystems]); + + let sortedSubsystems = useMemo(() => { + return sortSubsystemBy(filteredSubsystems, sortBy, sortAscending); + }, [sortBy, sortAscending, filteredSubsystems]); + + const overallUsage = subsystems.reduce( + (acc, subsystem) => acc + subsystem.tick_usage, + 0, + ); + const overallOverrun = subsystems.reduce( + (acc, subsystem) => acc + subsystem.tick_overrun, + 0, + ); + + return ( + + +
+ + World Time: {world_time} + Map CPU: {map_cpu.toFixed(2)}% + + Overall Usage: {(overallUsage * 0.01).toFixed(2)}% + + + Overall Overrun: {(overallOverrun * 0.01).toFixed(2)}% + + +
+
{ + act('toggle_fast_update'); + }} + /> + } + > + setFilterName(value)} + /> + + + + + + +
+
+ + {sortedSubsystems.map((subsystem) => ( + + ))} + +
+
+
+ ); +};