mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-07-22 18:52:41 +01:00
4ab234f10c
tl;dr level collections that can easily be logically restructured/moved as necessary will eventually be used for radio and overmaps location resolution. this is the last part of https://github.com/Citadel-Station-13/Citadel-Station-13-RP/pull/4841 that has not yet been integrated. this will be a draft for a while. --------- Co-authored-by: LetterN <24603524+LetterN@users.noreply.github.com>
532 lines
17 KiB
Plaintext
532 lines
17 KiB
Plaintext
/**
|
||
* 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
|
||
*
|
||
* Defines a subsystem to be managed by the [Master Controller][/datum/controller/master]
|
||
*
|
||
* 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.
|
||
|
||
/**
|
||
* Name of the subsystem
|
||
* YOU MUST CHANGE THIS
|
||
*/
|
||
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().
|
||
* Must be a positive integer.
|
||
*/
|
||
var/wait = 20
|
||
|
||
/**
|
||
* Priority Weight:
|
||
*
|
||
* When mutiple subsystems need to run in the same tick,
|
||
* higher priority subsystems will be given a higher share of the tick before MC_TICK_CHECK triggers a sleep,
|
||
* higher priority subsystems also run before lower priority subsystems.
|
||
*/
|
||
var/priority = FIRE_PRIORITY_DEFAULT
|
||
|
||
/**
|
||
* [Subsystem Flags][SS_NO_INIT] to control binary behavior.
|
||
* Flags must be set at compile time or before preinit finishes to take full effect.
|
||
* (You can also restart the mc to force them to process again)
|
||
*/
|
||
var/subsystem_flags = NONE
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
var/can_fire = TRUE
|
||
|
||
/**
|
||
* Bitmap of what game states can this subsystem fire at.
|
||
* See [RUNLEVELS_DEFAULT] for more details.
|
||
*/
|
||
var/runlevels = RUNLEVELS_DEFAULT
|
||
|
||
//# The following variables are managed by the MC and should not be modified directly.
|
||
|
||
/// Last time ignite() was called and fire() ran to completion.
|
||
///
|
||
/// * this is set by the MC's processing loop
|
||
/// * 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
|
||
|
||
/// Running average of the amount of tick usage in percents of a tick it takes the subsystem to complete a run.
|
||
var/tick_usage = 0
|
||
|
||
/// Running average of the amount of tick usage (in percents of a game tick) the subsystem has spent past its allocated time without pausing.
|
||
var/tick_overrun = 0
|
||
|
||
/// Tracks the current execution state of the subsystem. Used to handle subsystems that sleep in fire so the mc doesn't run them again while they are sleeping.
|
||
var/state = SS_IDLE
|
||
|
||
/// Tracks how many fires the subsystem has consecutively paused on in the current run.
|
||
var/paused_ticks = 0
|
||
|
||
/// Tracks how much of a tick the subsystem has consumed in the current run.
|
||
var/paused_tick_usage
|
||
|
||
/// Tracks how many fires the subsystem takes to complete a run on average.
|
||
var/ticks = 1
|
||
|
||
/// 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
|
||
|
||
/**
|
||
* Priority at the time the subsystem entered the queue.
|
||
* Needed to avoid changes in priority (by admins and the like) from breaking things.
|
||
*/
|
||
var/queued_priority
|
||
|
||
/**
|
||
* How many times we suspect a subsystem type has crashed the MC.
|
||
* 3 strikes and you're out!
|
||
*/
|
||
var/static/list/failure_strikes
|
||
|
||
/// Next subsystem in the queue of subsystems to run this tick.
|
||
///
|
||
/// * the queue is a doubly-linked non-circular linked list
|
||
var/datum/controller/subsystem/queue_next
|
||
/// Previous subsystem in the queue of subsystems to run this tick.
|
||
///
|
||
/// * the queue is a doubly-linked non-circular linked list
|
||
var/datum/controller/subsystem/queue_prev
|
||
|
||
//* Recomputed at start of Loop(), as well as on changes *//
|
||
|
||
/// The **nominal** world.time in deciseconds, before runs
|
||
var/nominal_dt_ds
|
||
/// The **nominal** world.time in seconds, before runs
|
||
var/nominal_dt_s
|
||
|
||
//* Tracked by ignite() *//
|
||
|
||
/// running average of our 'personal' tick drift
|
||
///
|
||
/// * 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
|
||
/// * 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.
|
||
*/
|
||
|
||
/**
|
||
* Called before global vars are initialized
|
||
* Called before Recover()
|
||
*
|
||
* ! Warning: Old subsystem won't be cleaned up yet if recovering,
|
||
* ! be sure to reference us normally and not with SSname. pattern,
|
||
* ! unless you are doing it on purpose.
|
||
*
|
||
* ? Prefer Initialize() where possible, don't put anything laggy in here please.
|
||
*/
|
||
/datum/controller/subsystem/proc/PreInit(recovering)
|
||
return
|
||
|
||
/**
|
||
* Called after global vars are initialized
|
||
* Called before Recover()
|
||
*
|
||
* ! Warning: Old subsystem won't be cleaned up yet if recovering,
|
||
* ! be sure to reference us normally and not with SSname. pattern,
|
||
* ! unless you are doing it on purpose.
|
||
*
|
||
* ? Prefer Initialize() where possible, don't put anything laggy in here please.
|
||
*/
|
||
/datum/controller/subsystem/proc/Preload(recovering)
|
||
return
|
||
|
||
/**
|
||
* 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
|
||
// 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
|
||
|
||
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!
|
||
* fire() seems more suitable. This is the procedure that gets called every 'wait' deciseconds.
|
||
* Sleeping in here prevents future fires until returned.
|
||
*/
|
||
/datum/controller/subsystem/proc/fire(resumed = FALSE)
|
||
subsystem_flags |= SS_NO_FIRE
|
||
CRASH("Subsystem [src]([type]) does not fire() but did not set the SS_NO_FIRE flag. Please add the SS_NO_FIRE flag to any subsystem that doesn't fire so it doesn't get added to the processing list and waste cpu.")
|
||
|
||
/datum/controller/subsystem/Destroy()
|
||
dequeue()
|
||
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)
|
||
* (this lets us sort our run order correctly without having to re-sort the entire already sorted list)
|
||
*/
|
||
/datum/controller/subsystem/proc/enqueue()
|
||
var/SS_priority = priority
|
||
var/SS_flags = subsystem_flags
|
||
var/datum/controller/subsystem/queue_node
|
||
var/queue_node_priority
|
||
var/queue_node_flags
|
||
|
||
for (queue_node = Master.queue_head; queue_node; queue_node = queue_node.queue_next)
|
||
queue_node_priority = queue_node.queued_priority
|
||
queue_node_flags = queue_node.subsystem_flags
|
||
|
||
if (queue_node_flags & SS_TICKER)
|
||
if (!(SS_flags & SS_TICKER))
|
||
continue
|
||
|
||
if (queue_node_priority < SS_priority)
|
||
break
|
||
|
||
else if (queue_node_flags & SS_BACKGROUND)
|
||
if (!(SS_flags & SS_BACKGROUND))
|
||
break
|
||
|
||
if (queue_node_priority < SS_priority)
|
||
break
|
||
|
||
else
|
||
if (SS_flags & SS_BACKGROUND)
|
||
continue
|
||
|
||
if (SS_flags & SS_TICKER)
|
||
break
|
||
|
||
if (queue_node_priority < SS_priority)
|
||
break
|
||
|
||
queued_time = world.time
|
||
queued_priority = SS_priority
|
||
state = SS_QUEUED
|
||
|
||
/// update the running total of priorities in the queue of the MC
|
||
if (SS_flags & SS_BACKGROUND)
|
||
Master.queue_priority_count_bg += SS_priority
|
||
else
|
||
Master.queue_priority_count += SS_priority
|
||
|
||
queue_next = queue_node
|
||
if (!queue_node) // We stopped at the end, add to tail.
|
||
queue_prev = Master.queue_tail
|
||
if (Master.queue_tail)
|
||
Master.queue_tail.queue_next = src
|
||
|
||
else // Empty queue, we also need to set the head.
|
||
Master.queue_head = src
|
||
|
||
Master.queue_tail = src
|
||
|
||
else if (queue_node == Master.queue_head) // Insert at start of list.
|
||
Master.queue_head.queue_prev = src
|
||
Master.queue_head = src
|
||
queue_prev = null
|
||
|
||
else
|
||
queue_node.queue_prev.queue_next = src
|
||
queue_prev = queue_node.queue_prev
|
||
queue_node.queue_prev = src
|
||
|
||
/datum/controller/subsystem/proc/dequeue()
|
||
// eject from doubly linked list
|
||
if (queue_next)
|
||
queue_next.queue_prev = queue_prev
|
||
if (queue_prev)
|
||
queue_prev.queue_next = queue_next
|
||
// ensure MC's references aren't us
|
||
if (src == Master.queue_tail)
|
||
Master.queue_tail = queue_prev
|
||
if (src == Master.queue_head)
|
||
Master.queue_head = queue_next
|
||
|
||
queued_time = 0
|
||
if (state == SS_QUEUED)
|
||
state = SS_IDLE
|
||
|
||
/datum/controller/subsystem/proc/pause()
|
||
. = TRUE
|
||
switch(state)
|
||
if(SS_RUNNING)
|
||
state = SS_PAUSED
|
||
if(SS_SLEEPING)
|
||
state = SS_PAUSING
|
||
|
||
/// Called after the config has been loaded or reloaded.
|
||
/datum/controller/subsystem/proc/on_config_loaded()
|
||
return
|
||
|
||
/**
|
||
* 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(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 "\[[state_letter()]\] [name]"
|
||
|
||
/**
|
||
* Returns our status symbol.
|
||
*/
|
||
/datum/controller/subsystem/proc/state_letter()
|
||
// 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.
|
||
* For instance, during cpu intensive operations like explosions.
|
||
*/
|
||
/datum/controller/subsystem/proc/postpone(cycles = 1)
|
||
if(next_fire - world.time < wait)
|
||
next_fire += (wait*cycles)
|
||
|
||
/datum/controller/subsystem/vv_edit_var(var_name, var_value)
|
||
switch (var_name)
|
||
if (NAMEOF(src, can_fire))
|
||
// This is so the subsystem doesn't rapid fire to make up missed ticks causing more lag
|
||
if (var_value)
|
||
next_fire = world.time + wait
|
||
if (NAMEOF(src, queued_priority)) // Editing this breaks things.
|
||
return FALSE
|
||
if (NAMEOF(src, wait))
|
||
return set_wait(var_value)
|
||
|
||
. = ..()
|
||
|
||
/**
|
||
* Called when max z is changed since subsystems hook it so much.
|
||
*
|
||
* todo: maploader might need to be able to init the new level before this fires if this ever does more than make indice-lists synchronize.
|
||
*
|
||
* Arguments:
|
||
* * old_z_count - The old z count.
|
||
* * new_z_count - The new z count.
|
||
*/
|
||
/datum/controller/subsystem/proc/on_max_z_changed(old_z_count, new_z_count)
|
||
return
|
||
|
||
/**
|
||
* Called when world ticklag is changed
|
||
*
|
||
* @params
|
||
* * old_ticklag
|
||
* * new_ticklag
|
||
*/
|
||
/datum/controller/subsystem/proc/on_ticklag_changed(old_ticklag, new_ticklag)
|
||
recompute_wait_dt()
|
||
|
||
/**
|
||
* Called when SQL is reconnected after being disconnected
|
||
*/
|
||
/datum/controller/subsystem/proc/on_sql_reconnect()
|
||
return
|
||
|
||
//* Wait *//
|
||
|
||
/datum/controller/subsystem/proc/set_wait(new_wait)
|
||
ASSERT(isnum(new_wait))
|
||
src.wait = new_wait
|
||
recompute_wait_dt()
|
||
return TRUE
|
||
|
||
/datum/controller/subsystem/proc/recompute_wait_dt()
|
||
nominal_dt_ds = max(world.tick_lag, (subsystem_flags & SS_TICKER)? (wait * world.tick_lag) : (wait))
|
||
nominal_dt_s = nominal_dt_ds * 0.1
|