From 9d6a141e5b672650743c347d9eda782daed18acf Mon Sep 17 00:00:00 2001 From: silicons <2003111+silicons@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:41:25 -0800 Subject: [PATCH] mc update: init stages, better subsystem init tracking, etc (#6857) modifies the hollow metrics API; that'll probably be implemented soon adds init stages changes SSatoms to not abuse its `initialized` variable closes #6858 --- citadel.dme | 20 +- code/__DEFINES/MC.dm | 118 ---- .../__DEFINES/controllers/_master-runlevel.dm | 32 + code/__DEFINES/controllers/_master.dm | 59 ++ .../{_repositories.dm => _repository.dm} | 0 code/__DEFINES/controllers/_subsystem-init.dm | 92 +++ .../controllers/_subsystem-priority.dm | 68 +++ code/__DEFINES/controllers/_subsystem.dm | 164 ++++++ code/__DEFINES/controllers/_subsystems.dm | 237 -------- code/__DEFINES/controllers/atoms.dm | 37 ++ code/__DEFINES/controllers/timer.dm | 10 + code/__DEFINES/metrics.dm | 6 + code/__HELPERS/sorts/comparators.dm | 19 - code/___compile_options.dm | 8 - code/controllers/failsafe.dm | 4 + code/controllers/master.dm | 546 +++++++++++------- code/controllers/subsystem.dm | 256 +++++--- .../subsystem/__test_bad_subsystem_sleeps.dm | 44 ++ code/controllers/subsystem/ai_holders.dm | 3 +- code/controllers/subsystem/ai_movement.dm | 11 +- code/controllers/subsystem/ai_scheduling.dm | 8 +- code/controllers/subsystem/air.dm | 17 +- code/controllers/subsystem/alarm.dm | 2 +- .../controllers/subsystem/ambient_lighting.dm | 2 +- code/controllers/subsystem/ao.dm | 2 +- code/controllers/subsystem/assets.dm | 6 +- code/controllers/subsystem/atoms.dm | 47 +- code/controllers/subsystem/automata.dm | 2 +- .../subsystem/characters/_characters.dm | 2 +- code/controllers/subsystem/chat.dm | 3 +- code/controllers/subsystem/chemistry.dm | 1 + code/controllers/subsystem/dbcore/_dbcore.dm | 3 +- code/controllers/subsystem/early_init.dm | 3 +- .../subsystem/emergency_shuttle.dm | 1 + code/controllers/subsystem/events.dm | 2 +- code/controllers/subsystem/fail2topic.dm | 3 +- code/controllers/subsystem/game_master.dm | 2 +- code/controllers/subsystem/holomaps.dm | 2 +- code/controllers/subsystem/icon_smooth.dm | 4 +- code/controllers/subsystem/inactivity.dm | 4 +- code/controllers/subsystem/input.dm | 9 +- code/controllers/subsystem/ipintel.dm | 3 +- code/controllers/subsystem/job/_job.dm | 3 +- code/controllers/subsystem/legacy_atc.dm | 2 +- code/controllers/subsystem/legacy_lore.dm | 2 +- code/controllers/subsystem/lighting.dm | 2 +- code/controllers/subsystem/machines.dm | 15 +- .../controllers/subsystem/mapping/_mapping.dm | 3 +- code/controllers/subsystem/materials.dm | 2 +- code/controllers/subsystem/media_tracks.dm | 2 +- code/controllers/subsystem/minimaps.dm | 2 +- code/controllers/subsystem/nanoui.dm | 15 +- code/controllers/subsystem/nightshift.dm | 5 +- code/controllers/subsystem/overlays.dm | 3 +- code/controllers/subsystem/overmap.dm | 2 +- code/controllers/subsystem/parallax.dm | 2 +- .../subsystem/persistence/persistence.dm | 5 +- code/controllers/subsystem/planets.dm | 2 +- code/controllers/subsystem/plants.dm | 2 +- code/controllers/subsystem/playtime.dm | 2 +- code/controllers/subsystem/preferences.dm | 3 +- .../subsystem/processing/circuits.dm | 2 +- .../subsystem/processing/instruments.dm | 3 +- .../subsystem/processing/processing.dm | 4 +- code/controllers/subsystem/repository.dm | 3 +- code/controllers/subsystem/shuttles.dm | 3 +- code/controllers/subsystem/sound/_sound.dm | 3 +- code/controllers/subsystem/spatial_grids.dm | 3 +- code/controllers/subsystem/statpanel.dm | 20 +- code/controllers/subsystem/status_effects.dm | 2 +- code/controllers/subsystem/sun.dm | 1 + code/controllers/subsystem/supply.dm | 2 +- code/controllers/subsystem/ticker.dm | 3 +- code/controllers/subsystem/timer.dm | 1 + .../subsystem/{lobby.dm => titlescreen.dm} | 21 +- code/controllers/subsystem/transcore_vr.dm | 4 +- code/controllers/subsystem/transfer.dm | 2 +- code/controllers/subsystem/turbolift.dm | 2 +- code/controllers/subsystem/vis_overlays.dm | 2 +- code/controllers/subsystem/vote.dm | 4 +- code/controllers/subsystem/world.dm | 2 +- code/controllers/subsystem/xenoarch.dm | 2 +- code/controllers/subsystem/zcopy.dm | 2 +- .../atoms/atoms_initializing_EXPENSIVE.dm | 6 +- .../objects/structures/tables/materials.dm | 2 +- .../structures/tables/update_triggers.dm | 2 +- code/modules/ai/holders/ai_holder-movement.dm | 7 + .../ai/holders/ai_holder-scheduling.dm | 1 + code/modules/ai/holders/ai_holder-ticking.dm | 3 + .../ai/holders/polaris/ai_holder_targeting.dm | 2 +- .../middleware/keybindings.dm | 2 +- code/modules/keybindings/bindings_client.dm | 4 +- code/modules/metrics/api.dm | 78 --- code/modules/metrics/api/nested_numerical.dm | 23 + code/modules/metrics/api/numerical.dm | 23 + code/modules/metrics/api/spatial_series.dm | 31 + code/modules/metrics/api/string_set.dm | 17 + code/modules/metrics/api/time_series.dm | 30 + code/modules/metrics/metrics/controllers.dm | 4 + code/modules/mob/mob_helpers.dm | 2 +- code/modules/mob/new_player/login.dm | 2 +- code/modules/power/apc.dm | 2 - 102 files changed, 1343 insertions(+), 925 deletions(-) delete mode 100644 code/__DEFINES/MC.dm create mode 100644 code/__DEFINES/controllers/_master-runlevel.dm create mode 100644 code/__DEFINES/controllers/_master.dm rename code/__DEFINES/controllers/{_repositories.dm => _repository.dm} (100%) create mode 100644 code/__DEFINES/controllers/_subsystem-init.dm create mode 100644 code/__DEFINES/controllers/_subsystem-priority.dm create mode 100644 code/__DEFINES/controllers/_subsystem.dm delete mode 100644 code/__DEFINES/controllers/_subsystems.dm create mode 100644 code/__DEFINES/controllers/atoms.dm create mode 100644 code/__DEFINES/metrics.dm create mode 100644 code/controllers/subsystem/__test_bad_subsystem_sleeps.dm rename code/controllers/subsystem/{lobby.dm => titlescreen.dm} (64%) delete mode 100644 code/modules/metrics/api.dm create mode 100644 code/modules/metrics/api/nested_numerical.dm create mode 100644 code/modules/metrics/api/numerical.dm create mode 100644 code/modules/metrics/api/spatial_series.dm create mode 100644 code/modules/metrics/api/string_set.dm create mode 100644 code/modules/metrics/api/time_series.dm create mode 100644 code/modules/metrics/metrics/controllers.dm diff --git a/citadel.dme b/citadel.dme index a6da8994250c..77dc70d24882 100644 --- a/citadel.dme +++ b/citadel.dme @@ -64,7 +64,7 @@ #include "code\__DEFINES\maps.dm" #include "code\__DEFINES\math.dm" #include "code\__DEFINES\matrices.dm" -#include "code\__DEFINES\MC.dm" +#include "code\__DEFINES\metrics.dm" #include "code\__DEFINES\misc.dm" #include "code\__DEFINES\move_force.dm" #include "code\__DEFINES\movement.dm" @@ -156,9 +156,14 @@ #include "code\__DEFINES\combat\damage.dm" #include "code\__DEFINES\combat\explosions.dm" #include "code\__DEFINES\combat\shieldcall.dm" -#include "code\__DEFINES\controllers\_repositories.dm" -#include "code\__DEFINES\controllers\_subsystems.dm" +#include "code\__DEFINES\controllers\_master-runlevel.dm" +#include "code\__DEFINES\controllers\_master.dm" +#include "code\__DEFINES\controllers\_repository.dm" +#include "code\__DEFINES\controllers\_subsystem-init.dm" +#include "code\__DEFINES\controllers\_subsystem-priority.dm" +#include "code\__DEFINES\controllers\_subsystem.dm" #include "code\__DEFINES\controllers\assets.dm" +#include "code\__DEFINES\controllers\atoms.dm" #include "code\__DEFINES\controllers\dbcore.dm" #include "code\__DEFINES\controllers\grids.dm" #include "code\__DEFINES\controllers\persistence.dm" @@ -570,7 +575,6 @@ #include "code\controllers\subsystem\legacy_atc.dm" #include "code\controllers\subsystem\legacy_lore.dm" #include "code\controllers\subsystem\lighting.dm" -#include "code\controllers\subsystem\lobby.dm" #include "code\controllers\subsystem\machines.dm" #include "code\controllers\subsystem\materials.dm" #include "code\controllers\subsystem\media_tracks.dm" @@ -606,6 +610,7 @@ #include "code\controllers\subsystem\ticker.dm" #include "code\controllers\subsystem\time_track.dm" #include "code\controllers\subsystem\timer.dm" +#include "code\controllers\subsystem\titlescreen.dm" #include "code\controllers\subsystem\transcore_vr.dm" #include "code\controllers\subsystem\transfer.dm" #include "code\controllers\subsystem\turbolift.dm" @@ -3494,9 +3499,14 @@ #include "code\modules\media\media_tracks.dm" #include "code\modules\media\mediamanager.dm" #include "code\modules\media\walkpod.dm" -#include "code\modules\metrics\api.dm" #include "code\modules\metrics\metric.dm" #include "code\modules\metrics\metric_base.dm" +#include "code\modules\metrics\api\nested_numerical.dm" +#include "code\modules\metrics\api\numerical.dm" +#include "code\modules\metrics\api\spatial_series.dm" +#include "code\modules\metrics\api\string_set.dm" +#include "code\modules\metrics\api\time_series.dm" +#include "code\modules\metrics\metrics\controllers.dm" #include "code\modules\mining\mine_turfs.dm" #include "code\modules\mining\drilling\drill.dm" #include "code\modules\mining\drilling\scanner.dm" diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm deleted file mode 100644 index 0cfbf865dd76..000000000000 --- a/code/__DEFINES/MC.dm +++ /dev/null @@ -1,118 +0,0 @@ -#define MC_TICK_CHECK ( ( TICK_USAGE > Master.current_ticklimit || src.state != SS_RUNNING ) ? pause() : 0 ) -#define MC_TICK_CHECK_USAGE ( ( TICK_USAGE > Master.current_ticklimit ) ? pause() : 0 ) - -#define MC_SPLIT_TICK_INIT(phase_count) var/original_tick_limit = Master.current_ticklimit; var/split_tick_phases = ##phase_count -#define MC_SPLIT_TICK \ - if(split_tick_phases > 1){\ - Master.current_ticklimit = ((original_tick_limit - TICK_USAGE) / split_tick_phases) + TICK_USAGE;\ - --split_tick_phases;\ - } else {\ - Master.current_ticklimit = original_tick_limit;\ - } - -// Used to smooth out costs to try and avoid oscillation. -#define MC_AVERAGE_FAST(average, current) (0.7 * (average) + 0.3 * (current)) -#define MC_AVERAGE(average, current) (0.8 * (average) + 0.2 * (current)) -#define MC_AVERAGE_SLOW(average, current) (0.9 * (average) + 0.1 * (current)) - -#define MC_AVG_FAST_UP_SLOW_DOWN(average, current) (average > current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) -#define MC_AVG_SLOW_UP_FAST_DOWN(average, current) (average < current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) - -#define NEW_SS_GLOBAL(varname) if(varname != src){if(istype(varname)){PreInit(TRUE);Preload(TRUE);Recover();qdel(varname);}varname = src;} - -#define START_PROCESSING(Processor, Datum) if (!(Datum.datum_flags & DF_ISPROCESSING)) {Datum.datum_flags |= DF_ISPROCESSING;Processor.processing += Datum} -#define STOP_PROCESSING(Processor, Datum) Datum.datum_flags &= ~DF_ISPROCESSING;Processor.processing -= Datum - -//! SubSystem flags (Please design any new flags so that the default is off, to make adding flags to subsystems easier) - -/// subsystem does not initialize. -#define SS_NO_INIT (1<<0) - -/** subsystem does not fire. */ -/// (like can_fire = 0, but keeps it from getting added to the processing subsystems list) -/// (Requires a MC restart to change) -#define SS_NO_FIRE (1<<1) - -/** subsystem only runs on spare cpu (after all non-background subsystems have ran that tick) */ -/// SS_BACKGROUND has its own priority bracket -#define SS_BACKGROUND (1<<2) - -/// subsystem does not tick check, and should not run unless there is enough time (or its running behind (unless background)) -#define SS_NO_TICK_CHECK (1<<3) - -/** Treat wait as a tick count, not DS, run every wait ticks. */ -/// (also forces it to run first in the tick, above even SS_NO_TICK_CHECK subsystems) -/// (implies all runlevels because of how it works) -/// (overrides SS_BACKGROUND) -/// This is designed for basically anything that works as a mini-mc (like SStimer) -/// -/// * Ticker is its own priority bucket. The highest one. Be careful. -/// * Ticker disables tick overrun punishment. -#define SS_TICKER (1<<4) - -/** keep the subsystem's timing on point by firing early if it fired late last fire because of lag */ -/// ie: if a 20ds subsystem fires say 5 ds late due to lag or what not, its next fire would be in 15ds, not 20ds. -/// -/// * This will only keep timing past the last 10 seconds, it will not attempt to catch the subsystem up without bounds. -/// * This disables tick overrun punishment. -#define SS_KEEP_TIMING (1<<5) - -/** Calculate its next fire after its fired. */ -/// (IE: if a 5ds wait SS takes 2ds to run, its next fire should be 5ds away, not 3ds like it normally would be) -/// This flag overrides SS_KEEP_TIMING -#define SS_POST_FIRE_TIMING (1<<6) - -/// If this subsystem doesn't initialize, it should not report as a hard error in CI. -/// This should be used for subsystems that are flaky for complicated reasons, such as -/// the Lua subsystem, which relies on auxtools, which is unstable. //! We don't have the Lua system, but this is a good example. -/// It should not be used simply to silence CI. -#define SS_OK_TO_FAIL_INIT (1<<7) - -DEFINE_BITFIELD(subsystem_flags, list( - BITFIELD(SS_NO_INIT), - BITFIELD(SS_BACKGROUND), - BITFIELD(SS_NO_TICK_CHECK), - BITFIELD(SS_TICKER), - BITFIELD(SS_KEEP_TIMING), - BITFIELD(SS_POST_FIRE_TIMING), - BITFIELD(SS_OK_TO_FAIL_INIT), -)) - - -//! SUBSYSTEM STATES -/// aint doing shit. -#define SS_IDLE 0 -/// queued to run -#define SS_QUEUED 1 -/// actively running -#define SS_RUNNING 2 -/// paused by mc_tick_check -#define SS_PAUSED 3 -/// fire() slept. -#define SS_SLEEPING 4 -/// in the middle of pausing -#define SS_PAUSING 5 -#define SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/##X);\ -/datum/controller/subsystem/##X/New(){\ - NEW_SS_GLOBAL(SS##X);\ -}\ -/datum/controller/subsystem/##X - -#define PROCESSING_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/processing/##X);\ -/datum/controller/subsystem/processing/##X/New(){\ - NEW_SS_GLOBAL(SS##X);\ -}\ -/datum/controller/subsystem/processing/##X - -// Boilerplate code for multi-step processors. See machines.dm for example use. -#define INTERNAL_PROCESS_STEP(this_step, initial_step, proc_to_call, cost_var, next_step)\ -if(current_step == this_step || (initial_step && !resumed)) /* So we start at step 1 if not resumed.*/ {\ - timer = TICK_USAGE;\ - proc_to_call(resumed);\ - cost_var = MC_AVERAGE(cost_var, TICK_DELTA_TO_MS(TICK_USAGE - timer));\ - if(state != SS_RUNNING){\ - return;\ - }\ - resumed = 0;\ - current_step = next_step;\ -} diff --git a/code/__DEFINES/controllers/_master-runlevel.dm b/code/__DEFINES/controllers/_master-runlevel.dm new file mode 100644 index 000000000000..6f505ef2e8c7 --- /dev/null +++ b/code/__DEFINES/controllers/_master-runlevel.dm @@ -0,0 +1,32 @@ +//* Runlevels *// +//* Must be powers of 2. Runlevels should be in order of progression. *// +//* Only subsystem with a runlevel matching the MC's will be ticked. *// +//* The first runlevel (value '1') will be the default runlevel when the MC is first initialized. *// + +/// "Initialize Only" - Used for subsystems that should never be fired (Should also have SS_NO_FIRE set). +#define RUNLEVEL_INIT 0 +/// Initial runlevel before setup. Returns to here if setup fails. +#define RUNLEVEL_LOBBY 1 +/// While the gamemode setup is running. I.E gameticker.setup() +#define RUNLEVEL_SETUP 2 +/// After successful game ticker setup, while the round is running. +#define RUNLEVEL_GAME 4 +/// When round completes but before reboot. +#define RUNLEVEL_POSTGAME 8 + +/// default runlevels for most subsystems +#define RUNLEVELS_DEFAULT (RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME) +/// all valid runlevels - subsystems with this will run all the time after their MC init stage. +#define RUNLEVELS_ALL (RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME) + +var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_GAME, RUNLEVEL_POSTGAME) +/// Convert from the runlevel bitfield constants to index in runlevel_flags list. +#define RUNLEVEL_FLAG_TO_INDEX(flag) (log(2, flag) + 1) + +DEFINE_BITFIELD(runlevels, list( + BITFIELD(RUNLEVEL_INIT), + BITFIELD(RUNLEVEL_LOBBY), + BITFIELD(RUNLEVEL_SETUP), + BITFIELD(RUNLEVEL_GAME), + BITFIELD(RUNLEVEL_POSTGAME), +)) diff --git a/code/__DEFINES/controllers/_master.dm b/code/__DEFINES/controllers/_master.dm new file mode 100644 index 000000000000..26502007cf94 --- /dev/null +++ b/code/__DEFINES/controllers/_master.dm @@ -0,0 +1,59 @@ +/** + * This file (and its -dash files) is called MC, but actually holds quite a lot of logic including init orders + * and subsystems as the MC and subsystems make up the global orchestration system of the codebase. + */ + +#define MC_TICK_CHECK ( ( TICK_USAGE > Master.current_ticklimit || src.state != SS_RUNNING ) ? pause() : 0 ) +#define MC_TICK_CHECK_USAGE ( ( TICK_USAGE > Master.current_ticklimit ) ? pause() : 0 ) + +#define MC_SPLIT_TICK_INIT(phase_count) var/original_tick_limit = Master.current_ticklimit; var/split_tick_phases = ##phase_count +#define MC_SPLIT_TICK \ + if(split_tick_phases > 1){\ + Master.current_ticklimit = ((original_tick_limit - TICK_USAGE) / split_tick_phases) + TICK_USAGE;\ + --split_tick_phases;\ + } else {\ + Master.current_ticklimit = original_tick_limit;\ + } + +// Used to smooth out costs to try and avoid oscillation. +#define MC_AVERAGE_FAST(average, current) (0.7 * (average) + 0.3 * (current)) +#define MC_AVERAGE(average, current) (0.8 * (average) + 0.2 * (current)) +#define MC_AVERAGE_SLOW(average, current) (0.9 * (average) + 0.1 * (current)) + +#define MC_AVG_FAST_UP_SLOW_DOWN(average, current) (average > current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) +#define MC_AVG_SLOW_UP_FAST_DOWN(average, current) (average < current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) + +#define START_PROCESSING(Processor, Datum) if (!(Datum.datum_flags & DF_ISPROCESSING)) {Datum.datum_flags |= DF_ISPROCESSING;Processor.processing += Datum} +#define STOP_PROCESSING(Processor, Datum) Datum.datum_flags &= ~DF_ISPROCESSING;Processor.processing -= Datum + +/// Returns true if the MC is initialized and running. +/// Optional argument init_stage controls what stage the mc must have initializted to count as initialized. Defaults to INITSTAGE_MAX if not specified. +#define MC_RUNNING(INIT_STAGE...) (Master && Master.processing > 0 && Master.current_runlevel && Master.init_stage_completed == (max(min(INIT_STAGE_MAX, ##INIT_STAGE), 1))) +/// Returns true if the MC is at atleast a given init stage. Defaults to fully initialized. +/// +/// * This does not check anything else about the MC, including if it's actually running. +#define MC_INITIALIZED(INIT_STAGE...) (Master?.init_stage_completed >= max(INIT_STAGE_MAX, ##INIT_STAGE)) + +//* Recreate_MC() return values *// + +#define MC_RESTART_RTN_FAILED -1 +#define MC_RESTART_RTN_COOLDOWN 0 +#define MC_RESTART_RTN_SUCCESS 1 + +//* Master Controller Loop() return values *// + +/// Unknown or error +#define MC_LOOP_RTN_UNKNOWN 0 +/// New initialize stage happened +#define MC_LOOP_RTN_NEWSTAGES 1 +/// We want the MC to exit. +#define MC_LOOP_RTN_GRACEFUL_EXIT 2 + +//* Master Controller RunQueue() return values *// + +/// Unknown or error +#define MC_RUN_RTN_UNKNOWN 0 +/// Success; full completion +#define MC_RUN_RTN_FULL_COMPLETION 1 +/// Atleast one subsystem was sleeping or pausing +#define MC_RUN_RTN_PARTIAL_COMPLETION 2 diff --git a/code/__DEFINES/controllers/_repositories.dm b/code/__DEFINES/controllers/_repository.dm similarity index 100% rename from code/__DEFINES/controllers/_repositories.dm rename to code/__DEFINES/controllers/_repository.dm diff --git a/code/__DEFINES/controllers/_subsystem-init.dm b/code/__DEFINES/controllers/_subsystem-init.dm new file mode 100644 index 000000000000..12aaff4db93b --- /dev/null +++ b/code/__DEFINES/controllers/_subsystem-init.dm @@ -0,0 +1,92 @@ +//* Initialization Stages *// +//* After each stage, the MC starts ticking that stage while later stages are still waiting to init. *// +//* MC init stages must be a positive number, and init stages must all be consequetive! *// + +/// Early initializations required for server function; database, timers, tgui, etc +#define INIT_STAGE_BACKEND 1 +/// Pre-mapload initializations +#define INIT_STAGE_EARLY 2 +/// Mapload +#define INIT_STAGE_WORLD 3 +/// Late +#define INIT_STAGE_LATE 4 + +/// Last init stage we need to do. +/// +/// * This must be set to the maximum INIT_STAGE. +#define INIT_STAGE_MAX 4 + +//* Initialization Orders *// + +/** + *! Subsystem init_order, from highest priority to lowest priority. + *? Subsystems shutdown in the reverse of the order they initialize in. + *? Subsystems should always have init_order defined, even if they don't initialize, if they use init. + *? The numbers just define the ordering, they are meaningless otherwise. + */ + +//* Backend *// + +#define INIT_ORDER_FAIL2TOPIC 100 +#define INIT_ORDER_DBCORE 50 +#define INIT_ORDER_REPOSITORY 25 +#define INIT_ORDER_TIMER 10 +#define INIT_ORDER_STATPANELS 0 + +//* Early *// + +#define INIT_ORDER_EARLY_INIT 200 +#define INIT_ORDER_INPUT 170 +#define INIT_ORDER_PREFERENCES 150 +#define INIT_ORDER_JOBS 125 +#define INIT_ORDER_ASSETS 100 +#define INIT_ORDER_INSTRUMENTS 50 +#define INIT_ORDER_AI_SCHEDULING 25 +#define INIT_ORDER_AI_MOVEMENT 25 +#define INIT_ORDER_AI_HOLDERS 25 + +//* World *// + +#define INIT_ORDER_CHARACTERS 140 +#define INIT_ORDER_SOUNDS 130 +#define INIT_ORDER_GARBAGE 120 +#define INIT_ORDER_VIS 90 +#define INIT_ORDER_SERVER_MAINT 75 +#define INIT_ORDER_MEDIA_TRACKS 65 +#define INIT_ORDER_CHEMISTRY 60 +#define INIT_ORDER_MATERIALS 55 +#define INIT_ORDER_PHOTOGRAPHY 50 +#define INIT_ORDER_MAPPING 45 +#define INIT_ORDER_SPATIAL_GRIDS 43 //! must be after SSmapping so we know world.maxx and world.maxy +#define INIT_ORDER_GAME_WORLD 40 +#define INIT_ORDER_LEGACY_ATC 37 +#define INIT_ORDER_LEGACY_LORE 35 +#define INIT_ORDER_PLANTS 25 +#define INIT_ORDER_ALARMS 20 +#define INIT_ORDER_RESEARCH 17 +#define INIT_ORDER_ATOMS 15 +#define INIT_ORDER_MACHINES 10 +#define INIT_ORDER_SHUTTLES 3 +#define INIT_ORDER_DEFAULT 0 +#define INIT_ORDER_AIR -1 +#define INIT_ORDER_PLANETS -2 +#define INIT_ORDER_PERSISTENCE -3 +#define INIT_ORDER_AMBIENT_OCCLUSION -5 +#define INIT_ORDER_HOLOMAPS -5 +#define INIT_ORDER_ICON_SMOOTHING -6 +#define INIT_ORDER_EVENTS -10 +#define INIT_ORDER_OVERMAPS -20 +#define INIT_ORDER_TICKER -30 +#define INIT_ORDER_LIGHTING -40 +#define INIT_ORDER_ZMIMIC -45 +#define INIT_ORDER_AMBIENT_LIGHT -46 +#define INIT_ORDER_XENOARCH -50 +#define INIT_ORDER_CIRCUIT -60 +#define INIT_ORDER_AI -70 + +//* Late *// + +#define INIT_ORDER_OVERLAY 200 +#define INIT_ORDER_TITLESCREEN 150 +#define INIT_ORDER_NIGHTSHIFT 75 +#define INIT_ORDER_CHAT -100 //! Should be last to ensure chat remains smooth during init. diff --git a/code/__DEFINES/controllers/_subsystem-priority.dm b/code/__DEFINES/controllers/_subsystem-priority.dm new file mode 100644 index 000000000000..cd006fa47cdc --- /dev/null +++ b/code/__DEFINES/controllers/_subsystem-priority.dm @@ -0,0 +1,68 @@ + +/** + *! Subsystem fire priority, from lowest to highest priority + *? If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child) + */ + +//? Background Subsystems - Below normal +// Any ../subsystem/.. is here unless it doesn't have SS_BACKGROUND in subsystem_flags! +// This means by default, ../subsystem/processing/.. is here! + +#define FIRE_PRIORITY_RADIATION 10 //! laggy as hell, bottom barrel until optimizations are done. +#define FIRE_PRIORITY_GARBAGE 15 +#define FIRE_PRIORITY_CHARACTERS 20 +#define FIRE_PRIORITY_PARALLAX 20 +#define FIRE_PRIORITY_AIR 25 +#define FIRE_PRIORITY_ASSET_LOADING 25 +#define FIRE_PRIORITY_PLANETS 25 +#define FIRE_PRIORITY_PROCESS 50 +// DEFAULT PRIORITY IS HERE (50) + +//? Normal Subsystems - Above background, below ticker +// Any ../subsystem/.. without SS_TICKER or SS_BACKGROUND in subsystem_flags is here! + +#define FIRE_PRIORITY_PING 5 +#define FIRE_PRIORITY_SHUTTLES 5 +#define FIRE_PRIORITY_PLANTS 5 +#define FIRE_PRIORITY_NIGHTSHIFT 6 +#define FIRE_PRIORITY_VOTE 9 +#define FIRE_PRIORITY_VIS 10 +#define FIRE_PRIORITY_SERVER_MAINT 10 +#define FIRE_PRIORITY_ZMIMIC 10 +#define FIRE_PRIORITY_ALARMS 20 +#define FIRE_PRIORITY_AIRFLOW 20 +#define FIRE_PRIORITY_SPACEDRIFT 25 +#define FIRE_PRIORITY_OBJ 40 +// DEFAULT PRIORITY IS HERE (50) +#define FIRE_PRIORITY_LIGHTING 50 +#define FIRE_PRIORITY_INSTRUMENTS 50 +#define FIRE_PRIORITY_MACHINES 50 +#define FIRE_PRIORITY_AI 65 +#define FIRE_PRIORITY_AI_HOLDERS 65 +#define FIRE_PRIORITY_AI_MOVEMENT 75 +#define FIRE_PRIORITY_AI_SCHEDULING 75 +#define FIRE_PRIORITY_NANO 80 +#define FIRE_PRIORITY_TGUI 80 +#define FIRE_PRIORITY_OVERMAP_PHYSICS 90 +#define FIRE_PRIORITY_PROJECTILES 90 +#define FIRE_PRIORITY_THROWING 90 +#define FIRE_PRIORITY_STATPANELS 100 +#define FIRE_PRIORITY_OVERLAYS 100 +#define FIRE_PRIORITY_SMOOTHING 100 +#define FIRE_PRIORITY_CHAT 100 +#define FIRE_PRIORITY_INPUT 100 + +//? Ticker Subsystems - Highest priority +// Any subsystem flagged with SS_TICKER is here! +// Do not unnecessarily set your subsystem as TICKER. +// Is your feature as important as movement, chat, or timers? +// Probably not! Go to normal bracket instead! + +// DEFAULT PRIORITY IS HERE (50) +#define FIRE_PRIORITY_DPC 100 +#define FIRE_PRIORITY_TIMER 100 + +//? Special + +/// This is used as the default regardless of bucket. Check above. +#define FIRE_PRIORITY_DEFAULT 50 diff --git a/code/__DEFINES/controllers/_subsystem.dm b/code/__DEFINES/controllers/_subsystem.dm new file mode 100644 index 000000000000..f9fce73f1a02 --- /dev/null +++ b/code/__DEFINES/controllers/_subsystem.dm @@ -0,0 +1,164 @@ +/** + *! Defines for subsystems + * + *? Lots of important stuff in here, make sure you have your brain switched on when editing this file! + */ + +//* Subsystem Definition Macros *// + +/** + * Macro to fire off all logic when a subsystem is created - this is done immediately on New(). + */ +#define NEW_SS_GLOBAL(varname) if(varname != src){if(istype(varname)){PreInit(TRUE);Preload(TRUE);Recover();qdel(varname);}varname = src;} + +/** + * Defines a normal subsystem. + */ +#define SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/##X);\ +/datum/controller/subsystem/##X/New(){\ + NEW_SS_GLOBAL(SS##X);\ +}\ +/datum/controller/subsystem/##X + +/** + * Defines a processing subsystem. + */ +#define PROCESSING_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/processing/##X);\ +/datum/controller/subsystem/processing/##X/New(){\ + NEW_SS_GLOBAL(SS##X);\ +}\ +/datum/controller/subsystem/processing/##X + +//* Subsystem flags *// +//* Please design any new flags so that the default is off, to make adding flags to subsystems easier. *// + +/** + * Subsystem does not need Initialize() called. + * + * * The subsystem will still fire when its init stage is completed, unless it is + * marked with [SS_NO_FIRE] or its `can_fire` is set to FALSE. + * * The subsystem will still have its `initialized` variable set to TRUE. + */ +#define SS_NO_INIT (1<<0) + +/** + * Subsystem does not need fire() called / does not require scheduling and ticking. + * + * * This is exactly like setting `can_fire` to FALSE, but is permanent without a MC restart. + * * Use for subsystems that will never require processing, rather than one that needs it turned off and on now and then. + */ +#define SS_NO_FIRE (1<<1) + +/** + * Subsystem will try its best to only run on spare CPU, after all non-background subsystems have ran. + * + * * This pushes a subsystem into the background fire_priority bracket. + */ +#define SS_BACKGROUND (1<<2) + +/// subsystem does not tick check, and should not run unless there is enough time (or its running behind (unless background)) +// todo: this should be deprecated; please do not use this on new subsystems without good reason. +#define SS_NO_TICK_CHECK (1<<3) + +/** Treat wait as a tick count, not DS, run every wait ticks. */ +/// (also forces it to run first in the tick, above even SS_NO_TICK_CHECK subsystems) +/// (implies all runlevels because of how it works) +/// (overrides SS_BACKGROUND) +/// This is designed for basically anything that works as a mini-mc (like SStimer) +/// +/// * Ticker is its own priority bucket. The highest one. Be careful. +/// * Ticker disables tick overrun punishment. +#define SS_TICKER (1<<4) + +/** keep the subsystem's timing on point by firing early if it fired late last fire because of lag */ +/// ie: if a 20ds subsystem fires say 5 ds late due to lag or what not, its next fire would be in 15ds, not 20ds. +/// +/// * This will only keep timing past the last 10 seconds, it will not attempt to catch the subsystem up without bounds. +/// * This disables tick overrun punishment. +#define SS_KEEP_TIMING (1<<5) + +/** Calculate its next fire after its fired. */ +/// (IE: if a 5ds wait SS takes 2ds to run, its next fire should be 5ds away, not 3ds like it normally would be) +/// This flag overrides SS_KEEP_TIMING +#define SS_POST_FIRE_TIMING (1<<6) + +/// If this subsystem doesn't initialize, it should not report as a hard error in CI. +/// This should be used for subsystems that are flaky for complicated reasons, such as +/// the Lua subsystem, which relies on auxtools, which is unstable. //! We don't have the Lua system, but this is a good example. +/// It should not be used simply to silence CI. +#define SS_OK_TO_FAIL_INIT (1<<7) + +DEFINE_BITFIELD(subsystem_flags, list( + BITFIELD(SS_NO_INIT), + BITFIELD(SS_BACKGROUND), + BITFIELD(SS_NO_TICK_CHECK), + BITFIELD(SS_TICKER), + BITFIELD(SS_KEEP_TIMING), + BITFIELD(SS_POST_FIRE_TIMING), + BITFIELD(SS_OK_TO_FAIL_INIT), +)) + +//* Subsystem `Initialize()` returns *// +/** + * Negative values incidate a failure or warning of some kind, positive are good. + * 0 and 1 are unused so that TRUE and FALSE are guarenteed to be invalid values. + */ + +/// Subsystem failed to initialize entirely. Print a warning, log, and disable firing. +#define SS_INIT_FAILURE -2 +/// The default return value which must be overriden. Will succeed with a warning. +#define SS_INIT_NONE -1 +/// Subsystem initialized sucessfully. +#define SS_INIT_SUCCESS 2 +/// If your system doesn't need to be initialized (by being disabled or something) +#define SS_INIT_NO_NEED 3 +/// Succesfully initialized, BUT do not announce it to players (generally to hide game mechanics it would otherwise spoil) +#define SS_INIT_NO_MESSAGE 4 + +//* Subsystem 'state' variable *// + +/** + * Not doing anything right now. + */ +#define SS_IDLE 0 +/** + * In the MC's run-queue + */ +#define SS_QUEUED 1 +/** + * Set before the MC ignites a subsystem. This is the state while it's currently running. + */ +#define SS_RUNNING 2 +/** + * We are requesting a pause. + * + * * Set by the pause() proc if we did not sleep yet during our fire(). + */ +#define SS_PAUSED 3 +/** + * fire() is currently sleeping. + */ +#define SS_SLEEPING 4 +/** + * We slept, and now we are requesting a pause. + * + * * Set by the pause() proc if we have slept since fire() was invoked. + * * Converted to SS_PAUSED by ignite() once we finally return from fire(), as we cannot immediately pause if + * we are sleeping. + */ +#define SS_PAUSING 5 + +//* Misc *// + +/// Boilerplate code for multi-step processors. See machines.dm for example use. +#define INTERNAL_SUBSYSTEM_PROCESS_STEP(this_step, initial_step, proc_to_call, cost_var, next_step)\ +if(current_step == this_step || (initial_step && !resumed)) /* So we start at step 1 if not resumed.*/ {\ + timer = TICK_USAGE;\ + proc_to_call(resumed);\ + cost_var = MC_AVERAGE(cost_var, TICK_DELTA_TO_MS(TICK_USAGE - timer));\ + if(state != SS_RUNNING){\ + return;\ + }\ + resumed = 0;\ + current_step = next_step;\ +} diff --git a/code/__DEFINES/controllers/_subsystems.dm b/code/__DEFINES/controllers/_subsystems.dm deleted file mode 100644 index 3a5f1b5360b2..000000000000 --- a/code/__DEFINES/controllers/_subsystems.dm +++ /dev/null @@ -1,237 +0,0 @@ -/** - *! Defines for subsystems and overlays - * - *? Lots of important stuff in here, make sure you have your brain switched on when editing this file! - */ - -//* Subsystem `initialized` variable *// - -// todo: implement these, separate out SSatoms initialization state to its own variable -// #define SUBSYSTEM_INITIALIZED_NOT_STARTED 0 -// #define SUBSYSTEM_INITIALIZED_INITIALIZING 1 -// #define SUBSYSTEM_INITIALIZED_DONE 2 - -//! ## Initialization subsystem - -/// New should not call Initialize. -#define INITIALIZATION_INSSATOMS 0 -/// New should call Initialize(FALSE). -#define INITIALIZATION_INNEW_REGULAR 1 -/// New should call Initialize(TRUE). -#define INITIALIZATION_INNEW_MAPLOAD 2 - -//! ### Initialization hints - -/// Nothing happens -#define INITIALIZE_HINT_NORMAL 0 - -/** - * call LateInitialize at the end of all atom Initalization. - * - * The item will be added to the late_loaders list, this is iterated over after - * initalization of subsystems is complete and calls LateInitalize on the atom - * see [this file for the LateIntialize proc](atom.html#proc/LateInitialize) - */ -#define INITIALIZE_HINT_LATELOAD 1 - -/// Call qdel on the atom after intialization. -#define INITIALIZE_HINT_QDEL 2 - -/// type and all subtypes should always immediately call Initialize in New(). -#define INITIALIZE_IMMEDIATE(X) ##X/New(loc, ...){\ - ..();\ - if(!(atom_flags & ATOM_INITIALIZED)) {\ - var/previous_initialized_value = SSatoms.initialized;\ - SSatoms.initialized = INITIALIZATION_INNEW_MAPLOAD;\ - args[1] = TRUE;\ - SSatoms.InitAtom(src, FALSE, args);\ - SSatoms.initialized = previous_initialized_value;\ - }\ -} - -//! ### SS runlevels - -/// "Initialize Only" - Used for subsystems that should never be fired (Should also have SS_NO_FIRE set). -#define RUNLEVEL_INIT 0 -/// Initial runlevel before setup. Returns to here if setup fails. -#define RUNLEVEL_LOBBY 1 -/// While the gamemode setup is running. I.E gameticker.setup() -#define RUNLEVEL_SETUP 2 -/// After successful game ticker setup, while the round is running. -#define RUNLEVEL_GAME 4 -/// When round completes but before reboot. -#define RUNLEVEL_POSTGAME 8 - -/// default runlevels for most subsystems -#define RUNLEVELS_DEFAULT (RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME) -/// all valid runlevels - subsystems with this will run all the time after their MC init stage. -#define RUNLEVELS_ALL (RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME) - -var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_GAME, RUNLEVEL_POSTGAME) -/// Convert from the runlevel bitfield constants to index in runlevel_flags list. -#define RUNLEVEL_FLAG_TO_INDEX(flag) (log(2, flag) + 1) - -DEFINE_BITFIELD(runlevels, list( - BITFIELD(RUNLEVEL_INIT), - BITFIELD(RUNLEVEL_LOBBY), - BITFIELD(RUNLEVEL_SETUP), - BITFIELD(RUNLEVEL_GAME), - BITFIELD(RUNLEVEL_POSTGAME), -)) - -/** - *! Subsystem init_order, from highest priority to lowest priority. - *? Subsystems shutdown in the reverse of the order they initialize in. - *? The numbers just define the ordering, they are meaningless otherwise. - */ - -// todo: tg init brackets - -// core security system, used by client/New() -#define INIT_ORDER_FAIL2TOPIC 200 -// core security system, used by client/New() -#define INIT_ORDER_IPINTEL 197 - -// core timing system, used by almost everything -#define INIT_ORDER_TIMER 195 -// just about every feature on the server requires the database backend -// for storage and durability of permeance. -#define INIT_ORDER_DBCORE 190 -// repository is just struct storage. its things depend on database, -// but should depend on nothing else. -// -// for the rare occasion when a prototype requires asset registration, -// it should be able to recognize if SSassets is ready, -// and only queue an udpate if its asset is already loaded. -#define INIT_ORDER_REPOSITORY 187 -// early init initializes what is basically expensive global variables. it needs to go before assets. -#define INIT_ORDER_EARLY_INIT 185 -// assets is loaded early because things hook into this to register *their* assets -#define INIT_ORDER_ASSETS 180 - -#define INIT_ORDER_STATPANELS 170 -#define INIT_ORDER_PREFERENCES 165 -#define INIT_ORDER_INPUT 160 -#define INIT_ORDER_JOBS 150 -#define INIT_ORDER_CHARACTERS 140 -#define INIT_ORDER_SOUNDS 130 -#define INIT_ORDER_GARBAGE 120 -#define INIT_ORDER_VIS 90 -#define INIT_ORDER_SERVER_MAINT 75 -#define INIT_ORDER_INSTRUMENTS 70 -#define INIT_ORDER_MEDIA_TRACKS 65 -#define INIT_ORDER_CHEMISTRY 60 -#define INIT_ORDER_MATERIALS 55 -#define INIT_ORDER_PHOTOGRAPHY 50 -#define INIT_ORDER_AI_SCHEDULING 48 -#define INIT_ORDER_AI_MOVEMENT 48 -#define INIT_ORDER_AI_HOLDERS 48 -#define INIT_ORDER_MAPPING 45 -#define INIT_ORDER_SPATIAL_GRIDS 43 // must be after SSmapping so we know world.maxx and world.maxy -#define INIT_ORDER_GAME_WORLD 40 -#define INIT_ORDER_LEGACY_ATC 37 -#define INIT_ORDER_LEGACY_LORE 35 -#define INIT_ORDER_LOBBY 30 -#define INIT_ORDER_PLANTS 25 -#define INIT_ORDER_ALARMS 20 -#define INIT_ORDER_RESEARCH 17 -#define INIT_ORDER_ATOMS 15 -#define INIT_ORDER_MACHINES 10 -#define INIT_ORDER_SHUTTLES 3 -#define INIT_ORDER_DEFAULT 0 -#define INIT_ORDER_AIR -1 -#define INIT_ORDER_PLANETS -2 -#define INIT_ORDER_PERSISTENCE -3 -#define INIT_ORDER_AMBIENT_OCCLUSION -5 -#define INIT_ORDER_HOLOMAPS -5 -#define INIT_ORDER_NIGHTSHIFT -5 -#define INIT_ORDER_ICON_SMOOTHING -6 -#define INIT_ORDER_OVERLAY -7 -#define INIT_ORDER_EVENTS -10 -#define INIT_ORDER_OVERMAPS -20 -#define INIT_ORDER_TICKER -30 -#define INIT_ORDER_LIGHTING -40 -#define INIT_ORDER_ZMIMIC -45 -#define INIT_ORDER_AMBIENT_LIGHT -46 -#define INIT_ORDER_XENOARCH -50 -#define INIT_ORDER_CIRCUIT -60 -#define INIT_ORDER_AI -70 -#define INIT_ORDER_CHAT -100 //! Should be last to ensure chat remains smooth during init. - - -/** - *! Subsystem fire priority, from lowest to highest priority - *? If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child) - */ - -//? Background Subsystems - Below normal -// Any ../subsystem/.. is here unless it doesn't have SS_BACKGROUND in subsystem_flags! -// This means by default, ../subsystem/processing/.. is here! - -#define FIRE_PRIORITY_RADIATION 10 //! laggy as hell, bottom barrel until optimizations are done. -#define FIRE_PRIORITY_GARBAGE 15 -#define FIRE_PRIORITY_CHARACTERS 20 -#define FIRE_PRIORITY_PARALLAX 20 -#define FIRE_PRIORITY_AIR 25 -#define FIRE_PRIORITY_ASSET_LOADING 25 -#define FIRE_PRIORITY_PLANETS 25 -#define FIRE_PRIORITY_PROCESS 50 -// DEFAULT PRIORITY IS HERE (50) - -//? Normal Subsystems - Above background, below ticker -// Any ../subsystem/.. without SS_TICKER or SS_BACKGROUND in subsystem_flags is here! - -#define FIRE_PRIORITY_PING 5 -#define FIRE_PRIORITY_SHUTTLES 5 -#define FIRE_PRIORITY_PLANTS 5 -#define FIRE_PRIORITY_NIGHTSHIFT 6 -#define FIRE_PRIORITY_VOTE 9 -#define FIRE_PRIORITY_VIS 10 -#define FIRE_PRIORITY_SERVER_MAINT 10 -#define FIRE_PRIORITY_ZMIMIC 10 -#define FIRE_PRIORITY_ALARMS 20 -#define FIRE_PRIORITY_AIRFLOW 20 -#define FIRE_PRIORITY_SPACEDRIFT 25 -#define FIRE_PRIORITY_OBJ 40 -// DEFAULT PRIORITY IS HERE (50) -#define FIRE_PRIORITY_LIGHTING 50 -#define FIRE_PRIORITY_INSTRUMENTS 50 -#define FIRE_PRIORITY_MACHINES 50 -#define FIRE_PRIORITY_AI 65 -#define FIRE_PRIORITY_AI_HOLDERS 65 -#define FIRE_PRIORITY_AI_MOVEMENT 75 -#define FIRE_PRIORITY_AI_SCHEDULING 75 -#define FIRE_PRIORITY_NANO 80 -#define FIRE_PRIORITY_TGUI 80 -#define FIRE_PRIORITY_OVERMAP_PHYSICS 90 -#define FIRE_PRIORITY_PROJECTILES 90 -#define FIRE_PRIORITY_THROWING 90 -#define FIRE_PRIORITY_STATPANELS 100 -#define FIRE_PRIORITY_OVERLAYS 100 -#define FIRE_PRIORITY_SMOOTHING 100 -#define FIRE_PRIORITY_CHAT 100 -#define FIRE_PRIORITY_INPUT 100 - -//? Ticker Subsystems - Highest priority -// Any subsystem flagged with SS_TICKER is here! -// Do not unnecessarily set your subsystem as TICKER. -// Is your feature as important as movement, chat, or timers? -// Probably not! Go to normal bracket instead! - -// DEFAULT PRIORITY IS HERE (50) -#define FIRE_PRIORITY_DPC 100 -#define FIRE_PRIORITY_TIMER 100 - -//? Special - -/// This is used as the default regardless of bucket. Check above. -#define FIRE_PRIORITY_DEFAULT 50 - -/** - * Create a new timer and add it to the queue. - * Arguments: - * * callback the callback to call on timer finish - * * wait deciseconds to run the timer for - * * atom_flags atom_flags for this timer, see: code\__DEFINES\subsystems.dm - */ -#define addtimer(args...) _addtimer(args, file = __FILE__, line = __LINE__) diff --git a/code/__DEFINES/controllers/atoms.dm b/code/__DEFINES/controllers/atoms.dm new file mode 100644 index 000000000000..851ab297914c --- /dev/null +++ b/code/__DEFINES/controllers/atoms.dm @@ -0,0 +1,37 @@ +//* Values for the `atom_init_status` variable on SSatoms. *// + +/// New should not call Initialize. +#define ATOM_INIT_IN_SUBSYSTEM 0 +/// New should call Initialize(FALSE) - not mapload +#define ATOM_INIT_IN_NEW_REGULAR 1 +/// New should call Initialize(TRUE) - is in a mapload +#define ATOM_INIT_IN_NEW_MAPLOAD 2 + +//! ### Initialization hints + +/// Nothing happens +#define INITIALIZE_HINT_NORMAL 0 + +/** + * call LateInitialize at the end of all atom Initalization. + * + * The item will be added to the late_loaders list, this is iterated over after + * initalization of subsystems is complete and calls LateInitalize on the atom + * see [this file for the LateIntialize proc](atom.html#proc/LateInitialize) + */ +#define INITIALIZE_HINT_LATELOAD 1 + +/// Call qdel on the atom after intialization. +#define INITIALIZE_HINT_QDEL 2 + +/// type and all subtypes should always immediately call Initialize in New(). +#define INITIALIZE_IMMEDIATE(X) ##X/New(loc, ...){\ + ..();\ + if(!(atom_flags & ATOM_INITIALIZED)) {\ + var/previous_initialized_value = SSatoms.initialized;\ + SSatoms.initialized = ATOM_INIT_IN_NEW_MAPLOAD;\ + args[1] = TRUE;\ + SSatoms.InitAtom(src, FALSE, args);\ + SSatoms.initialized = previous_initialized_value;\ + }\ +} diff --git a/code/__DEFINES/controllers/timer.dm b/code/__DEFINES/controllers/timer.dm index 500019080619..2d6de7928b1b 100644 --- a/code/__DEFINES/controllers/timer.dm +++ b/code/__DEFINES/controllers/timer.dm @@ -1,4 +1,14 @@ //! ## Timing subsystem + +/** + * Create a new timer and add it to the queue. + * Arguments: + * * callback the callback to call on timer finish + * * wait deciseconds to run the timer for + * * atom_flags atom_flags for this timer, see: code\__DEFINES\subsystems.dm + */ +#define addtimer(args...) _addtimer(args, file = __FILE__, line = __LINE__) + /** * Don't run if there is an identical unique timer active * diff --git a/code/__DEFINES/metrics.dm b/code/__DEFINES/metrics.dm new file mode 100644 index 000000000000..4286dc004792 --- /dev/null +++ b/code/__DEFINES/metrics.dm @@ -0,0 +1,6 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +//* Metric Categories *// + +#define METRIC_CATEGORY_SERVER "server" diff --git a/code/__HELPERS/sorts/comparators.dm b/code/__HELPERS/sorts/comparators.dm index ace8dd066988..5049291233e3 100644 --- a/code/__HELPERS/sorts/comparators.dm +++ b/code/__HELPERS/sorts/comparators.dm @@ -92,25 +92,6 @@ GLOBAL_VAR_INIT(cmp_field, "name") /proc/cmp_ckey_dsc(client/a, client/b) return sorttext(a.ckey, b.ckey) -/** - * Sorts subsystems alphabetically. - */ -/proc/cmp_subsystem_display(datum/controller/subsystem/a, datum/controller/subsystem/b) - return sorttext(b.name, a.name) - -/** - * Sorts subsystems by init_order. - */ -/proc/cmp_subsystem_init(datum/controller/subsystem/a, datum/controller/subsystem/b) - // Uses initial() so it can be used on types. - return initial(b.init_order) - initial(a.init_order) - -/** - * Sorts subsystems by priority. - */ -/proc/cmp_subsystem_priority(datum/controller/subsystem/a, datum/controller/subsystem/b) - return a.priority - b.priority - /proc/cmp_filter_data_priority(list/A, list/B) return A["priority"] - B["priority"] diff --git a/code/___compile_options.dm b/code/___compile_options.dm index ed656eb5a738..456b5cabb66a 100644 --- a/code/___compile_options.dm +++ b/code/___compile_options.dm @@ -79,19 +79,11 @@ */ // #define USE_BYOND_TRACY -/** - * If this is uncommented, Autowiki will generate edits and shut down the server. - * Prefer the autowiki build target instead. - */ -// #define AUTOWIKI - - /** * If this is uncommented, will profile mapload atom initializations. */ // #define PROFILE_MAPLOAD_INIT_ATOM - /** * If this is uncommented, force our verb processing into just the 2% of a tick. * We normally reserve for it. diff --git a/code/controllers/failsafe.dm b/code/controllers/failsafe.dm index 3384590cfb3c..657bf083ffb5 100644 --- a/code/controllers/failsafe.dm +++ b/code/controllers/failsafe.dm @@ -36,6 +36,10 @@ var/datum/controller/failsafe/Failsafe /datum/controller/failsafe/New() + // Do not contaminate `usr`; if this is set, the MC main loop will have the usr of whoever called it, + // which results in all procs called by the MC inheriting that usr. + usr = null + // Highlander-style: there can only be one! Kill off the old and replace it with the new. if(Failsafe != src) if(istype(Failsafe)) diff --git a/code/controllers/master.dm b/code/controllers/master.dm index 71dc99baf347..7d0c002dad77 100644 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -27,9 +27,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new /// How many times have we ran? var/iteration = 0 - /// Are we initialized? - var/initialized = FALSE - /// world.time of last fire, for tracking lag outside of the mc. var/last_run @@ -37,8 +34,15 @@ GLOBAL_REAL(Master, /datum/controller/master) = new var/list/subsystems //# Vars for keeping track of tick drift. - var/init_timeofday - var/init_time + + /** + * The world.timeofday that the current Loop() started at. + */ + var/loop_start_timeofday + /** + * The world.time that the current Loop() started at. + */ + var/loop_start_time var/tickdrift = 0 /// How long is the MC sleeping between runs, read only (set by Loop() based off of anti-tick-contention heuristics). @@ -47,15 +51,12 @@ GLOBAL_REAL(Master, /datum/controller/master) = new /// Makes the mc main loop runtime. var/make_runtime = FALSE - var/initializations_finished_with_no_players_logged_in // I wonder what this could be? - /// The type of the last subsystem to be process()'d. var/last_type_processed /// For scheduling different subsystems for different stages of the round. var/current_runlevel - var/sleep_offline_after_initializations = TRUE var/static/restart_clear = 0 var/static/restart_timeout = 0 @@ -63,7 +64,29 @@ GLOBAL_REAL(Master, /datum/controller/master) = new var/static/random_seed - //* Processing Variables *// + //* Iniitialization *// + + /// The subsystem currently being initialized. + var/datum/controller/subsystem/current_initializing_subsystem + /// Are we initialized? This means all subsystems have been initialized. + var/initialized = FALSE + /// Set if it is specified to pause the world while no one is logged in after initializations, and we did pause. + var/initializations_finished_with_no_players_logged_in + /// Set to determine if we should sleep offline after initializations if no one is connected. + /// + /// * This is turned off by unit tests automatically. + var/sleep_offline_after_initializations = TRUE + + //* Global State *// + //* These are tracked through MC restarts. *// + + /// The current initialization stage we're at. + var/static/init_stage_completed = 0 + /// The init stage currently being ran by the main ticker loop + var/init_stage_ticking + + //* Processing Variables *// + //* These are set during a Loop(). *// /// total fire_priority of all non-background subsystems in the queue var/queue_priority_count = 0 @@ -75,6 +98,9 @@ GLOBAL_REAL(Master, /datum/controller/master) = new /// End of queue linked list (used for appending to the list). var/datum/controller/subsystem/queue_tail + //* Control Variables *// + //* These are accessed globally and are used to allow the MC to control the server's tick when outside of a MC proc. *// + /** * current tick limit, assigned before running a subsystem. * used by CHECK_TICK as well so that the procs subsystems call can obey that SS's tick limits. @@ -82,6 +108,10 @@ GLOBAL_REAL(Master, /datum/controller/master) = new var/static/current_ticklimit = TICK_LIMIT_RUNNING /datum/controller/master/New() + // Do not contaminate `usr`; if this is set, the MC main loop will have the usr of whoever called it, + // which results in all procs called by the MC inheriting that usr. + usr = null + //# 1. load configs if(!config_legacy) load_configuration() @@ -129,13 +159,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new for(var/datum/controller/subsystem/S in _subsystems) S.Preload(FALSE) - /datum/controller/master/Destroy() ..() // Tell qdel() to Del() this object. return QDEL_HINT_HARDDEL_NOW - /datum/controller/master/Shutdown() processing = FALSE tim_sort(subsystems, GLOBAL_PROC_REF(cmp_subsystem_init)) @@ -146,18 +174,18 @@ GLOBAL_REAL(Master, /datum/controller/master) = new log_world("Shutdown complete") - /** + * MC reboot proc. + * * 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() - usr = null // yeah let's not contaminate the MC call stack with our usr. - . = -1 // So if we runtime, things know we failed. + . = MC_RESTART_RTN_FAILED // So if we runtime, things know we failed. if (world.time < Master.restart_timeout) - return 0 + return MC_RESTART_RTN_COOLDOWN if (world.time < Master.restart_clear) Master.restart_count *= 0.5 @@ -168,27 +196,28 @@ GLOBAL_REAL(Master, /datum/controller/master) = new try new/datum/controller/master() catch - return -1 - - return 1 + return MC_RESTART_RTN_FAILED + return MC_RESTART_RTN_SUCCESS /datum/controller/master/Recover() var/msg = "## DEBUG: [time2text(world.timeofday)] MC restarted. Reports:\n" - for (var/varname in Master.vars) - switch (varname) - if("name", "tag", "bestF", "type", "parent_type", "vars", "statclick") // Built-in junk. - continue - - else - var/varval = Master.vars[varname] - if (istype(varval, /datum)) // Check if it has a type var. - var/datum/D = varval - msg += "\t [varname] = [D]([D.type])\n" - - else - msg += "\t [varname] = [varval]\n" - + var/list/master_attributes = Master.vars + var/list/filtered_variables = list( + NAMEOF(src, name), + NAMEOF(src, parent_type), + NAMEOF(src, statclick), + NAMEOF(src, tag), + NAMEOF(src, type), + NAMEOF(src, vars), + ) + for (var/varname in master_attributes - filtered_variables) + var/varval = master_attributes[varname] + if (isdatum(varval)) // Check if it has a type var. + var/datum/D = varval + msg += "\t [varname] = [D]([D.type])\n" + else + msg += "\t [varname] = [varval]\n" log_world(msg) var/datum/controller/subsystem/BadBoy = Master.last_type_processed @@ -200,96 +229,169 @@ GLOBAL_REAL(Master, /datum/controller/master) = new if(2) msg = "The [BadBoy.name] subsystem was the last to fire for 2 controller restarts. It will be recovered now and disabled if it happens again." FireHim = TRUE - if(3) - msg = "The [BadBoy.name] subsystem seems to be destabilizing the MC and will be offlined." + msg = "The [BadBoy.name] subsystem seems to be destabilizing the MC and will be put offline." BadBoy.subsystem_flags |= SS_NO_FIRE - if(msg) to_chat(GLOB.admins, SPAN_BOLDANNOUNCE("[msg]")) log_world(msg) if (istype(Master.subsystems)) if(FireHim) - Master.subsystems += new BadBoy.type // NEW_SS_GLOBAL will remove the old one. - + Master.subsystems += new BadBoy.type //NEW_SS_GLOBAL will remove the old one subsystems = Master.subsystems current_runlevel = Master.current_runlevel - initialized = TRUE StartProcessing(10) - else to_chat(world, SPAN_BOLDANNOUNCE("The Master Controller is having some issues, we will need to re-initialize EVERYTHING")) - Initialize(20, TRUE) - + Initialize(20, TRUE, FALSE) /** * Please don't stuff random bullshit here, * Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize() + * + * @params + * * delay - wait this many deciseconds before initializing + * * init_sss - initialize all subsystems + * * tgs_prime - notify TGS that initializations are done after */ /datum/controller/master/Initialize(delay, init_sss, tgs_prime) set waitfor = FALSE if(delay) sleep(delay) - - if(tgs_prime) - world.TgsInitializationComplete() - if(init_sss) init_subtypes(/datum/controller/subsystem, subsystems) + // Announce start to_chat(world, SPAN_BOLDANNOUNCE("Initializing subsystems...")) + var/mc_started = FALSE + + // We want to initialize subsystems by stage, in the init_order provided for subsystems within the same stage. + init_stage_completed = 0 + var/list/stage_sorted_subsystems = new(INIT_STAGE_MAX) + for(var/i in 1 to INIT_STAGE_MAX) + stage_sorted_subsystems[i] = list() // Sort subsystems by init_order, so they initialize in the correct order. tim_sort(subsystems, GLOBAL_PROC_REF(cmp_subsystem_init)) - var/start_timeofday = REALTIMEOFDAY - // Initialize subsystems. - current_ticklimit = config_legacy.tick_limit_mc_init - for (var/datum/controller/subsystem/SS in subsystems) - if (SS.subsystem_flags & SS_NO_INIT) - continue - - SS.Initialize(REALTIMEOFDAY) - CHECK_TICK + // Collect subsystems by init_stage. This has precedence over init_order. + for(var/datum/controller/subsystem/subsystem as anything in subsystems) + var/subsystem_init_stage = subsystem.init_stage + if (!isnum(subsystem_init_stage) || subsystem_init_stage < 1 || subsystem_init_stage > INIT_STAGE_MAX || round(subsystem_init_stage) != subsystem_init_stage) + stack_trace("ERROR: MC: subsystem `[subsystem.type]` has invalid init_stage: `[subsystem_init_stage]`. Setting to `[INIT_STAGE_MAX]`") + subsystem_init_stage = subsystem.init_stage = INIT_STAGE_MAX + stage_sorted_subsystems[subsystem_init_stage] += subsystem - current_ticklimit = TICK_LIMIT_RUNNING - var/time = (REALTIMEOFDAY - start_timeofday) / 10 + // Sort subsystems by display setting for easy access. + tim_sort(subsystems, GLOBAL_PROC_REF(cmp_subsystem_display)) - var/msg = "Initializations complete within [time] second[time == 1 ? "" : "s"]!" + // Initialize subsystems. The ticker loop will be started immediately upon the first stage being done. + var/rtod_start = REALTIMEOFDAY + + for(var/current_init_stage in 1 to INIT_STAGE_MAX) + for(var/datum/controller/subsystem/subsystem in stage_sorted_subsystems[current_init_stage]) + current_initializing_subsystem = subsystem + initialize_subsystem(subsystem) + current_initializing_subsystem = null + CHECK_TICK + init_stage_completed = current_init_stage + if(!mc_started) + mc_started = TRUE + if(!current_runlevel) + // intentionally not using the defines here as the MC does not care about runlevel semantics; + // the first runlevel is always used. + SetRunLevel(1) + Master.StartProcessing(0) + + var/rtod_end = REALTIMEOFDAY + var/took_seconds = round((rtod_end - rtod_start) / 10, 0.01) + + // Announce, log, and record end + var/msg = "Initializations complete within [took_seconds] second[took_seconds == 1 ? "" : "s"]!" to_chat(world, SPAN_BOLDANNOUNCE("[msg]")) log_world(msg) - if (!current_runlevel) - SetRunLevel(RUNLEVEL_LOBBY) - - // Sort subsystems by display setting for easy access. - tim_sort(subsystems, GLOBAL_PROC_REF(cmp_subsystem_display)) + // Set world options. + world.set_fps(config_legacy.fps) + // Fire initialization toast if(world.system_type == MS_WINDOWS && CONFIG_GET(flag/toast_notification_on_init) && !length(GLOB.clients)) world.shelleo("start /min powershell -ExecutionPolicy Bypass -File tools/initToast/initToast.ps1 -name \"[world.name]\" -icon %CD%\\icons\\CS13_16.png -port [world.port]") - // Set world options. - - world.set_fps(config_legacy.fps) + // Tell TGS we're initialized + if(tgs_prime) + world.TgsInitializationComplete() - var/initialized_tod = REALTIMEOFDAY + // Handle sleeping offline after initializations. + var/rtod_sleep_offline_check = REALTIMEOFDAY if(sleep_offline_after_initializations) world.sleep_offline = TRUE - - sleep(1) - + sleep(1 TICK) if(sleep_offline_after_initializations) // && CONFIG_GET(flag/resume_after_initializations)) world.sleep_offline = FALSE + initializations_finished_with_no_players_logged_in = rtod_sleep_offline_check < REALTIMEOFDAY - 10 - initializations_finished_with_no_players_logged_in = initialized_tod < REALTIMEOFDAY - 10 +/** + * Initialize a given subsystem and handle the results. + * + * Arguments: + * * subsystem - the subsystem to initialize. + */ +/datum/controller/master/proc/initialize_subsystem(datum/controller/subsystem/subsystem) + // Do not re-init already initialized subsystems if it's somehow called again. + if(subsystem.subsystem_flags & SS_NO_INIT) + subsystem.initialized = TRUE + return + if(subsystem.initialized) + return - initialized = TRUE + // todo: dylib high-precision timers + var/rtod_start = REALTIMEOFDAY + var/initialize_result = subsystem.Initialize() + var/rtod_end = REALTIMEOFDAY + var/took_seconds = round((rtod_end - rtod_start) / 10, 0.01) + + metric_set_nested_numerical(/datum/metric/nested_numerical/subsystem_init_time, "[subsystem.type]", took_seconds) + + // "[message_prefix] [seconds] seconds." + var/message_prefix + // should to_chat the message to world rather than just log + var/tell_everyone + // use a warning spans + var/chat_warning + + switch(initialize_result) + if(SS_INIT_FAILURE) + message_prefix = "Failed to initialize [subsystem.name] subsystem after" + tell_everyone = TRUE + chat_warning = TRUE + // Since this is an explicit failure, shut its ticking off. We also will not set its initialized variable. + subsystem.subsystem_flags |= SS_NO_FIRE + if(SS_INIT_NONE) + message_prefix = "Initialized [subsystem.name] subsystem with errors within" + tell_everyone = TRUE + chat_warning = TRUE + subsystem.initialized = TRUE + warning("[subsystem.name] subsystem does not implement Initialize() or it returns ..(). If the former is true, the SS_NO_INIT flag should be set for this subsystem.") + if(SS_INIT_SUCCESS) + message_prefix = "Initialized [subsystem.name] subsystem within" + tell_everyone = TRUE + subsystem.initialized = TRUE + if(SS_INIT_NO_MESSAGE) + message_prefix = "Initialized [subsystem.name] subsystem within" + subsystem.initialized = TRUE + if(SS_INIT_NO_NEED) + else + warning("[subsystem.name] subsystem initialized, returning invalid result [initialize_result]. This is a bug.") - // Loop. - Master.StartProcessing(0) + var/message = "[message_prefix] [took_seconds] second[took_seconds == 1 ? "" : "s"]." + var/chat_message = chat_warning ? SPAN_BOLDWARNING(message) : SPAN_BOLDANNOUNCE(message) + if(tell_everyone && message_prefix) + to_chat(world, chat_message) + log_world(message) /datum/controller/master/proc/SetRunLevel(new_runlevel) var/old_runlevel = current_runlevel @@ -311,11 +413,17 @@ GLOBAL_REAL(Master, /datum/controller/master) = new sleep(delay) testing("Master starting processing") - var/rtn = Loop() - if (rtn > 0 || processing < 0) - return // This was suppose to happen. + var/started_stage + var/rtn = MC_LOOP_RTN_UNKNOWN + do + started_stage = init_stage_completed + rtn = Loop(started_stage) + while (rtn == MC_LOOP_RTN_NEWSTAGES && processing > 0 && started_stage < init_stage_completed) + + if (rtn >= MC_LOOP_RTN_GRACEFUL_EXIT || processing < 0) + return //this was suppose to happen. - // Loop ended, restart the mc. + //loop ended, restart the mc log_game("MC crashed or runtimed, restarting") message_admins("MC crashed or runtimed, restarting") var/rtn2 = Recreate_MC() @@ -324,23 +432,26 @@ GLOBAL_REAL(Master, /datum/controller/master) = new message_admins("Failed to recreate MC (Error code: [rtn2]), it's up to the failsafe now") Failsafe.defcon = 2 - /** * Main loop! * This is where the magic happens. */ -/datum/controller/master/proc/Loop() +/datum/controller/master/proc/Loop(init_stage) . = -1 // Prep the loop (most of this is because we want MC restarts to reset as much state as we can, and because local vars rock // All this shit is here so that flag edits can be refreshed by restarting the MC. (and for speed) - var/list/SStickersubsystems = list() + var/list/ticker_subsystems = list() var/list/runlevel_sorted_subsystems = list(list()) // Ensure we always have at least one runlevel. var/timer = world.time for (var/thing in subsystems) var/datum/controller/subsystem/SS = thing - if (SS.subsystem_flags & SS_NO_FIRE) + // Skip non-firing + if(SS.subsystem_flags & SS_NO_FIRE) + continue + // Skip those that are after our init stage, or are not initialized. + if(SS.init_stage > init_stage) continue SS.queued_time = 0 @@ -352,7 +463,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new SS.recompute_wait_dt() if (SS.subsystem_flags & SS_TICKER) - SStickersubsystems += SS + ticker_subsystems += SS // Timer subsystems aren't allowed to bunch up, so we offset them a bit. timer += world.tick_lag * rand(0, 1) SS.next_fire = timer @@ -378,52 +489,74 @@ GLOBAL_REAL(Master, /datum/controller/master) = new * These sort by lower priorities first to reduce the number of loops needed to add subsequent SS's to the queue. * (higher subsystems will be sooner in the queue, adding them later in the loop means we don't have to loop thru them next queue add) */ - tim_sort(SStickersubsystems, GLOBAL_PROC_REF(cmp_subsystem_priority)) + tim_sort(ticker_subsystems, GLOBAL_PROC_REF(cmp_subsystem_priority)) for(var/I in runlevel_sorted_subsystems) tim_sort(I, GLOBAL_PROC_REF(cmp_subsystem_priority)) - I += SStickersubsystems + I += ticker_subsystems var/cached_runlevel = current_runlevel var/list/current_runlevel_subsystems = runlevel_sorted_subsystems[cached_runlevel] - init_timeofday = REALTIMEOFDAY - init_time = world.time + loop_start_timeofday = REALTIMEOFDAY + loop_start_time = world.time + init_stage_ticking = init_stage iteration = 1 + + var/init_stage_change_pending = FALSE var/error_level = 0 var/sleep_delta = 1 //# The actual loop. while (1) - tickdrift = max(0, MC_AVERAGE_FAST(tickdrift, (((REALTIMEOFDAY - init_timeofday) - (world.time - init_time)) / world.tick_lag))) + var/new_tickdrift = (((REALTIMEOFDAY - loop_start_timeofday) - (world.time - loop_start_time)) / world.tick_lag) + tickdrift = max(0, MC_AVERAGE_FAST(tickdrift, new_tickdrift)) var/starting_tick_usage = TICK_USAGE + + // check if we need to queue an init stage change + if(init_stage != init_stage_completed) + // set stage change pending; this'll stop new (but not paused / sleeping) subsystems from being queued to run. + init_stage_change_pending = TRUE + // ensure that 1. queue is empty and 2. no sleeping subsystems (as those don't stay in queue) exist + if(!queue_head && !laggy_sleeping_subsystem_check()) + return MC_LOOP_RTN_NEWSTAGES + + // If we're paused for some reason, well, pause. if (processing <= 0) current_ticklimit = TICK_LIMIT_RUNNING sleep(10) continue - /** - * Anti-tick-contention heuristics: - * If there are mutiple sleeping procs running before us hogging the cpu, we have to run later. - * (because sleeps are processed in the order received, longer sleeps are more likely to run first) - */ - if (starting_tick_usage > TICK_LIMIT_MC) // If there isn't enough time to bother doing anything this tick, sleep a bit. - sleep_delta *= 2 - current_ticklimit = TICK_LIMIT_RUNNING * 0.5 - sleep(world.tick_lag * (processing * sleep_delta)) - continue + // If we're fully initialized, run normal tick heuristics. Otherwise, always run every tick. + if (init_stage == INIT_STAGE_MAX) + /** + * Anti-tick-contention heuristics: + * If there are mutiple sleeping procs running before us hogging the cpu, we have to run later. + * (because sleeps are processed in the order received, longer sleeps are more likely to run first) + */ + if (starting_tick_usage > TICK_LIMIT_MC) + // If there isn't enough time to bother doing anything this tick, sleep increasingly longer times. + sleep_delta *= 2 + // Instruct CHECK_TICK to use a lot less tick than it usually wouldb e allowed to. + current_ticklimit = TICK_LIMIT_RUNNING * 0.5 + sleep(world.tick_lag * (processing * sleep_delta)) + continue - /** - * Byond resumed us late. - * Assume it might have to do the same next tick. - */ - if (last_run + CEILING(world.tick_lag * (processing * sleep_delta), world.tick_lag) < world.time) - sleep_delta += 1 + /** + * Byond resumed us late. + * Assume it might have to do the same next tick. + */ + if (last_run + CEILING(world.tick_lag * (processing * sleep_delta), world.tick_lag) < world.time) + sleep_delta += 1 - sleep_delta = MC_AVERAGE_FAST(sleep_delta, 1) // Decay sleep_delta. + // Decay sleep_delta + sleep_delta = MC_AVERAGE_FAST(sleep_delta, 1) - if (starting_tick_usage > (TICK_LIMIT_MC*0.75)) // We ran 3/4 of the way into the tick. - sleep_delta += 1 + // We ran 3/4 of the way into the tick + if (starting_tick_usage > (TICK_LIMIT_MC*0.75)) + sleep_delta += 1 + else + sleep_delta = 1 //# Debug. if (make_runtime) @@ -435,96 +568,103 @@ GLOBAL_REAL(Master, /datum/controller/master) = new //# Now do the actual stuff. - //* **Experimental**: Check every tick. + // Check if runlevel changed if(cached_runlevel != current_runlevel) - // Resechedule subsystems. - var/list/old_subsystems = current_runlevel_subsystems + // Resechedule subsystems that are not already part of the runlevel, and are running behind. + var/list/old_runlevel_subsystems = current_runlevel_subsystems cached_runlevel = current_runlevel current_runlevel_subsystems = runlevel_sorted_subsystems[cached_runlevel] - - // Now we'll go through all the subsystems we want to offset and give them a next_fire. - for(var/datum/controller/subsystem/SS as anything in current_runlevel_subsystems) - // We only want to offset it if it's new and also behind. - if(SS.next_fire > world.time || (SS in old_subsystems)) + for(var/datum/controller/subsystem/adding_to_runlevel as anything in (current_runlevel_subsystems - old_runlevel_subsystems)) + if(adding_to_runlevel.next_fire > world.time) continue + adding_to_runlevel.next_fire = world.time + world.tick_lag * rand(0, DS2TICKS(min(adding_to_runlevel.wait, 2 SECONDS))) - SS.next_fire = world.time + world.tick_lag * rand(0, DS2TICKS(min(SS.wait, 2 SECONDS))) - - //* **Experimental**: Check every queue, every tick. - if (CheckQueue(current_runlevel_subsystems) <= 0 || CheckQueue(SStickersubsystems) <= 0) - if (!SoftReset(SStickersubsystems, runlevel_sorted_subsystems)) - log_world("MC: SoftReset() failed, crashing") - return + // If no init stage change is pending, re-queue any subsystems that are idle and are ready to fire. + if(!init_stage_change_pending) + if (CheckQueue(current_runlevel_subsystems) <= 0 || CheckQueue(ticker_subsystems) <= 0) + stack_trace("MC: CheckQueue failed. Current error_level is [round(error_level, 0.25)]") + if (!SoftReset(ticker_subsystems, runlevel_sorted_subsystems)) + error_level++ + CRASH("MC: SoftReset() failed, exiting loop()") - if (!error_level) - iteration++ - - error_level++ - current_ticklimit = TICK_LIMIT_RUNNING - sleep(10) - continue - - if (queue_head) - if (RunQueue() <= 0) - if (!SoftReset(SStickersubsystems, runlevel_sorted_subsystems)) - log_world("MC: SoftReset() failed, crashing") - return - - if (!error_level) + if (error_level < 2) //except for the first strike, stop incrmenting our iteration so failsafe enters defcon iteration++ - - error_level++ + else + cached_runlevel = null //3 strikes, Lets reset the runlevel lists current_ticklimit = TICK_LIMIT_RUNNING - sleep(10) + sleep((1 SECONDS) * error_level) + error_level++ continue - error_level-- - if (!queue_head) // Reset the counts if the queue is empty, in the off chance they get out of sync. + if (queue_head) + var/run_result = RunQueue() + switch(run_result) + if(MC_RUN_RTN_UNKNOWN) + // Error running queue + stack_trace("MC: RunQueue failed. Current error_level is [round(error_level, 0.25)]") + if (error_level > 1) //skip the first error, + if (!SoftReset(ticker_subsystems, runlevel_sorted_subsystems)) + CRASH("MC: SoftReset() failed, exiting loop()") + + if (error_level <= 2) //after 3 strikes stop incrmenting our iteration so failsafe enters defcon + iteration++ + else + cached_runlevel = null //3 strikes, Lets also reset the runlevel lists + current_ticklimit = TICK_LIMIT_RUNNING + sleep((1 SECONDS) * error_level) + error_level++ + continue + error_level++ + + if (error_level > 0) + error_level = max(MC_AVERAGE_SLOW(error_level-1, error_level), 0) + if (!queue_head) //reset the counts if the queue is empty, in the off chance they get out of sync queue_priority_count = 0 queue_priority_count_bg = 0 iteration++ last_run = world.time src.sleep_delta = MC_AVERAGE_FAST(src.sleep_delta, sleep_delta) - current_ticklimit = TICK_LIMIT_RUNNING - 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. - sleep(world.tick_lag * (processing * sleep_delta)) + // We're about to go to sleep. Set the tick budget for other sleeping procs. + if (init_stage != INIT_STAGE_MAX) + // Still initializing, allow up to 100% dilation (50% of normal FPS). + current_ticklimit = TICK_LIMIT_RUNNING * 2 + else + // Already initialized; use normal heuristics. + current_ticklimit = TICK_LIMIT_RUNNING + 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 + sleep(world.tick_lag * (processing * sleep_delta)) /** - * This is what decides if something should run. + * Checks a list of subsystems and enqueues anything that is idle and is ready to run. * - * Arguments: + * @params * * subsystemstocheck - List of systems to check. */ -/datum/controller/master/proc/CheckQueue(list/subsystemstocheck) +/datum/controller/master/proc/CheckQueue(list/datum/controller/subsystem/subsystemstocheck) . = FALSE // So the mc knows if we runtimed. - // We create our variables outside of the loops to save on overhead. - var/datum/controller/subsystem/SS - var/SS_flags - - for (var/thing in subsystemstocheck) - if (!thing) - subsystemstocheck -= thing + for (var/datum/controller/subsystem/SS as anything in subsystemstocheck) + if(!SS) + subsystemstocheck -= SS - SS = thing if (SS.state != SS_IDLE) continue - if (SS.can_fire <= 0) continue - if (SS.next_fire > world.time) continue - SS_flags = SS.subsystem_flags + var/SS_flags = SS.subsystem_flags + + // if it's flagged as NO_FIRE for some reason (probably because something faulted and shut it off), completely boot it if (SS_flags & SS_NO_FIRE) subsystemstocheck -= SS continue - + // keep timing: do not run faster than 1.33x of base speed, to prevent catch-up from going too fast if ((SS_flags & (SS_TICKER|SS_KEEP_TIMING)) == SS_KEEP_TIMING && SS.last_fire + (SS.wait * 0.75) > world.time) continue @@ -532,10 +672,9 @@ GLOBAL_REAL(Master, /datum/controller/master) = new . = TRUE - /// Run thru the queue of subsystems to run, running them while balancing out their allocated tick precentage. /datum/controller/master/proc/RunQueue() - . = FALSE + . = MC_RUN_RTN_UNKNOWN var/datum/controller/subsystem/queue_node var/queue_node_flags var/queue_node_priority @@ -545,11 +684,13 @@ GLOBAL_REAL(Master, /datum/controller/master) = new var/tick_precentage var/tick_remaining var/ran = TRUE // This is right. - var/ran_non_SSticker = FALSE + var/ran_non_ticker = FALSE var/bg_calc // Have we swtiched current_tick_budget to background mode yet? // the % of tick used by the current running subsystem var/queue_node_tick_usage + // is a subsystem stopping mid-cycle? this means either pausing or sleeping + var/something_is_mid_cycle /** * Keep running while we have stuff to run and we haven't gone over a tick @@ -573,7 +714,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new * (unless we haven't even ran anything this tick, since its unlikely they will ever be able run in those cases, so we just let them run) */ if (queue_node_flags & SS_NO_TICK_CHECK) - if (queue_node.tick_usage > TICK_LIMIT_RUNNING - TICK_USAGE && ran_non_SSticker) + if (queue_node.tick_usage > TICK_LIMIT_RUNNING - TICK_USAGE && ran_non_ticker) queue_node.queued_priority += queue_priority_count * 0.1 queue_priority_count -= queue_node_priority queue_priority_count += queue_node.queued_priority @@ -599,47 +740,58 @@ GLOBAL_REAL(Master, /datum/controller/master) = new current_ticklimit = round(TICK_USAGE + tick_precentage) if (!(queue_node_flags & SS_TICKER)) - ran_non_SSticker = TRUE + ran_non_ticker = TRUE ran = TRUE - queue_node_paused = (queue_node.state == SS_PAUSED || queue_node.state == SS_PAUSING) + queue_node_paused = (queue_node.state == SS_PAUSED) last_type_processed = queue_node queue_node.state = SS_RUNNING - // ignite / fire the head node + // ignite() will return immediately even if fire() sleeps. queue_node_tick_usage = TICK_USAGE var/state = queue_node.ignite(queue_node_paused) queue_node_tick_usage = TICK_USAGE - queue_node_tick_usage - if (state == SS_RUNNING) - state = SS_IDLE + switch(state) + if(SS_RUNNING) + // fire() ran to completion + state = SS_IDLE + if(SS_PAUSED) + // fire() ran and then pause()'d + something_is_mid_cycle = TRUE + if(SS_SLEEPING) + // fire() slept; the subsystem may or may not pause later + something_is_mid_cycle = TRUE + else + stack_trace("subsystem had unexpected state: [state]") + state = SS_IDLE current_tick_budget -= queue_node_priority - if (queue_node_tick_usage < 0) queue_node_tick_usage = 0 queue_node.tick_overrun = max(0, MC_AVG_FAST_UP_SLOW_DOWN(queue_node.tick_overrun, queue_node_tick_usage-tick_precentage)) queue_node.state = state - // if it paused mid-run, track that + // if it paused mid-run, track that ; do not eject it from the queue if (state == SS_PAUSED) queue_node.paused_ticks++ queue_node.paused_tick_usage += queue_node_tick_usage queue_node = queue_node.queue_next continue - // it did not pause; this is a complete run - + // it did not pause. either this is a complete run, or the subsystem is sleeping. + // in either case, we will track what we can and eject it; if it's sleeping, we can no longer manage the fire() call. queue_node.ticks = MC_AVERAGE(queue_node.ticks, queue_node.paused_ticks) - queue_node_tick_usage += queue_node.paused_tick_usage + queue_node_tick_usage += queue_node.paused_tick_usage queue_node.tick_usage = MC_AVERAGE_FAST(queue_node.tick_usage, queue_node_tick_usage) queue_node.cost = MC_AVERAGE_FAST(queue_node.cost, TICK_DELTA_TO_MS(queue_node_tick_usage)) + queue_node.paused_ticks = 0 queue_node.paused_tick_usage = 0 @@ -651,29 +803,14 @@ GLOBAL_REAL(Master, /datum/controller/master) = new queue_node.last_fire = world.time queue_node.times_fired++ - // schedule next run - if (queue_node_flags & SS_TICKER) - // ticker: run this many ticks after always - queue_node.next_fire = world.time + (world.tick_lag * queue_node.wait) - else if (queue_node_flags & SS_POST_FIRE_TIMING) - // post fire timing: fire this much wait after current time, with tick overrun punishment - queue_node.next_fire = world.time + queue_node.wait + (world.tick_lag * (queue_node.tick_overrun/100)) - else if (queue_node_flags & SS_KEEP_TIMING) - // keep timing: fire this much wait after *the last time we should have fired*, without tick overrun punishment - // **experimental**: do not keep timing past last 10 seconds, if something is running behind that much don't permanently accelerate it. - queue_node.next_fire = max(world.time - 10 SECONDS, queue_node.next_fire + queue_node.wait) - else - // normal: fire this much wait after when we were queued, with tick overrun punishment - queue_node.next_fire = queue_node.queued_time + queue_node.wait + (world.tick_lag * (queue_node.tick_overrun/100)) - - queue_node.queued_time = 0 - - // Remove from queue. + // update the next time it should be available to queue + queue_node.update_next_fire() + // remove from queue queue_node.dequeue() // move to next queue_node = queue_node.queue_next - . = TRUE + . = something_is_mid_cycle ? MC_RUN_RTN_PARTIAL_COMPLETION : MC_RUN_RTN_FULL_COMPLETION /** * Resets the queue, and all subsystems, while filtering out the subsystem lists called if any mc's queue procs runtime or exit improperly. @@ -729,11 +866,9 @@ GLOBAL_REAL(Master, /datum/controller/master) = new log_world("MC: SoftReset: Finished.") . = TRUE - /datum/controller/master/stat_entry() return "(TickRate:[Master.processing]) (Iteration:[Master.iteration])" - /datum/controller/master/StartLoadingMap() // todo: this is kind of awful because this procs every subsystem unnecessarily // you might say this is microoptimizations but this is called a seriously high number of times during a load. @@ -748,19 +883,16 @@ GLOBAL_REAL(Master, /datum/controller/master) = new var/datum/controller/subsystem/SS = S SS.StopLoadingMap() -/* -/datum/controller/master/proc/UpdateTickRate() - if (!processing) - return - var/client_count = length(GLOB.clients) - if (client_count < CONFIG_GET(number/mc_tick_rate/disable_high_pop_mc_mode_amount)) - processing = CONFIG_GET(number/mc_tick_rate/base_mc_tick_rate) - else if (client_count > CONFIG_GET(number/mc_tick_rate/high_pop_mc_mode_amount)) - processing = CONFIG_GET(number/mc_tick_rate/high_pop_mc_tick_rate) -*/ - - /datum/controller/master/proc/OnConfigLoad() for (var/thing in subsystems) var/datum/controller/subsystem/SS = thing SS.OnConfigLoad() + +/** + * CitRP snowflake special: Check if any subsystems are sleeping. + */ +/datum/controller/master/proc/laggy_sleeping_subsystem_check() + for(var/datum/controller/subsystem/ss in subsystems) + if(ss.state == SS_SLEEPING) + return TRUE + return FALSE diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm index b12deb3adb41..bf1333d28b4e 100644 --- a/code/controllers/subsystem.dm +++ b/code/controllers/subsystem.dm @@ -1,3 +1,26 @@ +/** + * Sorts subsystems for display (alphabetically). + */ +/proc/cmp_subsystem_display(datum/controller/subsystem/a, datum/controller/subsystem/b) + return sorttext(b.name, a.name) + +/** + * Sorts subsystems by init_order and init_stage. + */ +/proc/cmp_subsystem_init(datum/controller/subsystem/a, datum/controller/subsystem/b) + // Uses initial() so it can be used on types. + if(a.init_stage != b.init_stage) + return initial(a.init_stage) - initial(b.init_stage) + return initial(b.init_order) - initial(a.init_order) + +/** + * Sorts subsystems by priority, from lowest to highest. + * + * * This does not take into account SS_BACKGROUND and SS_TICKER flags! + */ +/proc/cmp_subsystem_priority(datum/controller/subsystem/a, datum/controller/subsystem/b) + return a.priority - b.priority + /** * # Subsystem base class * @@ -5,6 +28,15 @@ * * Simply define a child of this subsystem, using the [SUBSYSTEM_DEF] macro, and the MC will handle registration. * Changing the name is required. + * + * ## Sleeping + * + * If a subsystem sleeps during a tick, it is very, very bad. + * + * * Sleeping orphans the subsystem's call stack from the MC's. The MC is no longer able to control the subsystem's tick usage. + * * Sleeping is handled, but not perfect. This means the MC won't crash / do anything nasty, but normal timing will nonetheless + * be affected. + * * Sleeping causes things like paused tick tracking to be inaccurate. */ /datum/controller/subsystem //# Metadata; you should define these. @@ -15,12 +47,30 @@ */ name = "fire coderbus" + //* Initialization & Shutdown *// + /** * Order of initialization. * Higher numbers are initialized first, lower numbers later. * Use or create defines such as [INIT_ORDER_DEFAULT] so we can see the order in one file. + * + * * This is secondary to [init_stage]. */ var/init_order = INIT_ORDER_DEFAULT + /** + * Which stage does this subsystem init at. Earlier stages can fire while later stages init. + * + * * This is higher in precedence than [init_order]. + * * This determines when the subsystem starts firing; besure to set this if you need ticking even if you are using SS_NO_INIT! + */ + var/init_stage = INIT_STAGE_WORLD + /** + * This variable is set to TRUE after the subsystem has been initialized. + * + * * If this subsystem is marked as SS_NO_FIRE, this still will be set to TRUE. We just won't call Initialize(). + * * This will remain FALSE if initialization is an explicit failure. + */ + var/initialized = FALSE /** * Time to wait (in deciseconds) between each call to fire(). @@ -44,16 +94,6 @@ */ var/subsystem_flags = NONE - /** - * Which stage does this subsystem init at. - * Earlier stages can fire while later stages init. - */ - //var/init_stage = INITSTAGE_MAIN - - /// This var is set to TRUE after the subsystem has been initialized. - // todo: see __DEFINES/controllers/_subsystems.dm; this shouldn't just be TRUE / FALSE - var/initialized = FALSE - /** * Set to FALSE to prevent fire() calls, mostly for admin use or subsystems that may be resumed later. * use the [SS_NO_FIRE] flag instead for systems that never fire to keep it from even being added to list that is checked every tick. @@ -68,16 +108,20 @@ //# The following variables are managed by the MC and should not be modified directly. - /// Last world.time we did a full ignite()/fire() without pausing + /// Last time ignite() was called and fire() ran to completion. /// /// * this is set by the MC's processing loop - /// * this is a heuristic; subsystems that have weird pausing behaviors won't work right with this. - /// * this is why it's crucial subsystems call pause() if they didn't finish a run! + /// * sleeping will count as a fire(), despite potentially not finishing a cycle. var/last_fire = 0 /// Scheduled world.time for next ignite(). /// /// * this is set by the MC's processing loop var/next_fire = 0 + /// Tracks the number of times fire() was ran to completion after an ignite(). + /// + /// * this is set by the MC's processing loop + /// * sleeping will count as a time fired, despite potentially not finishing a cycle. + var/times_fired = 0 /// Running average of the amount of milliseconds it takes the subsystem to complete a run (including all resumes but not the time spent paused). var/cost = 0 @@ -100,10 +144,10 @@ /// Tracks how many fires the subsystem takes to complete a run on average. var/ticks = 1 - /// Tracks the amount of completed runs for the subsystem. - var/times_fired = 0 - /// Time the subsystem entered the queue, (for timing and priority reasons). + /// + /// * This doesn't take into account pauses and sleeps. queued_time is the time it was initially put into queue + /// for a full firing cycle. var/queued_time = 0 /** @@ -140,21 +184,20 @@ /// /// * this is pretty much time dilation for this subsystem /// * this is based on wait time; e.g. 100% means we're running twice as slow, etc - var/tick_dilation_avg = 0 - /// How much of a tick (in percents of a tick) were we allocated last fire. - var/tick_allocation_last = 0 - /// How much of a tick (in percents of a tick) do we get allocated by the mc on avg. - var/tick_allocation_avg = 0 + /// * this is also reset by update_next_fire() if 'reset timing' arg is specified + var/tracked_average_dilation = 0 + /// Last world.time we did a full ignite()/fire() without pausing + /// + /// * this is set when fire() finishes, whether normally or by sleeping, without pausing. + /// * this is set by ignite() + var/tracked_last_completion = 0 + /** * # Do not blindly add vars here to the bottom, put it where it goes above. * # If your var only has two values, put it in as a flag. */ -// Do not override -// /datum/controller/subsystem/New() -// return - /** * Called before global vars are initialized * Called before Recover() @@ -182,34 +225,60 @@ return /** - * This is used so the mc knows when the subsystem sleeps. - * DO NOT OVERRIDE THIS. + * Used to initialize the subsystem AFTER the map has loaded. + * This is expected to be overriden by subtypes. + */ +/datum/controller/subsystem/Initialize() + return SS_INIT_NONE + +/** + * Usually called via datum/controller/subsystem/New() when replacing a subsystem (i.e. due to a recurring crash). + * Should attempt to salvage what it can from the old instance of subsystem. + */ +/datum/controller/subsystem/Recover() + return TRUE + +/** + * Handles logic used to track fire() and sleeps. + * + * * If fire() sleeps, the return value will be SS_SLEEPING. + * * If fire() does not sleep, the return value will be SS_PAUSED or SS_RUNNING. + * + * @return the state we're now in. This return value is only used if fire() does not sleep. */ /datum/controller/subsystem/proc/ignite(resumed = FALSE) SHOULD_NOT_OVERRIDE(TRUE) + // This makes us return the last return value when we (or anything we call; e.g. fire()) sleeps. set waitfor = FALSE + // Paranoid set. . = SS_IDLE - - tick_allocation_last = Master.current_ticklimit-(TICK_USAGE) - tick_allocation_avg = MC_AVERAGE(tick_allocation_avg, tick_allocation_last) - + // Set to SLEEPING so the MC knows if anything below this sleeps. . = SS_SLEEPING + // Fire. This can potentially sleep. If it does, the rest of the proc will be disregarded by the MC. fire(resumed) + // If fire() does not sleep, this will set our return value to RUNNING or PAUSED, depending on if we hit pause(). + // If fire() does sleep, 'state' will have already been overwritten by the MC to be SLEEPING, + // and if pause() is hit after the sleep, it will be changed to PAUSING. . = state - if (state == SS_SLEEPING) - state = SS_IDLE - if (state == SS_PAUSING) - var/QT = queued_time - enqueue() - state = SS_PAUSED - queued_time = QT - else - // track time between runs - var/full_run_took = world.time - last_fire - var/new_tick_dilation = (full_run_took / nominal_dt_ds) * 100 - 100 - tick_dilation_avg = max(0, MC_AVERAGE_SLOW(tick_dilation_avg, new_tick_dilation)) - last_fire = world.time + switch(state) + if(SS_PAUSING) + // sleeping & did pause; MC already moved on, and we've been ejected from queue. Re-insert into queue. + var/was_queued_at = queued_time + enqueue() + state = SS_PAUSED + queued_time = was_queued_at + if(SS_RUNNING, SS_SLEEPING) + // full run finished ; track tick dilation average, last fire, and prepare to re-insert into queue. + var/full_run_took = world.time - tracked_last_completion + var/new_tick_dilation = (full_run_took / nominal_dt_ds) * 100 - 100 + tracked_average_dilation = max(0, MC_AVERAGE_SLOW(tracked_average_dilation, new_tick_dilation)) + tracked_last_completion = world.time + state = SS_IDLE + if(SS_PAUSED) + // we paused; nothing special, move on. the MC will handle it. + else + CRASH("unexpected state in [src] ([type]): [state]") /** * previously, this would have been named 'process()' but that name is used everywhere for different things! @@ -222,13 +291,40 @@ /datum/controller/subsystem/Destroy() dequeue() - can_fire = 0 + can_fire = FALSE subsystem_flags |= SS_NO_FIRE if (Master) Master.subsystems -= src - return ..() +/** + * Updates `next_fire` for the next run. + * + * @params + * * reset_time - Entirely reset the subsystem's stateful time tracking including tick-overrun, post fire timing, etc. + */ +/datum/controller/subsystem/proc/update_next_fire(reset_time) + if(reset_time) + next_fire = (subsystem_flags & SS_TICKER) ? (world.time + (world.tick_lag * wait)) : (world.time + wait) + tracked_last_completion = world.time + return + + var/queue_node_flags = subsystem_flags + + if (queue_node_flags & SS_TICKER) + // ticker: run this many ticks after always + next_fire = world.time + (world.tick_lag * wait) + else if (queue_node_flags & SS_POST_FIRE_TIMING) + // post fire timing: fire this much wait after current time, with tick overrun punishment + next_fire = world.time + wait + (world.tick_lag * (tick_overrun / 100)) + else if (queue_node_flags & SS_KEEP_TIMING) + // keep timing: fire this much wait after *the last time we should have fired*, without tick overrun punishment + // **experimental**: do not keep timing past last 10 seconds, if something is running behind that much don't permanently accelerate it. + next_fire = max(world.time - 10 SECONDS, next_fire + wait) + else + // normal: fire this much wait after when we were queued, with tick overrun punishment + next_fire = queued_time + wait + (world.tick_lag * (tick_overrun / 100)) + /** * Queue it to run. * (we loop thru a linked list until we get to the end or find the right point) @@ -321,7 +417,6 @@ switch(state) if(SS_RUNNING) state = SS_PAUSED - if(SS_SLEEPING) state = SS_PAUSING @@ -329,43 +424,50 @@ /datum/controller/subsystem/proc/OnConfigLoad() return -/** - * Used to initialize the subsystem AFTER the map has loaded. - * This is expected to be overriden by subtypes. - */ -/datum/controller/subsystem/Initialize(start_timeofday) - initialized = TRUE - var/time = (REALTIMEOFDAY - start_timeofday) / 10 - var/msg = "Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!" - to_chat(world, SPAN_BOLDANNOUNCE("[msg]")) - log_world(msg) - log_subsystem("INIT", msg) - return time - /** * Hook for printing stats to the "MC" statuspanel for admins to see performance and related stats etc. */ /datum/controller/subsystem/stat_entry() if(can_fire && !(SS_NO_FIRE & subsystem_flags)) - . = "[round(cost,1)]ms | D:[round(tick_dilation_avg,1)]% | U:[round(tick_usage,1)]% | O:[round(tick_overrun,1)]% | T:[round(ticks,0.1)] " + . = "[round(cost,1)]ms | D:[round(tracked_average_dilation,1)]% | U:[round(tick_usage,1)]% | O:[round(tick_overrun,1)]% | T:[round(ticks,0.1)] " else . = "OFFLINE " /datum/controller/subsystem/stat_key() - return can_fire? "\[[state_letter()]\][name]" : name + return "\[[state_letter()]\] [name]" +/** + * Returns our status symbol. + */ /datum/controller/subsystem/proc/state_letter() - switch (state) - if (SS_RUNNING) - . = "R" - if (SS_QUEUED) - . = "Q" - if (SS_PAUSED, SS_PAUSING) - . = "P" - if (SS_SLEEPING) - . = "S" - if (SS_IDLE) - . = " " + // R: running + // Q: queued + // P: pausing / paused + // S: sleeping + // I: initializing + // D: done initializing, waiting for init stage to finish + // blank: idle + if(Master.init_stage_completed >= init_stage) + switch (state) + if (SS_RUNNING) + . = "R" + if (SS_QUEUED) + . = "Q" + if (SS_PAUSED, SS_PAUSING) + . = "P" + if (SS_SLEEPING) + . = "S" + if (SS_IDLE) + . = " " + else + if(subsystem_flags & SS_NO_INIT) + . = "D" + if(src == Master.current_initializing_subsystem) + . = "I" + else if(initialized) + . = "D" + else + . = "W" /** * Could be used to postpone a costly subsystem for (default one) var/cycles, cycles. @@ -375,14 +477,6 @@ if(next_fire - world.time < wait) next_fire += (wait*cycles) -/** - * Usually called via datum/controller/subsystem/New() when replacing a subsystem (i.e. due to a recurring crash). - * Should attempt to salvage what it can from the old instance of subsystem. - */ -/datum/controller/subsystem/Recover() - return TRUE - - /datum/controller/subsystem/vv_edit_var(var_name, var_value) switch (var_name) if (NAMEOF(src, can_fire)) diff --git a/code/controllers/subsystem/__test_bad_subsystem_sleeps.dm b/code/controllers/subsystem/__test_bad_subsystem_sleeps.dm new file mode 100644 index 000000000000..0b9f7ddd8833 --- /dev/null +++ b/code/controllers/subsystem/__test_bad_subsystem_sleeps.dm @@ -0,0 +1,44 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +/** + * Used to intentionally fuck up the MC fire() by sleeping ridiculous + * amounts of time. + * + * Used to test sleep handling and stage change behavior. + * + * **This file should not be ticked outside of special development testing.** + */ +#warn Bad subsystem sleep tester is ticked. + +SUBSYSTEM_DEF(__test_bad_subsystem_sleeps) + name = "-- TEST BAD SUBSYSTEM SLEEPS --" + init_stage = INIT_STAGE_BACKEND + init_order = 100 // this doesn't matter tbh; what matters is it runs as soon as possible so the test goes faster + subsystem_flags = SS_NO_INIT + runlevels = RUNLEVELS_ALL + + var/is_currently_sleeping = FALSE + var/should_resume = FALSE + var/first_fire = TRUE + var/mc_init_stage_at_start_of_sleep + +/datum/controller/subsystem/__test_bad_subsystem_sleeps/fire(resumed) + if(first_fire) + is_currently_sleeping = TRUE + mc_init_stage_at_start_of_sleep = Master.init_stage_ticking + sleep(10 SECONDS) + is_currently_sleeping = FALSE + if(mc_init_stage_at_start_of_sleep != Master.init_stage_ticking) + CRASH("master controller moved on when we were trying to block init") + should_resume = TRUE + pause() + return + if(is_currently_sleeping) + CRASH("re-fired while sleeping") + if(!resumed && should_resume) + CRASH("wasn't resumed when we should resume") + if(resumed) + should_resume = FALSE + if(mc_init_stage_at_start_of_sleep != Master.init_stage_ticking) + CRASH("master controller moved on when we were trying to block init") diff --git a/code/controllers/subsystem/ai_holders.dm b/code/controllers/subsystem/ai_holders.dm index 2ecfc9b96c02..a4b304c37575 100644 --- a/code/controllers/subsystem/ai_holders.dm +++ b/code/controllers/subsystem/ai_holders.dm @@ -14,6 +14,7 @@ SUBSYSTEM_DEF(ai_holders) subsystem_flags = NONE priority = FIRE_PRIORITY_AI_HOLDERS init_order = INIT_ORDER_AI_HOLDERS + init_stage = INIT_STAGE_EARLY wait = 0 /// all ticking ai holders @@ -37,7 +38,7 @@ SUBSYSTEM_DEF(ai_holders) /datum/controller/subsystem/ai_holders/Initialize() active_holders = list() rebuild() - return ..() + return SS_INIT_SUCCESS /datum/controller/subsystem/ai_holders/on_ticklag_changed(old_ticklag, new_ticklag) rebuild() diff --git a/code/controllers/subsystem/ai_movement.dm b/code/controllers/subsystem/ai_movement.dm index 619744ad82fb..0c228e285c4e 100644 --- a/code/controllers/subsystem/ai_movement.dm +++ b/code/controllers/subsystem/ai_movement.dm @@ -18,6 +18,7 @@ SUBSYSTEM_DEF(ai_movement) subsystem_flags = NONE priority = FIRE_PRIORITY_AI_MOVEMENT init_order = INIT_ORDER_AI_MOVEMENT + init_stage = INIT_STAGE_EARLY wait = 0 /// ais that are moving using a movement handler right now @@ -45,7 +46,7 @@ SUBSYSTEM_DEF(ai_movement) moving_ais = list() rebuild() init_ai_pathfinders() - return ..() + return SS_INIT_SUCCESS /datum/controller/subsystem/ai_movement/on_ticklag_changed(old_ticklag, new_ticklag) rebuild() @@ -86,8 +87,10 @@ SUBSYSTEM_DEF(ai_movement) if(reschedule_delay) // eject; we don't change being_processed.ticking_(next|previous) if(being_processed.movement_bucket_next == being_processed) + // this was the only holder in the bucket buckets[bucket_offset] = null else + // this was not the only holder in the bucket, stitch it back together after the ejection. buckets[bucket_offset] = being_processed.movement_bucket_next being_processed.movement_bucket_next.movement_bucket_prev = being_processed.movement_bucket_prev being_processed.movement_bucket_prev.movement_bucket_next = being_processed.movement_bucket_next @@ -105,7 +108,7 @@ SUBSYSTEM_DEF(ai_movement) being_processed.movement_bucket_next = being_processed.movement_bucket_prev = being_processed being_processed.movement_bucket_position = inject_offset else - // get out + // get out if not rescheduling unregister_moving(being_processed) if(MC_TICK_CHECK) break @@ -136,7 +139,7 @@ SUBSYSTEM_DEF(ai_movement) moving_ais -= holder stack_trace("bad holder found") continue - // doubly linked list inject + // circular double-linked list inject if(!isnull(buckets[position])) var/datum/ai_holder/existing = buckets[position] holder.movement_bucket_next = existing.movement_bucket_next @@ -156,7 +159,7 @@ SUBSYSTEM_DEF(ai_movement) buckets.len = BUCKET_AMOUNT for(var/i in 1 to length(moving_ais)) var/datum/ai_holder/holder = buckets[i] - holder.movement_bucket_next = holder.movement_bucket_prev = null + holder.movement_bucket_next = holder.movement_bucket_prev = holder holder.movement_bucket_position = i /** diff --git a/code/controllers/subsystem/ai_scheduling.dm b/code/controllers/subsystem/ai_scheduling.dm index 0919a8e4c08a..13d7042b2d9b 100644 --- a/code/controllers/subsystem/ai_scheduling.dm +++ b/code/controllers/subsystem/ai_scheduling.dm @@ -14,6 +14,7 @@ SUBSYSTEM_DEF(ai_scheduling) subsystem_flags = NONE priority = FIRE_PRIORITY_AI_SCHEDULING init_order = INIT_ORDER_AI_SCHEDULING + init_stage = INIT_STAGE_EARLY wait = 0 /// rolling bucket list; these hold the head node of linked ai_holders. @@ -34,7 +35,7 @@ SUBSYSTEM_DEF(ai_scheduling) /datum/controller/subsystem/ai_scheduling/Initialize() rebuild() - return ..() + return SS_INIT_SUCCESS /datum/controller/subsystem/ai_scheduling/on_ticklag_changed(old_ticklag, new_ticklag) rebuild() @@ -116,6 +117,10 @@ SUBSYSTEM_DEF(ai_scheduling) * List of things allowed to use this: * * /datum/ai_holder * * /datum/ai_network + * + * Quirks: + * * This will never sleep on invocation. If the called proc sleeps, we blow right past. + * * Try to not have the called proc be ridiculously expensive as we are on a very fast-firing subsystem. */ /datum/ai_callback var/proc_ref @@ -143,4 +148,5 @@ SUBSYSTEM_DEF(ai_scheduling) /datum/ai_callback/proc/invoke() SHOULD_NOT_SLEEP(TRUE) + set waitfor = FALSE call(parent, proc_ref)(arglist(arguments)) diff --git a/code/controllers/subsystem/air.dm b/code/controllers/subsystem/air.dm index a7b59ec711b0..4d5637252898 100644 --- a/code/controllers/subsystem/air.dm +++ b/code/controllers/subsystem/air.dm @@ -89,7 +89,7 @@ SUBSYSTEM_DEF(air) continue generated_atmospheres[id] = SSair.generated_atmospheres[id] -/datum/controller/subsystem/air/Initialize(timeofday) +/datum/controller/subsystem/air/Initialize() #ifndef FASTBOOT_DISABLE_ZONES report_progress("Initializing [name] subsystem...") @@ -114,9 +114,7 @@ SUBSYSTEM_DEF(air) startup_active_edge_log = edge_log.Copy() //! Fancy blockquote of data. - var/time = (REALTIMEOFDAY - timeofday) / 10 var/list/blockquote_data = list( - SPAN_BOLDANNOUNCE("Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!