mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-30 00:29:02 +01:00
Controller Overview UI (#82739)
This commit is contained in:
@@ -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()
|
||||
@@ -578,11 +653,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
|
||||
|
||||
@@ -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 (
|
||||
<Collapsible
|
||||
title={name}
|
||||
key={ref}
|
||||
icon={icon}
|
||||
buttons={
|
||||
<Button
|
||||
icon="wrench"
|
||||
tooltip="View Variables"
|
||||
onClick={() => {
|
||||
act('view_variables', { ref: ref });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Init Order">{init_order}</LabeledList.Item>
|
||||
<LabeledList.Item label="Last Fire">{last_fire}</LabeledList.Item>
|
||||
<LabeledList.Item label="Next Fire">{next_fire}</LabeledList.Item>
|
||||
<LabeledList.Item label="Cost">{cost_ms}ms</LabeledList.Item>
|
||||
<LabeledList.Item label="Tick Usage">
|
||||
{(tick_usage * 0.01).toFixed(2)}%
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Tick Overrun">
|
||||
{(tick_overrun * 0.01).toFixed(2)}%
|
||||
</LabeledList.Item>
|
||||
{initialization_failure_message ? (
|
||||
<LabeledList.Item color="bad">
|
||||
{initialization_failure_message}
|
||||
</LabeledList.Item>
|
||||
) : undefined}
|
||||
</LabeledList>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
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<ControllerData>();
|
||||
const { world_time, map_cpu, subsystems, fast_update } = data;
|
||||
|
||||
const [filterName, setFilterName] = useState('');
|
||||
const [sortBy, setSortBy] = useState(SubsystemSortBy.NAME);
|
||||
const [sortAscending, setSortAscending] = useState<boolean>(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 (
|
||||
<Window>
|
||||
<Window.Content>
|
||||
<Section title="Master Overview">
|
||||
<Stack vertical>
|
||||
<Stack.Item>World Time: {world_time}</Stack.Item>
|
||||
<Stack.Item>Map CPU: {map_cpu.toFixed(2)}%</Stack.Item>
|
||||
<Stack.Item>
|
||||
Overall Usage: {(overallUsage * 0.01).toFixed(2)}%
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
Overall Overrun: {(overallOverrun * 0.01).toFixed(2)}%
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
<Section
|
||||
title="Filtering/Sorting"
|
||||
buttons={
|
||||
<Button
|
||||
tooltip="Fast Update"
|
||||
icon="fast-forward"
|
||||
color={fast_update ? 'good' : 'bad'}
|
||||
onClick={() => {
|
||||
act('toggle_fast_update');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
placeholder="Filter by name"
|
||||
value={filterName}
|
||||
onChange={(e, value) => setFilterName(value)}
|
||||
/>
|
||||
<Button
|
||||
icon="trash"
|
||||
tooltip="Reset filter"
|
||||
onClick={() => setFilterName('')}
|
||||
disabled={filterName === undefined}
|
||||
/>
|
||||
<Dropdown
|
||||
options={Object.values(SubsystemSortBy)}
|
||||
selected={sortBy}
|
||||
onSelected={(value) => setSortBy(value as SubsystemSortBy)}
|
||||
/>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
selected={sortAscending}
|
||||
onClick={() => setSortAscending(true)}
|
||||
>
|
||||
Ascending
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
selected={!sortAscending}
|
||||
onClick={() => setSortAscending(false)}
|
||||
>
|
||||
Descending
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
<Section title="Subsystem Overview" scrollable>
|
||||
<Stack vertical>
|
||||
{sortedSubsystems.map((subsystem) => (
|
||||
<SubsystemView key={subsystem.ref} data={subsystem} />
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user