diff --git a/code/controllers/admin.dm b/code/controllers/admin.dm
index 19fef28597..5c767ecb1b 100644
--- a/code/controllers/admin.dm
+++ b/code/controllers/admin.dm
@@ -1,14 +1,25 @@
// Clickable stat() button.
/obj/effect/statclick
name = "Initializing..."
+ // blocks_emissive = NONE
var/target
INITIALIZE_IMMEDIATE(/obj/effect/statclick)
-/obj/effect/statclick/Initialize(mapload, text, target) //Don't port this to Initialize it's too critical
+/obj/effect/statclick/Initialize(mapload, text, target)
. = ..()
name = text
src.target = target
+ if(istype(target, /datum)) //Harddel man bad
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/cleanup)
+
+/obj/effect/statclick/Destroy()
+ target = null
+ return ..()
+
+/obj/effect/statclick/proc/cleanup()
+ SIGNAL_HANDLER
+ qdel(src)
/obj/effect/statclick/proc/update(text)
name = text
@@ -51,3 +62,30 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick)
SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Failsafe Controller")
message_admins("Admin [key_name_admin(usr)] has restarted the [controller] controller.")
+
+/client/proc/debug_controller()
+ set category = "Debug"
+ set name = "Debug Controller"
+ set desc = "Debug the various periodic loop controllers for the game (be careful!)"
+
+ if(!holder)
+ return
+
+ var/list/controllers = list()
+ var/list/controller_choices = list()
+
+ for (var/datum/controller/controller in world)
+ if (istype(controller, /datum/controller/subsystem))
+ continue
+ controllers["[controller] (controller.type)"] = controller //we use an associated list to ensure clients can't hold references to controllers
+ controller_choices += "[controller] (controller.type)"
+
+ var/datum/controller/controller_string = input("Select controller to debug", "Debug Controller") as null|anything in controller_choices
+ var/datum/controller/controller = controllers[controller_string]
+
+ if (!istype(controller))
+ return
+ debug_variables(controller)
+
+ SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Failsafe Controller")
+ message_admins("Admin [key_name_admin(usr)] is debugging the [controller] controller.")
diff --git a/code/controllers/failsafe.dm b/code/controllers/failsafe.dm
index a15056e442..04b88eb3be 100644
--- a/code/controllers/failsafe.dm
+++ b/code/controllers/failsafe.dm
@@ -15,7 +15,7 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
// The alert level. For every failed poke, we drop a DEFCON level. Once we hit DEFCON 1, restart the MC.
var/defcon = 5
//the world.time of the last check, so the mc can restart US if we hang.
- // (Real friends look out for *eachother*)
+ // (Real friends look out for *eachother*)
var/lasttick = 0
// Track the MC iteration to make sure its still on track.
@@ -33,6 +33,22 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
/datum/controller/failsafe/Initialize()
set waitfor = FALSE
Failsafe.Loop()
+ if (!Master || defcon == 0) //Master is gone/not responding and Failsafe just exited its loop
+ defcon = 3 //Reset defcon level as its used inside the emergency loop
+ while (defcon > 0)
+ var/recovery_result = emergency_loop()
+ if (recovery_result == 1) //Exit emergency loop and delete self if it was able to recover MC
+ break
+ else if (defcon == 1) //Exit Failsafe if we weren't able to recover the MC in the last stage
+ log_game("FailSafe: Failed to recover MC while in emergency state. Failsafe exiting.")
+ message_admins(span_boldannounce("Failsafe failed criticaly while trying to recreate broken MC. Please manually fix the MC or reboot the server. Failsafe exiting now."))
+ message_admins(span_boldannounce("You can try manually calling these two procs:."))
+ message_admins(span_boldannounce("/proc/recover_all_SS_and_recreate_master: Most stuff should still function but expect instability/runtimes/broken stuff."))
+ message_admins(span_boldannounce("/proc/delete_all_SS_and_recreate_master: Most stuff will be broken but basic stuff like movement and chat should still work."))
+ else if (recovery_result == -1) //Failed to recreate MC
+ defcon--
+ sleep(initial(processing_interval)) //Wait a bit until the next try
+
if(!QDELETED(src))
qdel(src) //when Loop() returns, we delete ourselves and let the mc recreate us
@@ -45,42 +61,56 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
while(running)
lasttick = world.time
if(!Master)
- // Replace the missing Master! This should never, ever happen.
- new /datum/controller/master()
+ // Break out of the main loop so we go into emergency state
+ break
// Only poke it if overrides are not in effect.
if(processing_interval > 0)
if(Master.processing && Master.iteration)
+ if (defcon > 1 && (!Master.stack_end_detector || !Master.stack_end_detector.check()))
+
+ to_chat(GLOB.admins, span_boldannounce("ERROR: The Master Controller code stack has exited unexpectedly, Restarting..."))
+ defcon = 0
+ var/rtn = Recreate_MC()
+ if(rtn > 0)
+ master_iteration = 0
+ to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
+ else if(rtn < 0)
+ log_game("FailSafe: Could not restart MC, runtime encountered. Entering defcon 0")
+ to_chat(GLOB.admins, span_boldannounce("ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying."))
// Check if processing is done yet.
if(Master.iteration == master_iteration)
switch(defcon)
if(4,5)
--defcon
- if(3)
- message_admins("Notice: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks.")
- --defcon
- if(2)
- to_chat(GLOB.admins, "Warning: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks. Automatic restart in [processing_interval] ticks.")
- --defcon
- if(1)
- to_chat(GLOB.admins, "Warning: DEFCON [defcon_pretty()]. The Master Controller has still not fired within the last [(5-defcon) * processing_interval] ticks. Killing and restarting...")
+ if(3)
+ message_admins(span_adminnotice("Notice: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks."))
+ --defcon
+
+ if(2)
+ to_chat(GLOB.admins, span_boldannounce("Warning: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks. Automatic restart in [processing_interval] ticks."))
+ --defcon
+
+ if(1)
+ to_chat(GLOB.admins, span_boldannounce("Warning: DEFCON [defcon_pretty()]. The Master Controller has still not fired within the last [(5-defcon) * processing_interval] ticks. Killing and restarting..."))
--defcon
var/rtn = Recreate_MC()
if(rtn > 0)
defcon = 4
master_iteration = 0
- to_chat(GLOB.admins, "MC restarted successfully")
+ to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
else if(rtn < 0)
log_game("FailSafe: Could not restart MC, runtime encountered. Entering defcon 0")
- to_chat(GLOB.admins, "ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying.")
+ to_chat(GLOB.admins, span_boldannounce("ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying."))
//if the return number was 0, it just means the mc was restarted too recently, and it just needs some time before we try again
//no need to handle that specially when defcon 0 can handle it
+
if(0) //DEFCON 0! (mc failed to restart)
var/rtn = Recreate_MC()
if(rtn > 0)
defcon = 4
master_iteration = 0
- to_chat(GLOB.admins, "MC restarted successfully")
+ to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
else
defcon = min(defcon + 1,5)
master_iteration = Master.iteration
@@ -92,6 +122,57 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
defcon = 5
sleep(initial(processing_interval))
+//Emergency loop used when Master got deleted or the main loop exited while Defcon == 0
+//Loop is driven externally so runtimes only cancel the current recovery attempt
+/datum/controller/failsafe/proc/emergency_loop()
+ //The code in this proc should be kept as simple as possible, anything complicated like to_chat might rely on master existing and runtime
+ //The goal should always be to get a new Master up and running before anything else
+ . = -1
+ switch (defcon) //The lower defcon goes the harder we try to fix the MC
+ if (2 to 3) //Try to normally recreate the MC two times
+ . = Recreate_MC()
+ if (1) //Delete the old MC first so we don't transfer any info, in case that caused any issues
+ del(Master)
+ . = Recreate_MC()
+
+ if (. == 1) //We were able to create a new master
+ master_iteration = 0
+ SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
+ Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
+ to_chat(GLOB.admins, span_adminnotice("Failsafe recovered MC while in emergency state [defcon_pretty()]"))
+ else
+ log_game("FailSafe: Failsafe in emergency state and was unable to recreate MC while in defcon state [defcon_pretty()].")
+ message_admins(span_boldannounce("Failsafe in emergency state and master down, trying to recreate MC while in defcon level [defcon_pretty()] failed."))
+
+///Recreate all SSs which will still cause data survive due to Recover(), the new Master will then find and take them from global.vars
+/proc/recover_all_SS_and_recreate_master()
+ del(Master)
+ var/list/subsytem_types = subtypesof(/datum/controller/subsystem)
+ sortTim(subsytem_types, /proc/cmp_subsystem_init)
+ for(var/I in subsytem_types)
+ new I
+ . = Recreate_MC()
+ if (. == 1) //We were able to create a new master
+ SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
+ Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
+ to_chat(GLOB.admins, span_adminnotice("MC successfully recreated after recovering all subsystems!"))
+ else
+ message_admins(span_boldannounce("Failed to create new MC!"))
+
+///Delete all existing SS to basically start over
+/proc/delete_all_SS_and_recreate_master()
+ del(Master)
+ for(var/global_var in global.vars)
+ if (istype(global.vars[global_var], /datum/controller/subsystem))
+ del(global.vars[global_var])
+ . = Recreate_MC()
+ if (. == 1) //We were able to create a new master
+ SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
+ Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
+ to_chat(GLOB.admins, span_adminnotice("MC successfully recreated after deleting and recreating all subsystems!"))
+ else
+ message_admins(span_boldannounce("Failed to create new MC!"))
+
/datum/controller/failsafe/proc/defcon_pretty()
return defcon
diff --git a/code/controllers/globals.dm b/code/controllers/globals.dm
index 21f022acfd..7b5cc94d36 100644
--- a/code/controllers/globals.dm
+++ b/code/controllers/globals.dm
@@ -14,7 +14,7 @@ GLOBAL_REAL(GLOB, /datum/controller/global_vars)
var/datum/controller/exclude_these = new
gvars_datum_in_built_vars = exclude_these.vars + list(NAMEOF(src, gvars_datum_protected_varlist), NAMEOF(src, gvars_datum_in_built_vars), NAMEOF(src, gvars_datum_init_order))
- QDEL_IN(exclude_these, 0) //signal logging isn't ready
+ QDEL_IN(exclude_these, 0) //signal logging isn't ready
log_world("[vars.len - gvars_datum_in_built_vars.len] global variables")
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index 59ac68960c..055271fd86 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -18,15 +18,17 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
/datum/controller/master
name = "Master"
- // Are we processing (higher values increase the processing delay by n ticks)
+ /// Are we processing (higher values increase the processing delay by n ticks)
var/processing = TRUE
- // How many times have we ran
+ /// How many times have we ran
var/iteration = 0
+ /// Stack end detector to detect stack overflows that kill the mc's main loop
+ var/datum/stack_end_detector/stack_end_detector
- // world.time of last fire, for tracking lag outside of the mc
+ /// world.time of last fire, for tracking lag outside of the mc
var/last_run
- // List of subsystems to process().
+ /// List of subsystems to process().
var/list/subsystems
// Vars for keeping track of tick drift.
@@ -34,25 +36,27 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/init_time
var/tickdrift = 0
+ /// How long is the MC sleeping between runs, read only (set by Loop() based off of anti-tick-contention heuristics)
var/sleep_delta = 1
- ///Only run ticker subsystems for the next n ticks.
+ /// Only run ticker subsystems for the next n ticks.
var/skip_ticks = 0
+ /// makes the mc main loop runtime
var/make_runtime = FALSE
- var/initializations_finished_with_no_players_logged_in //I wonder what this could be?
+ var/initializations_finished_with_no_players_logged_in //I wonder what this could be?
- // The type of the last subsystem to be process()'d.
+ /// The type of the last subsystem to be fire()'d.
var/last_type_processed
- var/datum/controller/subsystem/queue_head //Start of queue linked list
- var/datum/controller/subsystem/queue_tail //End of queue linked list (used for appending to the list)
+ var/datum/controller/subsystem/queue_head //!Start of queue linked list
+ var/datum/controller/subsystem/queue_tail //!End of queue linked list (used for appending to the list)
var/queue_priority_count = 0 //Running total so that we don't have to loop thru the queue each run to split up the tick
var/queue_priority_count_bg = 0 //Same, but for background subsystems
- var/map_loading = FALSE //Are we loading in a new map?
+ var/map_loading = FALSE //!Are we loading in a new map?
- var/current_runlevel //for scheduling different subsystems for different stages of the round
+ var/current_runlevel //!for scheduling different subsystems for different stages of the round
var/sleep_offline_after_initializations = TRUE
var/static/restart_clear = 0
@@ -61,8 +65,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/static/random_seed
- //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
+ ///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
var/static/current_ticklimit = TICK_LIMIT_RUNNING
/datum/controller/master/New()
@@ -81,15 +85,27 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/list/_subsystems = list()
subsystems = _subsystems
if (Master != src)
- if (istype(Master))
+ if (istype(Master)) //If there is an existing MC take over his stuff and delete it
Recover()
qdel(Master)
+ Master = src
else
+ //Code used for first master on game boot or if existing master got deleted
+ Master = src
var/list/subsytem_types = subtypesof(/datum/controller/subsystem)
sortTim(subsytem_types, /proc/cmp_subsystem_init)
+ //Find any abandoned subsystem from the previous master (if there was any)
+ var/list/existing_subsystems = list()
+ for(var/global_var in global.vars)
+ if (istype(global.vars[global_var], /datum/controller/subsystem))
+ existing_subsystems += global.vars[global_var]
+ //Either init a new SS or if an existing one was found use that
for(var/I in subsytem_types)
- _subsystems += new I
- Master = src
+ var/datum/controller/subsystem/existing_subsystem = locate(I) in existing_subsystems
+ if (istype(existing_subsystem))
+ _subsystems += existing_subsystem
+ else
+ _subsystems += new I
if(!GLOB)
new /datum/controller/global_vars
@@ -109,7 +125,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
log_world("Shutdown complete")
// 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
+// -1 if we encountered a runtime trying to recreate it
/proc/Recreate_MC()
. = -1 //so if we runtime, things know we failed
if (world.time < Master.restart_timeout)
@@ -120,7 +136,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/delay = 50 * ++Master.restart_count
Master.restart_timeout = world.time + delay
Master.restart_clear = world.time + (delay * 2)
- Master.processing = FALSE //stop ticking this one
+ if (Master) //Can only do this if master hasn't been deleted
+ Master.processing = FALSE //stop ticking this one
try
new/datum/controller/master()
catch
@@ -156,22 +173,22 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
msg = "The [BadBoy.name] subsystem seems to be destabilizing the MC and will be offlined."
BadBoy.flags |= SS_NO_FIRE
if(msg)
- to_chat(GLOB.admins, "[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
StartProcessing(10)
else
- to_chat(world, "The Master Controller is having some issues, we will need to re-initialize EVERYTHING")
+ to_chat(world, span_boldannounce("The Master Controller is having some issues, we will need to re-initialize EVERYTHING"))
Initialize(20, TRUE)
// 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()
+// Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize()
/datum/controller/master/Initialize(delay, init_sss, tgs_prime)
set waitfor = 0
@@ -181,7 +198,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if(init_sss)
init_subtypes(/datum/controller/subsystem, subsystems)
- to_chat(world, "Initializing subsystems...")
+ to_chat(world, span_boldannounce("Initializing subsystems..."))
// Sort subsystems by init_order, so they initialize in the correct order.
sortTim(subsystems, /proc/cmp_subsystem_init)
@@ -190,7 +207,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
// Initialize subsystems.
current_ticklimit = CONFIG_GET(number/tick_limit_mc_init)
for (var/datum/controller/subsystem/SS in subsystems)
- if (SS.flags & SS_NO_INIT)
+ if (SS.flags & SS_NO_INIT || SS.initialized) //Don't init SSs with the correspondig flag or if they already are initialzized
continue
SS.Initialize(REALTIMEOFDAY)
CHECK_TICK
@@ -198,7 +215,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initializations complete within [time] second[time == 1 ? "" : "s"]!"
- to_chat(world, "[msg]")
+ to_chat(world, span_boldannounce("[msg]"))
log_world(msg)
if (!current_runlevel)
@@ -255,11 +272,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
/datum/controller/master/proc/Loop()
. = -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
+ // local vars rock
//all this shit is here so that flag edits can be refreshed by restarting the MC. (and for speed)
var/list/tickersubsystems = list()
- var/list/runlevel_sorted_subsystems = list(list()) //ensure we always have at least one runlevel
+ 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
@@ -305,8 +322,12 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/error_level = 0
var/sleep_delta = 1
var/list/subsystems_to_check
- //the actual loop.
+ //setup the stack overflow detector
+ stack_end_detector = new()
+ var/datum/stack_canary/canary = stack_end_detector.prime_canary()
+ canary.use_variable()
+ //the actual loop.
while (1)
tickdrift = max(0, MC_AVERAGE_FAST(tickdrift, (((REALTIMEOFDAY - init_timeofday) - (world.time - init_time)) / world.tick_lag)))
var/starting_tick_usage = TICK_USAGE
@@ -317,7 +338,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
//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)
+ // (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
@@ -423,6 +444,10 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
continue
if ((SS_flags & (SS_TICKER|SS_KEEP_TIMING)) == SS_KEEP_TIMING && SS.last_fire + (SS.wait * 0.75) > world.time)
continue
+ if (SS.postponed_fires >= 1)
+ SS.postponed_fires--
+ SS.update_nextfire()
+ continue
SS.enqueue()
. = 1
@@ -439,12 +464,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/tick_precentage
var/tick_remaining
var/ran = TRUE //this is right
- var/ran_non_ticker = FALSE
var/bg_calc //have we swtiched current_tick_budget to background mode yet?
var/tick_usage
//keep running while we have stuff to run and we haven't gone over a tick
- // this is so subsystems paused eariler can use tick time that later subsystems never used
+ // this is so subsystems paused eariler can use tick time that later subsystems never used
while (ran && queue_head && TICK_USAGE < TICK_LIMIT_MC)
ran = FALSE
bg_calc = FALSE
@@ -459,20 +483,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if (!(queue_node_flags & SS_TICKER) && skip_ticks)
queue_node = queue_node.queue_next
continue
- //super special case, subsystems where we can't make them pause mid way through
- //if we can't run them this tick (without going over a tick)
- //we bump up their priority and attempt to run them next tick
- //(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_ticker)
- if (!(queue_node_flags & SS_BACKGROUND))
- queue_node.queued_priority += queue_priority_count * 0.1
- queue_priority_count -= queue_node_priority
- queue_priority_count += queue_node.queued_priority
- current_tick_budget -= queue_node_priority
- queue_node = queue_node.queue_next
- continue
if (!bg_calc && (queue_node_flags & SS_BACKGROUND))
current_tick_budget = queue_priority_count_bg
@@ -481,7 +491,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
tick_remaining = TICK_LIMIT_RUNNING - TICK_USAGE
if (current_tick_budget > 0 && queue_node_priority > 0)
- tick_precentage = tick_remaining / (current_tick_budget / queue_node_priority)
+ //Give the subsystem a precentage of the remaining tick based on the remaning priority
+ tick_precentage = tick_remaining * (queue_node_priority / current_tick_budget)
else
tick_precentage = tick_remaining
@@ -489,8 +500,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
current_ticklimit = round(TICK_USAGE + tick_precentage)
- if (!(queue_node_flags & SS_TICKER))
- ran_non_ticker = TRUE
ran = TRUE
queue_node_paused = (queue_node.state == SS_PAUSED || queue_node.state == SS_PAUSING)
@@ -535,14 +544,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
queue_node.last_fire = world.time
queue_node.times_fired++
- if (queue_node_flags & SS_TICKER)
- queue_node.next_fire = world.time + (world.tick_lag * queue_node.wait)
- else if (queue_node_flags & SS_POST_FIRE_TIMING)
- 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)
- queue_node.next_fire += queue_node.wait
- else
- queue_node.next_fire = queue_node.queued_time + queue_node.wait + (world.tick_lag * (queue_node.tick_overrun/100))
+ queue_node.update_nextfire()
queue_node.queued_time = 0
@@ -554,7 +556,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
. = 1
//resets the queue, and all subsystems, while filtering out the subsystem lists
-// called if any mc's queue procs runtime or exit improperly.
+// called if any mc's queue procs runtime or exit improperly.
/datum/controller/master/proc/SoftReset(list/ticker_SS, list/runlevel_SS)
. = 0
log_world("MC: SoftReset called, resetting MC queue state.")
diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm
index 12798f3863..db780ca05b 100644
--- a/code/controllers/subsystem.dm
+++ b/code/controllers/subsystem.dm
@@ -29,11 +29,11 @@
var/initialized = FALSE
/// Set to 0 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
+ /// 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 //points of the game at which the SS can fire
+ var/runlevels = RUNLEVELS_DEFAULT //points of the game at which the SS can fire
/*
* The following variables are managed by the MC and should not be modified directly.
@@ -69,6 +69,9 @@
/// Tracks the amount of completed runs for the subsystem
var/times_fired = 0
+ /// How many fires have we been requested to postpone
+ var/postponed_fires = 0
+
/// Time the subsystem entered the queue, (for timing and priority reasons)
var/queued_time = 0
@@ -122,12 +125,38 @@
dequeue()
can_fire = 0
flags |= SS_NO_FIRE
- Master.subsystems -= src
+ if (Master)
+ Master.subsystems -= src
return ..()
+
+/** Update next_fire for the next run.
+ * reset_time (bool) - Ignore things that would normally alter the next fire, like tick_overrun, and last_fire. (also resets postpone)
+ */
+/datum/controller/subsystem/proc/update_nextfire(reset_time = FALSE)
+ var/queue_node_flags = flags
+
+ if (reset_time)
+ postponed_fires = 0
+ if (queue_node_flags & SS_TICKER)
+ next_fire = world.time + (world.tick_lag * wait)
+ else
+ next_fire = world.time + wait
+ return
+
+ if (queue_node_flags & SS_TICKER)
+ next_fire = world.time + (world.tick_lag * wait)
+ else if (queue_node_flags & SS_POST_FIRE_TIMING)
+ next_fire = world.time + wait + (world.tick_lag * (tick_overrun/100))
+ else if (queue_node_flags & SS_KEEP_TIMING)
+ next_fire += wait
+ else
+ 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)
+// (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 = flags
@@ -191,9 +220,9 @@
queue_next.queue_prev = queue_prev
if (queue_prev)
queue_prev.queue_next = queue_next
- if (src == Master.queue_tail)
+ if (Master && (src == Master.queue_tail))
Master.queue_tail = queue_prev
- if (src == Master.queue_head)
+ if (Master && (src == Master.queue_head))
Master.queue_head = queue_next
queued_time = 0
if (state == SS_QUEUED)
@@ -217,10 +246,11 @@
//used to initialize the subsystem AFTER the map has loaded
/datum/controller/subsystem/Initialize(start_timeofday)
initialized = TRUE
+ // SEND_SIGNAL(src, COMSIG_SUBSYSTEM_POST_INITIALIZE, start_timeofday)
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!"
- to_chat(world, "[msg]")
- log_subsystem("INIT", msg)
+ to_chat(world, span_boldannounce("[msg]"))
+ log_subsystem(msg)
return time
/datum/controller/subsystem/stat_entry(msg)
@@ -243,11 +273,10 @@
if (SS_IDLE)
. = " "
-//could be used to postpone a costly subsystem for (default one) var/cycles, cycles
-//for instance, during cpu intensive operations like explosions
+/// Causes the next "cycle" fires to be missed. Effect is accumulative but can reset by calling update_nextfire(reset_time = TRUE)
/datum/controller/subsystem/proc/postpone(cycles = 1)
- if(next_fire - world.time < wait)
- next_fire += (wait*cycles)
+ if (can_fire && cycles >= 1)
+ postponed_fires += 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
@@ -258,7 +287,7 @@
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
+ update_nextfire(reset_time = TRUE)
if (NAMEOF(src, queued_priority)) //editing this breaks things.
return FALSE
. = ..()
diff --git a/code/datums/helper_datums/stack_end_detector.dm b/code/datums/helper_datums/stack_end_detector.dm
new file mode 100644
index 0000000000..0d621f51b3
--- /dev/null
+++ b/code/datums/helper_datums/stack_end_detector.dm
@@ -0,0 +1,32 @@
+/**
+ Stack End Detector.
+ Can detect if a given code stack has exited, used by the mc for stack overflow detection.
+
+ **/
+/datum/stack_end_detector
+ var/datum/weakref/_WF
+ var/datum/stack_canary/_canary
+
+/datum/stack_end_detector/New()
+ _canary = new()
+ _WF = WEAKREF(_canary)
+
+/** Prime the stack overflow detector.
+ Store the return value of this proc call in a proc level var.
+ Can only be called once.
+**/
+/datum/stack_end_detector/proc/prime_canary()
+ if (!_canary)
+ CRASH("Prime_canary called twice")
+ . = _canary
+ _canary = null
+
+/// Returns true if the stack is still going. Calling before the canary has been primed also returns true
+/datum/stack_end_detector/proc/check()
+ return !!_WF.resolve()
+
+/// Stack canary. Will go away if the stack it was primed by is ended by byond for return or stack overflow reasons.
+/datum/stack_canary
+
+/// empty proc to avoid warnings about unused variables. Call this proc on your canary in the stack it's watching.
+/datum/stack_canary/proc/use_variable()
diff --git a/tgstation.dme b/tgstation.dme
index c3a1e7087c..f483bcbbad 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -665,6 +665,7 @@
#include "code\datums\helper_datums\events.dm"
#include "code\datums\helper_datums\getrev.dm"
#include "code\datums\helper_datums\icon_snapshot.dm"
+#include "code\datums\helper_datums\stack_end_detector.dm"
#include "code\datums\helper_datums\teleport.dm"
#include "code\datums\looping_sounds\_looping_sound.dm"
#include "code\datums\looping_sounds\item_sounds.dm"