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)}%
+
+
+
+
+
+
+ {sortedSubsystems.map((subsystem) => (
+
+ ))}
+
+
+
+
+ );
+};