diff --git a/cev_eris.dme b/cev_eris.dme
index ec1885774cd..49dc1ee2f24 100644
--- a/cev_eris.dme
+++ b/cev_eris.dme
@@ -163,6 +163,7 @@
#include "code\__HELPERS\shell.dm"
#include "code\__HELPERS\spawn_sync.dm"
#include "code\__HELPERS\stack_trace.dm"
+#include "code\__HELPERS\stoplag.dm"
#include "code\__HELPERS\text.dm"
#include "code\__HELPERS\time.dm"
#include "code\__HELPERS\turfs.dm"
@@ -276,6 +277,7 @@
#include "code\controllers\hooks-defs.dm"
#include "code\controllers\hooks.dm"
#include "code\controllers\master.dm"
+#include "code\controllers\profiler.dm"
#include "code\controllers\subsystem.dm"
#include "code\controllers\verbs.dm"
#include "code\controllers\configuration\config_entry.dm"
@@ -394,7 +396,6 @@
#include "code\datums\http.dm"
#include "code\datums\interaction_particle.dm"
#include "code\datums\licences.dm"
-#include "code\datums\loot_spawner.dm"
#include "code\datums\mind.dm"
#include "code\datums\mixed.dm"
#include "code\datums\mob_stats.dm"
diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm
index 4d5b7de37d8..4dd8f3ce601 100644
--- a/code/__DEFINES/MC.dm
+++ b/code/__DEFINES/MC.dm
@@ -18,7 +18,7 @@
#define MC_AVG_SLOW_UP_FAST_DOWN(average, current) (average < current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current))
///creates a running average of "things elapsed" per time period when you need to count via a smaller time period.
-///eg you want an average number of things happening per second but you measure the event every tick (100 milliseconds).
+///eg you want an average number of things happening per second but you measure the event every tick (50 milliseconds).
///make sure both time intervals are in the same units. doesnt work if current_duration > total_duration or if total_duration == 0
#define MC_AVG_OVER_TIME(average, current, total_duration, current_duration) ((((total_duration) - (current_duration)) / (total_duration)) * (average) + (current))
@@ -58,32 +58,49 @@ if(Datum.is_processing) {\
//! 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
+#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 2
+#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, this overrides SS_TICKER's priority bump
-#define SS_BACKGROUND 4
+#define SS_BACKGROUND (1 << 2)
/** Treat wait as a tick count, not DS, run every wait ticks. */
/// (also forces it to run first in the tick (unless SS_BACKGROUND))
/// (We don't want to be choked out by other subsystems queuing into us)
/// (implies all runlevels because of how it works)
/// This is designed for basically anything that works as a mini-mc (like SStimer)
-#define SS_TICKER 8
+#define SS_TICKER (1 << 3)
/** 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.
-#define SS_KEEP_TIMING 16
+#define SS_KEEP_TIMING (1 << 4)
/** 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 32
+#define SS_POST_FIRE_TIMING (1 << 5)
+
+/// 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.
+/// It should not be used simply to silence CI.
+#define SS_OK_TO_FAIL_INIT (1 << 6)
+
+/// This subsystem should not be queued if it has no work.
+/// Populate the [hibernate_checks] list with the names of vars to check before a subsystem is queued.
+/// If the length() of each var is 0, it will not be queued.
+#define SS_HIBERNATE (1 << 7)
+
+/// Don't show when this has init'd
+#define SS_NO_INIT_MESSAGE (1 << 8)
+
+/// Allows the subsystem to be dynamically swapped between backgrounding.
+#define SS_DYNAMIC (1 << 9)
//! SUBSYSTEM STATES
#define SS_IDLE 0 /// ain't doing shit.
@@ -102,6 +119,7 @@ if(Datum.is_processing) {\
/datum/controller/subsystem/##X/New(){\
NEW_SS_GLOBAL(SS##X);\
PreInit();\
+ ss_id=#X;\
}\
/datum/controller/subsystem/##X
@@ -113,10 +131,27 @@ if(Datum.is_processing) {\
/datum/controller/subsystem/timer/##X/fire() {..() /*just so it shows up on the profiler*/} \
/datum/controller/subsystem/timer/##X
+#define MOVEMENT_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/movement/##X);\
+/datum/controller/subsystem/movement/##X/New(){\
+ NEW_SS_GLOBAL(SS##X);\
+ PreInit();\
+}\
+/datum/controller/subsystem/movement/##X/fire() {..() /*just so it shows up on the profiler*/} \
+/datum/controller/subsystem/movement/##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);\
PreInit();\
+ ss_id="processing_[#X]";\
}\
/datum/controller/subsystem/processing/##X/fire() {..() /*just so it shows up on the profiler*/} \
/datum/controller/subsystem/processing/##X
+
+#define MOBS_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/mobs/##X);\
+/datum/controller/subsystem/mobs/##X/New(){\
+ NEW_SS_GLOBAL(SS##X);\
+ PreInit();\
+}\
+/datum/controller/subsystem/mobs/##X/fire() {..() /*just so it shows up on the profiler*/} \
+/datum/controller/subsystem/mobs/##X
diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm
index 02ea42c1a8e..01816ac3e61 100644
--- a/code/__DEFINES/gamemode.dm
+++ b/code/__DEFINES/gamemode.dm
@@ -1,25 +1,3 @@
-
-//SSticker.current_state values
-/// Game is loading
-#define GAME_STATE_STARTUP 0
-/// Game is loaded and in pregame lobby
-#define GAME_STATE_PREGAME 1
-/// Game is attempting to start the round
-#define GAME_STATE_SETTING_UP 2
-/// Game has round in progress
-#define GAME_STATE_PLAYING 3
-/// Game has round finished
-#define GAME_STATE_FINISHED 4
-
-// Used for SSticker.force_ending
-/// Default, round is not being forced to end.
-#define END_ROUND_AS_NORMAL 0
-/// End the round now as normal
-#define FORCE_END_ROUND 1
-/// For admin forcing roundend, can be used to distinguish the two
-#define ADMIN_FORCE_END_ROUND 2
-
-
#define BE_PLANT "BE_PLANT"
#define BE_SYNTH "BE_SYNTH"
#define BE_PAI "BE_PAI"
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index d61a1e01ddf..51bd514f372 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -27,7 +27,6 @@
#define INVISIBILITY_ABSTRACT 101 //only used for abstract objects (e.g. spacevine_controller), things that are not really there.
// Some arbitrary defines to be used by self-pruning global lists.
-#define PROCESS_KILL 26 // Used to trigger removal from a processing list.
#define MAX_GEAR_COST 5 // Used in chargen for accessory loadout limit.
// For secHUDs and medHUDs and variants. The number is the location of the image on the list hud_list of humans.
@@ -374,6 +373,5 @@
#define TRACY_DLL_PATH (world.system_type == MS_WINDOWS ? "prof.dll" : "./libprof.so")
/// Path for the byond-memorystats dll
-
#define MEMORYSTATS_DLL_PATH (world.system_type == MS_WINDOWS ? "memorystats.dll" : "./libmemorystats.so")
diff --git a/code/__DEFINES/subsystems-priority.dm b/code/__DEFINES/subsystems-priority.dm
index 2494829a1ac..c889d8ad2b2 100644
--- a/code/__DEFINES/subsystems-priority.dm
+++ b/code/__DEFINES/subsystems-priority.dm
@@ -6,7 +6,7 @@
// SS_TICKER
// < none >
-var/list/bitflags = list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768)
+GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768))
#define FIRE_PRIORITY_DEFAULT 50 // Default priority for both normal and background processes
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index a053e2eb069..0389c322fa9 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -1,3 +1,8 @@
+//! Defines for subsystems and overlays
+//!
+//! Lots of important stuff in here, make sure you have your brain switched on
+//! when editing this file
+// "My brain was never on to begin with. what makes you think I'll turn it on now?"
//! ## DB defines
/**
@@ -63,6 +68,9 @@
#define FLIGHTSUIT_PROCESSING_NONE 0
#define FLIGHTSUIT_PROCESSING_FULL 1
+/// Used to trigger object removal from a processing list
+#define PROCESS_KILL 26
+
//! ## Initialization subsystem
///New should not call Initialize
@@ -91,6 +99,7 @@
///Call qdel with a force of TRUE after initialization
#define INITIALIZE_HINT_QDEL_FORCE 3
+//TODO: initialized and other stuff needs to be changed to `flags_1`
///type and all subtypes should always immediately call Initialize in New()
#define INITIALIZE_IMMEDIATE(X) ##X/New(loc, ...){\
..();\
@@ -103,10 +112,30 @@
}\
}
+//! ### SS initialization hints
+/**
+ * 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
+
+/// Successful, but don't print anything. Useful if subsystem was disabled.
+#define SS_INIT_NO_NEED 3
+
+//! ### SS initialization load orders
// 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.
+#define INIT_ORDER_PROFILER 101
#define INIT_ORDER_GARBAGE 99
#define INIT_ORDER_CHUNKS 95
#define INIT_ORDER_DBCORE 85
@@ -189,11 +218,33 @@ if(Datum.is_processing) {\
#define START_PROCESSING_POWER_OBJECT(Datum) START_PROCESSING_IN_LIST(Datum, power_objects)
#define STOP_PROCESSING_POWER_OBJECT(Datum) STOP_PROCESSING_IN_LIST(Datum, power_objects)
-/// The timer key used to know how long subsystem initialization takes
-#define SS_INIT_TIMER_KEY "ss_init"
-
#define SS_HOLOMAPS_TIMER_KEY "ss_holomaps"
+//Hibernation states
+#define SS_NOT_HIBERNATING 0
+#define SS_WAKING_UP 1
+#define SS_IS_HIBERNATING 2
+
+//SSticker.current_state values
+/// Game is loading
+#define GAME_STATE_STARTUP 0
+/// Game is loaded and in pregame lobby
+#define GAME_STATE_PREGAME 1
+/// Game is attempting to start the round
+#define GAME_STATE_SETTING_UP 2
+/// Game has round in progress
+#define GAME_STATE_PLAYING 3
+/// Game has round finished
+#define GAME_STATE_FINISHED 4
+
+// Used for SSticker.force_ending
+/// Default, round is not being forced to end.
+#define END_ROUND_AS_NORMAL 0
+/// End the round now as normal
+#define FORCE_END_ROUND 1
+/// For admin forcing roundend, can be used to distinguish the two
+#define ADMIN_FORCE_END_ROUND 2
+
/**
Create a new timer and add it to the queue.
* Arguments:
@@ -203,3 +254,32 @@ if(Datum.is_processing) {\
* * timer_subsystem the subsystem to insert this timer into
*/
#define addtimer(args...) _addtimer(args, file = __FILE__, line = __LINE__)
+
+// Air subsystem subtasks
+#define SSAIR_PIPENETS 1
+#define SSAIR_ATMOSMACHINERY 2
+#define SSAIR_TILES_CUR 3
+#define SSAIR_TILES_DEF 4
+#define SSAIR_EDGES 5
+#define SSAIR_FIRE_ZONES 6
+#define SSAIR_HOTSPOTS 7
+#define SSAIR_ZONES 8
+
+#define SSAIR_TICK_MULTIPLIER 2
+
+
+// Subsystem delta times or tickrates, in seconds. I.e, how many seconds in between each process() call for objects being processed by that subsystem.
+// Only use these defines if you want to access some other objects processing seconds_per_tick, otherwise use the seconds_per_tick that is sent as a parameter to process()
+// #define SSFLUIDS_DT (SSplumbing.wait/10)
+#define SSMACHINES_DT (SSmachines.wait/10)
+#define SSMOBS_DT (SSmobs.wait/10)
+#define SSOBJ_DT (SSobj.wait/10)
+
+/// The change in the world's time from the subsystem's last fire in seconds.
+#define DELTA_WORLD_TIME(ss) ((world.time - ss.last_fire) * 0.1)
+
+/// Same as DELTA_WORLD_TIME but we ignore time spent hibernating
+#define DELTA_WORLD_TIME_WITHOUT_HIBERNATION(ss) ss.hibernation_state ? ss.wait : DELTA_WORLD_TIME(ss)
+
+/// The timer key used to know how long subsystem initialization takes
+#define SS_INIT_TIMER_KEY "ss_init"
diff --git a/code/__HELPERS/_global_lists.dm b/code/__HELPERS/_global_lists.dm
index 1b23b255edd..945aa2fe000 100644
--- a/code/__HELPERS/_global_lists.dm
+++ b/code/__HELPERS/_global_lists.dm
@@ -113,7 +113,7 @@ GLOBAL_LIST_EMPTY_TYPED(world_uplinks, /obj/item/device/uplink)
GLOBAL_LIST_EMPTY_TYPED(krabin_linked, /mob/living/carbon/human)
// Announcer intercom, because too much stuff creates an intercom for one message then hard del()s it.
-GLOBAL_DATUM(announcer, /obj/item/device/radio/intercom)
+GLOBAL_DATUM_INIT(announcer, /obj/item/device/radio/intercom, new())
GLOBAL_LIST_EMPTY(lastsignalers) // Keeps last 100 signals here in format: "[src] used \ref[src] @ location [src.loc]: [freq]/[code]"
diff --git a/code/__HELPERS/stoplag.dm b/code/__HELPERS/stoplag.dm
new file mode 100644
index 00000000000..da9ac354688
--- /dev/null
+++ b/code/__HELPERS/stoplag.dm
@@ -0,0 +1,27 @@
+//Increases delay as the server gets more overloaded,
+//as sleeps aren't cheap and sleeping only to wake up and sleep again is wasteful
+#define DELTA_CALC max(((max(world.tick_usage, world.cpu) / 100) * max(Master.sleep_delta,1)), 1)
+
+///returns the number of ticks slept
+/proc/stoplag(initial_delay)
+ if (!Master || Master.init_stage_completed < INITSTAGE_MAX)
+ sleep(world.tick_lag)
+ return 1
+ if (!initial_delay)
+ initial_delay = world.tick_lag
+// Unit tests are not the normal environemnt. The mc can get absolutely thigh crushed, and sleeping procs running for ages is much more common
+// We don't want spurious hard deletes off this, so let's only sleep for the requested period of time here yeah?
+#ifdef UNIT_TESTS
+ sleep(initial_delay)
+ return CEILING(DS2TICKS(initial_delay), 1)
+#else
+ . = 0
+ var/i = DS2TICKS(initial_delay)
+ do
+ . += CEILING(i * DELTA_CALC, 1)
+ sleep(i * world.tick_lag * DELTA_CALC)
+ i *= 2
+ while (TICK_USAGE > min(TICK_LIMIT_TO_RUN, Master.current_ticklimit))
+#endif
+
+#undef DELTA_CALC
diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm
index 78094643ed6..736300c497d 100644
--- a/code/__HELPERS/time.dm
+++ b/code/__HELPERS/time.dm
@@ -178,20 +178,3 @@ GLOBAL_VAR_INIT(rollovercheck_last_timeofday, 0)
response += "[counter][ticks?".[ticks]" : ""] Second[counter>1 ? "s" : ""]"
return response
-//Increases delay as the server gets more overloaded,
-//as sleeps aren't cheap and sleeping only to wake up and sleep again is wasteful
-#define DELTA_CALC max(((max(world.tick_usage, world.cpu) / 100) * max(Master.sleep_delta,1)), 1)
-
-/proc/stoplag()
- if (!Master || !(Master.current_runlevel & RUNLEVELS_DEFAULT))
- sleep(world.tick_lag)
- return 1
- . = 0
- var/i = 1
- do
- . += round(i*DELTA_CALC)
- sleep(i*world.tick_lag*DELTA_CALC)
- i *= 2
- while (world.tick_usage > min(TICK_LIMIT_TO_RUN, Master.current_ticklimit))
-
-#undef DELTA_CALC
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index ce453600ff1..0c5771e1122 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -19,6 +19,12 @@
// Time in minutes before empty server will restart
/datum/config_entry/number/empty_server_restart_time
+/// Countdown between lobby and the round starting.
+/datum/config_entry/number/lobby_countdown
+ default = 120
+ integer = FALSE
+ min_val = 0
+
/****************************/
/* Client Joining & IPs */
/****************************/
@@ -447,6 +453,32 @@
min_val = 0
integer = FALSE
+/********************************************/
+/* MASTER CONTROLLER HIGH POP MODE */
+/********************************************/
+
+/datum/config_entry/number/mc_tick_rate/base_mc_tick_rate
+ integer = FALSE
+ default = 1
+
+/datum/config_entry/number/mc_tick_rate/high_pop_mc_tick_rate
+ integer = FALSE
+ default = 1.1
+
+/datum/config_entry/number/mc_tick_rate/high_pop_mc_mode_amount
+ default = 65
+
+/datum/config_entry/number/mc_tick_rate/disable_high_pop_mc_mode_amount
+ default = 60
+
+/datum/config_entry/number/mc_tick_rate
+ abstract_type = /datum/config_entry/number/mc_tick_rate
+
+/datum/config_entry/number/mc_tick_rate/ValidateAndSet(str_val)
+ . = ..()
+ if (.)
+ Master.UpdateTickRate()
+
/*****************/
/* GAME */
/*****************/
@@ -492,6 +524,25 @@
default = -1
min_val = 0
+/datum/config_entry/flag/resume_after_initializations
+
+/datum/config_entry/flag/resume_after_initializations/ValidateAndSet(str_val)
+ . = ..()
+ if(. && MC_RUNNING())
+ world.sleep_offline = !config_entry_value
+
+/*****************/
+/* PROFILING */
+/*****************/
+
+/datum/config_entry/flag/auto_profile
+
+/datum/config_entry/number/drift_dump_threshold
+ default = 4 SECONDS
+
+/datum/config_entry/number/drift_profile_delay
+ default = 15 SECONDS
+
/*****************/
/* COOLDOWNS */
/*****************/
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index 4218e95a70e..0b9c4be6d8e 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -9,7 +9,6 @@
// See initialization order in /code/game/world.dm
GLOBAL_REAL(Master, /datum/controller/master)
-
/datum/controller/master
name = "Master"
@@ -33,6 +32,8 @@ GLOBAL_REAL(Master, /datum/controller/master)
var/init_timeofday
var/init_time
var/tickdrift = 0
+ /// Tickdrift as of last tick, w no averaging going on
+ var/olddrift = 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
@@ -61,6 +62,10 @@ GLOBAL_REAL(Master, /datum/controller/master)
/// Outside of initialization, returns null.
var/current_initializing_subsystem = null
+ /// The last decisecond we force dumped profiling information
+ /// Used to avoid spamming profile reads since they can be expensive (string memes)
+ var/last_profiled = 0
+
var/static/restart_clear = 0
var/static/restart_timeout = 0
var/static/restart_count = 0
@@ -73,14 +78,24 @@ GLOBAL_REAL(Master, /datum/controller/master)
var/list/list/stage_sorted_subsystems
+ /// Whether the Overview UI will update as fast as possible for viewers.
+ var/overview_fast_update = FALSE
+ /// Enables rolling usage averaging
+ var/use_rolling_usage = FALSE
+ /// How long to run our rolling usage averaging
+ var/rolling_usage_length = 5 SECONDS
+
/datum/controller/master/New()
+ // Ensure usr is null, to prevent any potential weirdness resulting from the MC having a usr if it's manually restarted.
+ usr = null
+
if(!config)
config = new
// Highlander-style: there can only be one! Kill off the old and replace it with the new.
if(!random_seed)
#ifdef UNIT_TESTS
- random_seed = 29051994
+ random_seed = 29051994 // How about 22475?
#else
random_seed = rand(1, 1e9)
#endif
@@ -97,7 +112,7 @@ GLOBAL_REAL(Master, /datum/controller/master)
//Code used for first master on game boot or if existing master got deleted
Master = src
var/list/subsystem_types = subtypesof(/datum/controller/subsystem)
- sortTim(subsystem_types, /proc/cmp_subsystem_init)
+ sortTim(subsystem_types, GLOBAL_PROC_REF(cmp_subsystem_init))
//Find any abandoned subsystem from the previous master (if there was any)
var/list/existing_subsystems = list()
@@ -123,13 +138,121 @@ GLOBAL_REAL(Master, /datum/controller/master)
/datum/controller/master/Shutdown()
processing = FALSE
- sortTim(subsystems, /proc/cmp_subsystem_init)
+ sortTim(subsystems, GLOBAL_PROC_REF(cmp_subsystem_init))
reverseRange(subsystems)
for(var/datum/controller/subsystem/ss in subsystems)
log_world("Shutting down [ss.name] subsystem...")
+ if (ss.slept_count > 0)
+ log_world("Warning: Subsystem `[ss.name]` slept [ss.slept_count] times.")
ss.Shutdown()
log_world("Shutdown complete")
+/client/proc/cmd_controller_view_ui()
+ set category = "Debug"
+ set name = "Controller Overview"
+ set desc = "View the current states of the Subsystem Controllers."
+ if(!holder?.check_for_rights(R_SERVER|R_DEBUG))
+ return
+ Master.ui_interact(mob)
+
+/datum/controller/master/ui_status(mob/user, datum/ui_state/state)
+ if(!user.client?.holder?.check_for_rights(R_SERVER|R_DEBUG))
+ return UI_CLOSE
+ return UI_INTERACTIVE
+
+/datum/controller/master/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(isnull(ui))
+ ui = new /datum/tgui(user, src, "ControllerOverview")
+ ui.open()
+ use_rolling_usage = TRUE
+
+/datum/controller/master/ui_close(mob/user)
+ var/valid_found = FALSE
+ for(var/datum/tgui/open_ui as anything in open_uis)
+ if(open_ui.user == user)
+ continue
+ valid_found = TRUE
+ if(!valid_found)
+ use_rolling_usage = FALSE
+ return ..()
+
+/datum/controller/master/ui_data(mob/user)
+ var/list/data = list()
+
+ var/list/subsystem_data = list()
+ for(var/datum/controller/subsystem/subsystem as anything in subsystems)
+ var/list/rolling_usage = subsystem.rolling_usage
+ subsystem.prune_rolling_usage()
+
+ // Then we sum
+ var/sum = 0
+ for(var/i in 2 to length(rolling_usage) step 2)
+ sum += rolling_usage[i]
+ var/average = sum / DS2TICKS(rolling_usage_length)
+
+ subsystem_data += list(list(
+ "name" = subsystem.name,
+ "ref" = REF(subsystem),
+ "init_order" = subsystem.init_order,
+ "last_fire" = subsystem.last_fire,
+ "next_fire" = subsystem.next_fire,
+ "can_fire" = subsystem.can_fire,
+ "doesnt_fire" = !!(subsystem.flags & SS_NO_FIRE),
+ "cost_ms" = subsystem.cost,
+ "tick_usage" = subsystem.tick_usage,
+ "usage_per_tick" = average,
+ "overtime" = subsystem.tick_overrun,
+ "initialized" = subsystem.initialized,
+ "initialization_failure_message" = subsystem.initialization_failure_message,
+ ))
+ data["subsystems"] = subsystem_data
+ data["world_time"] = world.time
+ data["map_cpu"] = world.map_cpu
+ data["fast_update"] = overview_fast_update
+ data["rolling_length"] = rolling_usage_length
+
+ return data
+
+/datum/controller/master/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ if(..())
+ return TRUE
+
+ switch(action)
+ if("toggle_fast_update")
+ overview_fast_update = !overview_fast_update
+ return TRUE
+
+ if("set_rolling_length")
+ var/length = text2num(params["rolling_length"])
+ if(!length || length < 0)
+ return
+ rolling_usage_length = length SECONDS
+ return TRUE
+
+ if("view_variables")
+ var/datum/controller/subsystem/subsystem = locate(params["ref"]) in subsystems
+ if(isnull(subsystem))
+ to_chat(ui.user, span_warning("Failed to locate subsystem."))
+ return
+ ui.user.client.debug_variables(subsystem)
+ return TRUE
+
+/datum/controller/master/proc/check_and_perform_fast_update()
+ PRIVATE_PROC(TRUE)
+ set waitfor = FALSE
+
+
+ if(!overview_fast_update)
+ return
+
+ var/static/already_updating = FALSE
+ if(already_updating)
+ return
+ already_updating = TRUE
+ SStgui.update_uis(src)
+ already_updating = FALSE
+
// Returns 1 if we created a new mc, 0 if we couldn't due to a recent restart,
// -1 if we encountered a runtime trying to recreate it
/proc/Recreate_MC()
@@ -150,48 +273,50 @@ GLOBAL_REAL(Master, /datum/controller/master)
return -1
return 1
-
/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 (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"
+ var/list/master_attributes = Master.vars
+ var/list/filtered_variables = list(
+ NAMEOF(src, name),
+ NAMEOF(src, parent_type),
+ 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/lastSS = Master.last_type_processed
+ var/datum/controller/subsystem/BadBoy = Master.last_type_processed
var/FireHim = FALSE
- if(istype(lastSS))
+ if(istype(BadBoy))
msg = null
- LAZYINITLIST(lastSS.failure_strikes)
- switch(++lastSS.failure_strikes[lastSS.type])
+ LAZYINITLIST(BadBoy.failure_strikes)
+ switch(++BadBoy.failure_strikes[BadBoy.type])
if(2)
- msg = "The [lastSS.name] subsystem was the last to fire for 2 controller restarts. It will be recovered now and disabled if it happens again."
+ 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 [lastSS.name] subsystem seems to be destabilizing the MC and will be offlined."
- lastSS.flags |= SS_NO_FIRE
+ msg = "The [BadBoy.name] subsystem seems to be destabilizing the MC and will be put offline."
+ BadBoy.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 lastSS.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, 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()
@@ -216,7 +341,7 @@ GLOBAL_REAL(Master, /datum/controller/master)
stage_sorted_subsystems[i] = list()
// Sort subsystems by init_order, so they initialize in the correct order.
- sortTim(subsystems, /proc/cmp_subsystem_init)
+ sortTim(subsystems, GLOBAL_PROC_REF(cmp_subsystem_init))
for (var/datum/controller/subsystem/subsystem as anything in subsystems)
var/subsystem_init_stage = subsystem.init_stage
@@ -226,23 +351,15 @@ GLOBAL_REAL(Master, /datum/controller/master)
stage_sorted_subsystems[subsystem_init_stage] += subsystem
// Sort subsystems by display setting for easy access.
- sortTim(subsystems, /proc/cmp_subsystem_display)
+ sortTim(subsystems, GLOBAL_PROC_REF(cmp_subsystem_display))
var/start_timeofday = REALTIMEOFDAY
+
log_world("Initializing subsystems...")
for (var/current_init_stage in 1 to INITSTAGE_MAX)
-
// Initialize subsystems.
- for (var/datum/controller/subsystem/subsystem in stage_sorted_subsystems[current_init_stage])
- if (subsystem.flags & SS_NO_INIT || subsystem.initialized) //Don't init SSs with the corresponding flag or if they are already initialzized
- continue
- current_initializing_subsystem = subsystem
+ for(var/datum/controller/subsystem/subsystem in stage_sorted_subsystems[current_init_stage])
subsystem.order_in_stage = stage_sorted_subsystems[current_init_stage].Find(subsystem)
-
- rustg_time_reset(SS_INIT_TIMER_KEY)
- log_game("Initializing [subsystem.name] subsystem...")
- world.name = "[get_default_world_name(FALSE)] - [subsystem.order_string()] Initializing [subsystem.name] subsystem..."
- subsystem.Initialize()
-
+ init_subsystem(subsystem)
CHECK_TICK
current_initializing_subsystem = null
init_stage_completed = current_init_stage
@@ -253,9 +370,7 @@ GLOBAL_REAL(Master, /datum/controller/master)
// Loop.
Master.StartProcessing(0)
- var/time = (REALTIMEOFDAY - start_timeofday) / 10
-
-
+ var/time = (REALTIMEOFDAY - start_timeofday) / (1 SECONDS)
var/msg_fancy = "Initializations complete within [get_colored_thresh_text("[time] second[time == 1 ? "" : "s"]!", time, 200 SECONDS / 10)]!"
msg = "Initializations complete within [time] second[time == 1 ? "" : "s"]!"
@@ -264,23 +379,113 @@ GLOBAL_REAL(Master, /datum/controller/master)
to_chat(world, span_boldannounce(msg_fancy))
log_world(msg)
- SSplexora.serverinitdone(time)
+ clear_profiler()
- if (tgs_prime)
- world.TgsInitializationComplete()
+ // basically, most songs end around the 5 minute mark,
+ // so lets give them time to actually play. we're resetting the countdown back to default
+ // because who knows how long we took for initializations, and whatever.
+ SSticker.SetTimeLeft(CONFIG_GET(number/lobby_countdown) * 10)
+
+ SSplexora.serverinitdone(time)
// Set world options.
world.change_fps(CONFIG_GET(number/fps))
var/initialized_tod = REALTIMEOFDAY
- // if(sleep_offline_after_initializations)
- // world.sleep_offline = TRUE
- sleep(1)
+ if(tgs_prime)
+ world.TgsInitializationComplete()
+
+ if(sleep_offline_after_initializations)
+ world.sleep_offline = TRUE
+ sleep(1 TICKS)
- // if(sleep_offline_after_initializations && CONFIG_GET(flag/resume_after_initializations))
- // world.sleep_offline = FALSE
+ if(sleep_offline_after_initializations && CONFIG_GET(flag/resume_after_initializations))
+ world.sleep_offline = FALSE
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/init_subsystem(datum/controller/subsystem/subsystem)
+ var/static/list/valid_results = list(
+ SS_INIT_FAILURE,
+ SS_INIT_NONE,
+ SS_INIT_SUCCESS,
+ SS_INIT_NO_NEED,
+ )
+
+ if ((subsystem.flags & SS_NO_INIT) || subsystem.initialized) //Don't init SSs with the corresponding flag or if they already are initialized
+ subsystem.ready = TRUE // set ready to TRUE, because the value of ready may still be checked on SS_NO_INIT subsystems as an "is this ready" check
+ return
+
+ current_initializing_subsystem = subsystem
+ rustg_time_reset(SS_INIT_TIMER_KEY)
+ if(!(subsystem.flags & SS_NO_INIT_MESSAGE))
+ log_game("Initializing [subsystem.name] subsystem...")
+ world.name = "[get_default_world_name(FALSE)] - [subsystem.order_string()] Initializing [subsystem.name] subsystem..."
+
+ var/result = subsystem.Initialize()
+
+ // Capture end time
+ var/time = rustg_time_milliseconds(SS_INIT_TIMER_KEY)
+ var/seconds = round(time / 1000, 0.01)
+
+ // Always update the blackbox tally regardless.
+ SSblackbox.record_feedback("tally", "subsystem_initialize", time, subsystem.name)
+
+ // Gave invalid return value.
+ if(result && !(result in valid_results))
+ warning("[subsystem.name] subsystem initialized, returning invalid result [result]. This is a bug.")
+
+ // just returned ..() or didn't implement Initialize() at all
+ if(result == SS_INIT_NONE || !result)
+ 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(result != SS_INIT_FAILURE)
+ // Some form of success, implicit failure, or the SS in unused.
+ subsystem.initialized = TRUE
+
+ SEND_SIGNAL(subsystem, COMSIG_SUBSYSTEM_POST_INITIALIZE)
+ else
+ // The subsystem officially reports that it failed to init and wishes to be treated as such.
+ subsystem.initialized = FALSE
+ subsystem.can_fire = FALSE
+
+ if((subsystem.flags & SS_NO_INIT_MESSAGE))
+ return
+
+ var/order_string = subsystem.order_string()
+ // The rest of this proc is printing the world log and updating the splash screen.
+ var/message
+ var/message_fancy
+ var/always_show = FALSE
+
+ switch(result)
+ if(SS_INIT_FAILURE)
+ message = "\[[order_string]\] Failed to initialize [subsystem.name] subsystem after"
+ message_fancy = "\[[order_string]\] [span_bold("Failed")] to initialize [subsystem.name] subsystem after"
+ always_show = TRUE
+ if(SS_INIT_SUCCESS)
+ message = "\[[order_string]\] Initialized [subsystem.name] subsystem within"
+ message_fancy = "\[[order_string]\] Initialized [span_adminsay(subsystem.name)] subsystem within"
+ if(SS_INIT_NO_NEED)
+ // This SS is disabled or is otherwise shy.
+ pass()
+ else
+ // SS_INIT_NONE or an invalid value.
+ message = "\[[order_string]\] Initialized [subsystem.name] subsystem with errors within"
+ message_fancy = "\[[order_string]\] Initialized [span_adminsay(subsystem.name)] subsystem [span_bold("with errors")] within"
+ always_show = TRUE
+
+ if((message && !(subsystem.flags & SS_NO_INIT_MESSAGE)) || always_show)
+ message_fancy += " [get_colored_thresh_text("[seconds] second[seconds == 1 ? "" : "s"]!", seconds, subsystem.init_time_threshold / 10)]"
+ message += " [seconds] second[seconds == 1 ? "" : "s"]!"
+ to_chat(world, span_boldannounce(message_fancy))
+ if(message)
+ log_world(message)
/datum/controller/master/proc/SetRunLevel(new_runlevel)
var/old_runlevel = current_runlevel
@@ -328,8 +533,7 @@ GLOBAL_REAL(Master, /datum/controller/master)
var/list/tickersubsystems = 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
+ for (var/datum/controller/subsystem/SS as anything in subsystems)
if (SS.flags & SS_NO_FIRE)
continue
if (SS.init_stage > init_stage)
@@ -340,15 +544,25 @@ GLOBAL_REAL(Master, /datum/controller/master)
SS.state = SS_IDLE
if ((SS.flags & (SS_TICKER|SS_BACKGROUND)) == SS_TICKER)
tickersubsystems += SS
- timer += world.tick_lag * rand(1, 5)
+ // Timer subsystems aren't allowed to bunch up, so we offset them a bit
+ timer += TICKS2DS(rand(0, 1))
SS.next_fire = timer
continue
+ // Now, we have to set starting next_fires for all our new non ticker kids
+ if(SS.init_stage == init_stage - 1 && (SS.runlevels & current_runlevel))
+ // Give em a random offset so things don't clump up too bad
+ var/delay = SS.wait
+ if(SS.flags & SS_TICKER)
+ delay = TICKS2DS(delay)
+ // Gotta convert to ticks cause rand needs integers
+ SS.next_fire = world.time + TICKS2DS(rand(0, DS2TICKS(min(delay, 2 SECONDS))))
+
var/ss_runlevels = SS.runlevels
var/added_to_any = FALSE
- for(var/I in 1 to bitflags.len)
- if(ss_runlevels & bitflags[I])
- while(runlevel_sorted_subsystems.len < I)
+ for(var/I in 1 to length(GLOB.bitflags))
+ if(ss_runlevels & GLOB.bitflags[I])
+ while(length(runlevel_sorted_subsystems) < I)
runlevel_sorted_subsystems += list(list())
runlevel_sorted_subsystems[I] += SS
added_to_any = TRUE
@@ -359,9 +573,9 @@ GLOBAL_REAL(Master, /datum/controller/master)
queue_tail = null
//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)
- sortTim(tickersubsystems, /proc/cmp_subsystem_priority)
+ sortTim(tickersubsystems, GLOBAL_PROC_REF(cmp_subsystem_priority))
for(var/I in runlevel_sorted_subsystems)
- sortTim(I, /proc/cmp_subsystem_priority)
+ sortTim(I, GLOBAL_PROC_REF(cmp_subsystem_priority))
I += tickersubsystems
var/cached_runlevel = current_runlevel
@@ -380,15 +594,20 @@ GLOBAL_REAL(Master, /datum/controller/master)
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)))
+ while (TRUE)
+ var/newdrift = ((REALTIMEOFDAY - init_timeofday) - (world.time - init_time)) / world.tick_lag
+ tickdrift = max(0, MC_AVERAGE_FAST(tickdrift, newdrift))
var/starting_tick_usage = TICK_USAGE
+ if(newdrift - olddrift >= CONFIG_GET(number/drift_dump_threshold))
+ AttemptProfileDump(CONFIG_GET(number/drift_profile_delay))
+ olddrift = newdrift
+
if (init_stage != init_stage_completed)
return MC_LOOP_RTN_NEWSTAGES
if (processing <= 0)
current_ticklimit = TICK_LIMIT_RUNNING
- sleep(10)
+ sleep(1 SECONDS)
continue
//Anti-tick-contention heuristics:
@@ -425,14 +644,20 @@ GLOBAL_REAL(Master, /datum/controller/master)
var/checking_runlevel = current_runlevel
if(cached_runlevel != checking_runlevel)
//resechedule subsystems
+ var/list/old_subsystems = current_runlevel_subsystems
cached_runlevel = checking_runlevel
current_runlevel_subsystems = runlevel_sorted_subsystems[cached_runlevel]
- var/stagger = world.time
- for(var/I in current_runlevel_subsystems)
- var/datum/controller/subsystem/SS = I
- if(SS.next_fire <= world.time)
- stagger += world.tick_lag * rand(1, 5)
- SS.next_fire = stagger
+
+ //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))
+ continue
+ // If they're new, give em a random offset so things don't clump up too bad
+ var/delay = SS.wait
+ if(SS.flags & SS_TICKER)
+ delay = TICKS2DS(delay)
+ SS.next_fire = world.time + TICKS2DS(rand(0, DS2TICKS(min(delay, 2 SECONDS))))
subsystems_to_check = current_runlevel_subsystems
else
@@ -502,11 +727,9 @@ GLOBAL_REAL(Master, /datum/controller/master)
if (processing * sleep_delta <= world.tick_lag)
current_ticklimit -= (TICK_LIMIT_RUNNING * 0.25) //reserve the tail 1/4 of the next tick for the mc if we plan on running next tick
+ check_and_perform_fast_update()
sleep(world.tick_lag * (processing * sleep_delta))
-
-
-
// This is what decides if something should run.
/datum/controller/master/proc/CheckQueue(list/subsystemstocheck)
. = 0 //so the mc knows if we runtimed
@@ -529,12 +752,25 @@ GLOBAL_REAL(Master, /datum/controller/master)
if (SS_flags & SS_NO_FIRE)
subsystemstocheck -= SS
continue
+ // If we're keeping timing and running behind,
+ // fire at most 25% faster then normal to try and make up the gap without spamming
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
+ if(SS_flags & SS_HIBERNATE)
+ var/list/check_vars = SS.hibernate_checks
+ var/enter_queue
+ for(var/i in 1 to length(check_vars))
+ if(LAZYLEN(SS.vars[check_vars[i]]))
+ enter_queue = TRUE
+ break
+ if(!enter_queue)
+ SS.hibernation_state = SS_IS_HIBERNATING
+ SS.update_nextfire()
+ continue
SS.enqueue()
. = 1
@@ -608,10 +844,22 @@ GLOBAL_REAL(Master, /datum/controller/master)
queue_node.state = SS_RUNNING
+ if(queue_node.profiler_focused)
+ world.Profile(PROFILE_START)
+
tick_usage = TICK_USAGE
var/state = queue_node.ignite(queue_node_paused)
tick_usage = TICK_USAGE - tick_usage
+ if(use_rolling_usage)
+ queue_node.prune_rolling_usage()
+ // Rolling usage is an unrolled list that we know the order off
+ // OPTIMIZATION POSTING
+ queue_node.rolling_usage += list(DS2TICKS(world.time), tick_usage)
+
+ if(queue_node.profiler_focused)
+ world.Profile(PROFILE_STOP)
+
if (state == SS_RUNNING)
state = SS_IDLE
current_tick_budget -= queue_node_priority
@@ -700,10 +948,17 @@ GLOBAL_REAL(Master, /datum/controller/master)
log_world("MC: SoftReset: Finished.")
. = 1
+/*
/// Warns us that the end of tick byond map_update will be laggier then normal, so that we can just skip running subsystems this tick.
/datum/controller/master/proc/laggy_byond_map_update_incoming()
if (!skip_ticks)
skip_ticks = 1
+*/
+
+/datum/controller/master/stat_entry(msg)
+ msg = "(TickRate:[Master.processing]) (Iteration:[Master.iteration]) (TickLimit: [round(Master.current_ticklimit, 0.1)])"
+ return msg
+
/datum/controller/master/StartLoadingMap()
//disallow more than one map to load at once, multithreading it will just cause race conditions
@@ -724,13 +979,26 @@ GLOBAL_REAL(Master, /datum/controller/master)
/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)
+ 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()
+
+/// Attempts to dump our current profile info into a file, triggered if the MC thinks shit is going down
+/// Accepts a delay in deciseconds of how long ago our last dump can be, this saves causing performance problems ourselves
+/datum/controller/master/proc/AttemptProfileDump(delay)
+ if(REALTIMEOFDAY - last_profiled <= delay)
+ return FALSE
+ last_profiled = REALTIMEOFDAY
+ SSprofiler.DumpFile(allow_yield = FALSE)
+
+/// Clears the profiler.
+/datum/controller/master/proc/clear_profiler()
+ world.Profile(PROFILE_CLEAR)
+ world.Profile(PROFILE_CLEAR, type = "sendmaps")
diff --git a/code/controllers/profiler.dm b/code/controllers/profiler.dm
new file mode 100644
index 00000000000..98cd4b06f08
--- /dev/null
+++ b/code/controllers/profiler.dm
@@ -0,0 +1,66 @@
+SUBSYSTEM_DEF(profiler)
+ name = "Profiler"
+ init_order = INIT_ORDER_PROFILER
+ runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
+ wait = 300 SECONDS
+ var/fetch_cost = 0
+ var/write_cost = 0
+
+/datum/controller/subsystem/profiler/Initialize()
+ if(CONFIG_GET(flag/auto_profile))
+ StartProfiling()
+ else
+ StopProfiling() //Stop the early start profiler
+ return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/profiler/OnConfigLoad()
+ if(CONFIG_GET(flag/auto_profile))
+ StartProfiling()
+ can_fire = TRUE
+ else
+ StopProfiling()
+ can_fire = FALSE
+
+/datum/controller/subsystem/profiler/fire()
+ DumpFile()
+
+/datum/controller/subsystem/profiler/Shutdown()
+ if(CONFIG_GET(flag/auto_profile))
+ DumpFile(allow_yield = FALSE)
+ world.Profile(PROFILE_CLEAR, type = "sendmaps")
+ return ..()
+
+/datum/controller/subsystem/profiler/stat_entry(msg)
+ msg += "F:[round(fetch_cost, 1)]ms"
+ msg += "|W:[round(write_cost, 1)]ms"
+ return msg
+
+/datum/controller/subsystem/profiler/proc/StartProfiling()
+ world.Profile(PROFILE_START)
+ world.Profile(PROFILE_START, type = "sendmaps")
+
+/datum/controller/subsystem/profiler/proc/StopProfiling()
+ world.Profile(PROFILE_STOP)
+ world.Profile(PROFILE_STOP, type = "sendmaps")
+
+/datum/controller/subsystem/profiler/proc/DumpFile(allow_yield = TRUE)
+ var/timer = TICK_USAGE_REAL
+ var/current_profile_data = world.Profile(PROFILE_REFRESH, format = "json")
+ var/current_sendmaps_data = world.Profile(PROFILE_REFRESH, type = "sendmaps", format = "json")
+ fetch_cost = MC_AVERAGE(fetch_cost, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))
+ if(allow_yield)
+ CHECK_TICK
+
+ if(!length(current_profile_data)) //Would be nice to have explicit proc to check this
+ stack_trace("Warning, profiling stopped manually before dump.")
+
+ var/timestamp = time2text(world.timeofday, "YYYY-MM-DD_hh-mm-ss")
+ var/prof_file = "[GLOB.log_directory]/profiler/profiler-[timestamp].json"
+ if(!length(current_sendmaps_data)) //Would be nice to have explicit proc to check this
+ stack_trace("Warning, sendmaps profiling stopped manually before dump.")
+ var/sendmaps_file = "[GLOB.log_directory]/profiler/sendmaps-[timestamp].json"
+
+ timer = TICK_USAGE_REAL
+ rustg_file_write(current_profile_data, prof_file)
+ rustg_file_write(current_sendmaps_data, sendmaps_file)
+ write_cost = MC_AVERAGE(write_cost, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))
diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm
index 5f4579bd209..cf258d8bb3b 100644
--- a/code/controllers/subsystem.dm
+++ b/code/controllers/subsystem.dm
@@ -28,9 +28,12 @@
/// 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.
+ /// This var is set to `INITIALIZATION_INNEW_REGULAR` after the subsystem has been initialized.
var/initialized = FALSE
+ /// Similar to above however this will be set even if an SS has SS_NO_INIT set.
+ var/ready = 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
var/can_fire = TRUE
@@ -38,6 +41,18 @@
///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
+ ///A list of var names present on this subsystem to be checked during CheckQueue. See [SS_HIBERNATE] for usage.
+ var/list/hibernate_checks
+
+ ///Subsystem ID. Used for when we need a technical name for the SS used by SSmetrics
+ var/ss_id = "generic_ss_id"
+
+ /**
+ * boolean set by admins. if TRUE then this subsystem will stop the world profiler after ignite() returns and start it again when called.
+ * used so that you can audit a specific subsystem or group of subsystems' synchronous call chain.
+ */
+ var/profiler_focused = FALSE
+
/*
* The following variables are managed by the MC and should not be modified directly.
*/
@@ -48,6 +63,9 @@
/// Scheduled world.time for next fire()
var/next_fire = 0
+ /// The subsystem had no work during CheckQueue and was not queued.
+ var/hibernation_state
+
/// 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
@@ -57,6 +75,9 @@
/// 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
+ /// Flat list of usage and time, every odd index is a log time, every even index is a usage
+ var/list/rolling_usage = list()
+
/// How much of a tick (in percents of a tick) were we allocated last fire.
var/tick_allocation_last = 0
@@ -66,6 +87,9 @@
/// 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 times a subsystem has ever slept in fire().
+ var/slept_count = 0
+
/// Tracks how many fires the subsystem has consecutively paused on in the current run
var/paused_ticks = 0
@@ -101,6 +125,9 @@
/// Index of this SS in the stages, Set by master.
var/order_in_stage
+ /// String to store an applicable error message for a subsystem crashing, used to help debug crashes in contexts such as Continuous Integration/Unit Tests
+ var/initialization_failure_message = null
+
//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.
@@ -127,8 +154,10 @@
fire(resumed)
. = state
if (state == SS_SLEEPING)
+ slept_count++
state = SS_IDLE
if (state == SS_PAUSING)
+ slept_count++
var/QT = queued_time
enqueue()
state = SS_PAUSED
@@ -178,6 +207,8 @@
/// (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()
+ hibernation_state = hibernation_state == SS_IS_HIBERNATING ? SS_WAKING_UP : SS_NOT_HIBERNATING
+
var/SS_priority = priority
var/SS_flags = flags
var/datum/controller/subsystem/queue_node
@@ -260,49 +291,26 @@
/// Called after the config has been loaded or reloaded.
/datum/controller/subsystem/proc/OnConfigLoad()
-//used to initialize the subsystem AFTER the map has loaded
+/**
+ * Used to initialize the subsystem. This is expected to be overriden by subtypes.
+ */
/datum/controller/subsystem/Initialize()
- initialized = TRUE
-
- // SEND_SIGNAL_OLD(src, COMSIG_SUBSYSTEM_POST_INITIALIZE)
-
- #ifndef OPENDREAM
- var/static/no_memstat = FALSE
- if(!no_memstat)
- try
- if(!rustg_file_exists(MEMORYSTATS_DLL_PATH))
- no_memstat = TRUE
- else
- var/memory_summary = trimtext(replacetext(call_ext(MEMORYSTATS_DLL_PATH, "memory_stats")(), "Server mem usage:", ""))
- if(memory_summary)
- rustg_file_append("=== [src.name] ===\n[memory_summary]\n", "[GLOB.log_directory]/profiler/memstat-init.txt")
- else
- no_memstat = TRUE
- catch
- no_memstat = TRUE
- #endif
-
- var/time = rustg_time_milliseconds(SS_INIT_TIMER_KEY)
- var/seconds = round(time / 1000, 0.01)
- var/order_string = order_string()
- var/msg_fancy = "\[[order_string]\] Initialized [span_adminsay(name)] subsystem within [get_colored_thresh_text("[seconds] second[seconds == 1 ? "" : "s"]!", seconds, init_time_threshold / 10)]"
- var/msg = "\[[order_string]\] Initialized [name] subsystem within [seconds] second[seconds == 1 ? "" : "s"]!"
- to_chat(world, span_boldannounce(msg_fancy))
- log_world(msg)
- return seconds
+ return SS_INIT_NONE
/datum/controller/subsystem/proc/order_string(include_stage = TRUE)
- return "[include_stage ? "S[init_stage]-" : ""][order_in_stage <= 9 ? "0" : ""][order_in_stage]/[Master.stage_sorted_subsystems[init_stage].len]"
+ return "[include_stage ? "S[init_stage]-" : ""][order_in_stage <= 9 ? "0" : ""][order_in_stage]/[length(Master.stage_sorted_subsystems[init_stage])]"
/datum/controller/subsystem/stat_entry(msg)
if(can_fire && !(SS_NO_FIRE & flags) && init_stage <= Master.init_stage_completed)
msg = "[round(cost,1)]ms|[round(tick_usage,1)]%([round(tick_overrun,1)]%)|[round(ticks,0.1)]\t[msg]"
else
msg = "OFFLINE\t[msg]"
-
return msg
/datum/controller/subsystem/proc/state_letter()
+ if(hibernation_state)
+ return hibernation_state == SS_WAKING_UP ? "W" : "H"
+
switch (state)
if (SS_RUNNING)
. = "R"
@@ -320,6 +328,15 @@
if (can_fire && cycles >= 1)
postponed_fires += cycles
+/// Prunes out of date entries in our rolling usage list
+/datum/controller/subsystem/proc/prune_rolling_usage()
+ var/list/rolling_usage = src.rolling_usage
+ var/cut_to = 0
+ while(cut_to + 2 <= length(rolling_usage) && rolling_usage[cut_to + 1] < DS2TICKS(world.time - Master.rolling_usage_length))
+ cut_to += 2
+ if(cut_to)
+ rolling_usage.Cut(1, cut_to + 1)
+
//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()
@@ -333,3 +350,54 @@
if (NAMEOF(src, queued_priority)) //editing this breaks things.
return FALSE
. = ..()
+ if(!.)
+ return
+ switch(var_name)
+ if (NAMEOF(src, wait), NAMEOF(src, flags))
+ update_nextfire(reset_time = TRUE)
+
+/**
+ * This allows subsystems to change their execution priority at runtime without
+ * affecting the existing queue logic or requiring a full MC restart.
+ *
+ * enable_background (bool) - TRUE to enable SS_BACKGROUND mode, FALSE to disable it
+ *
+ * Returns TRUE if the mode was successfully changed, FALSE otherwise
+ *
+ * NOTE: Only works on subsystems with SS_DYNAMIC for sanity reasons, unless `force` is set.
+ */
+/datum/controller/subsystem/proc/set_background_mode(enable_background = TRUE, force = FALSE)
+ if (!(flags & SS_DYNAMIC) && !force)
+ return FALSE
+
+ // Check if we're actually changing the state
+ var/currently_background = !!(flags & SS_BACKGROUND)
+ var/target_background = enable_background
+
+ if (currently_background == target_background)
+ return FALSE
+
+ // Store the old flag state for priority count adjustments
+ var/was_queued = (state == SS_QUEUED)
+ var/old_flags = flags
+
+ if (enable_background)
+ flags |= SS_BACKGROUND
+ else
+ flags &= ~SS_BACKGROUND
+
+ // If the subsystem is currently queued, we need to:
+ // 1. Update the priority counts
+ // 2. Requeue to maintain proper sort order
+ if (was_queued)
+ // Adjust the master controller's priority counts
+ if (old_flags & SS_BACKGROUND)
+ Master.queue_priority_count_bg -= queued_priority
+ else
+ Master.queue_priority_count -= queued_priority
+
+ // Remove then re-add
+ dequeue()
+ enqueue()
+
+ return TRUE
diff --git a/code/controllers/subsystems/air.dm b/code/controllers/subsystems/air.dm
index 73b1ada92cf..2a087c14d90 100644
--- a/code/controllers/subsystems/air.dm
+++ b/code/controllers/subsystems/air.dm
@@ -1,14 +1,3 @@
-#define SSAIR_PIPENETS 1
-#define SSAIR_ATMOSMACHINERY 2
-#define SSAIR_TILES_CUR 3
-#define SSAIR_TILES_DEF 4
-#define SSAIR_EDGES 5
-#define SSAIR_FIRE_ZONES 6
-#define SSAIR_HOTSPOTS 7
-#define SSAIR_ZONES 8
-
-#define SSAIR_TICK_MULTIPLIER 2
-
SUBSYSTEM_DEF(air)
name = "Air"
@@ -80,7 +69,7 @@ SUBSYSTEM_DEF(air)
setup_allturfs()
setup_atmos_machinery()
setup_pipenets()
- ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/air/fire(resumed = 0)
var/timer = world.tick_usage
@@ -521,11 +510,3 @@ SUBSYSTEM_DEF(air)
return TRUE
-#undef SSAIR_PIPENETS
-#undef SSAIR_ATMOSMACHINERY
-#undef SSAIR_TILES_CUR
-#undef SSAIR_TILES_DEF
-#undef SSAIR_EDGES
-#undef SSAIR_FIRE_ZONES
-#undef SSAIR_HOTSPOTS
-#undef SSAIR_ZONES
diff --git a/code/controllers/subsystems/alarm.dm b/code/controllers/subsystems/alarm.dm
index 1c362e547e6..7603956afa9 100644
--- a/code/controllers/subsystems/alarm.dm
+++ b/code/controllers/subsystems/alarm.dm
@@ -26,7 +26,7 @@ SUBSYSTEM_DEF(alarm)
/datum/controller/subsystem/alarm/Initialize(start_timeofday)
all_handlers = list(atmosphere_alarm, camera_alarm, fire_alarm, motion_alarm, power_alarm)
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/alarm/fire(resumed = FALSE)
if (!resumed)
diff --git a/code/controllers/subsystems/assets.dm b/code/controllers/subsystems/assets.dm
index 29a1d6b3f7c..8336c60c0fa 100644
--- a/code/controllers/subsystems/assets.dm
+++ b/code/controllers/subsystems/assets.dm
@@ -30,7 +30,7 @@ SUBSYSTEM_DEF(assets)
transport.Initialize(cache)
- ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/assets/Recover()
cache = SSassets.cache
diff --git a/code/controllers/subsystems/atoms.dm b/code/controllers/subsystems/atoms.dm
index 51199ac1adc..c78a12f1da2 100644
--- a/code/controllers/subsystems/atoms.dm
+++ b/code/controllers/subsystems/atoms.dm
@@ -30,7 +30,7 @@ SUBSYSTEM_DEF(atoms)
initialized = INITIALIZATION_INNEW_MAPLOAD
InitializeAtoms()
initialized = INITIALIZATION_INNEW_REGULAR
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/atoms/proc/InitializeAtoms(list/atoms, list/atoms_to_return = null)
if(initialized == INITIALIZATION_INSSATOMS)
diff --git a/code/controllers/subsystems/ban_cache.dm b/code/controllers/subsystems/ban_cache.dm
index a0ba145c8ad..2d10baecab3 100644
--- a/code/controllers/subsystems/ban_cache.dm
+++ b/code/controllers/subsystems/ban_cache.dm
@@ -10,7 +10,7 @@ SUBSYSTEM_DEF(ban_cache)
/datum/controller/subsystem/ban_cache/Initialize()
generate_queries()
- return ..()
+ return SS_INIT_SUCCESS
/// Generates ban caches for any logged in clients. This ensures the amount of in-series ban checking we have to do that actually involves sleeps is VERY low
/datum/controller/subsystem/ban_cache/proc/generate_queries()
diff --git a/code/controllers/subsystems/blackbox.dm b/code/controllers/subsystems/blackbox.dm
index 894a1536dd0..566f6048fdc 100644
--- a/code/controllers/subsystems/blackbox.dm
+++ b/code/controllers/subsystems/blackbox.dm
@@ -23,7 +23,7 @@ SUBSYSTEM_DEF(blackbox)
record_feedback("amount", "dm_build", DM_BUILD)
record_feedback("amount", "byond_version", world.byond_version)
record_feedback("amount", "byond_build", world.byond_build)
- return ..()
+ return SS_INIT_SUCCESS
//poll population
/datum/controller/subsystem/blackbox/fire()
diff --git a/code/controllers/subsystems/chat.dm b/code/controllers/subsystems/chat.dm
index 1262691b817..e1725424edb 100644
--- a/code/controllers/subsystems/chat.dm
+++ b/code/controllers/subsystems/chat.dm
@@ -18,9 +18,13 @@ SUBSYSTEM_DEF(chat)
/datum/controller/subsystem/chat/Initialize()
- . = ..()
- initialize_text_to_speech()
-
+ var/load_result = initialize_text_to_speech()
+ if(isnull(load_result))
+ return SS_INIT_NO_NEED
+ if(!load_result)
+ log_world("TTS config has a config token but NO seeds!")
+ return SS_INIT_FAILURE
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/chat/proc/generate_payload(client/target, message_data)
var/sequence = client_to_sequence_number[target.ckey]
diff --git a/code/controllers/subsystems/chemistry.dm b/code/controllers/subsystems/chemistry.dm
index ab5f7f2c549..a19bc968d6f 100644
--- a/code/controllers/subsystems/chemistry.dm
+++ b/code/controllers/subsystems/chemistry.dm
@@ -9,7 +9,8 @@ SUBSYSTEM_DEF(chemistry)
/datum/controller/subsystem/chemistry/Initialize(start_timeofday)
chemical_reactions = GLOB.chemical_reactions_list
chemical_reagents = GLOB.chemical_reagents_list
- return ..()
+ return SS_INIT_SUCCESS
+
/datum/controller/subsystem/chemistry/fire()
for(var/thing in active_holders)
diff --git a/code/controllers/subsystems/chunks.dm b/code/controllers/subsystems/chunks.dm
index 78d9a5ff42c..450a0c39eae 100644
--- a/code/controllers/subsystems/chunks.dm
+++ b/code/controllers/subsystems/chunks.dm
@@ -27,7 +27,7 @@ SUBSYSTEM_DEF(chunks)
chunk_list_by_zlevel[i][j] = new /datum/chunk(src)
RegisterSignal(SSdcs, COMSIG_MOB_INITIALIZED, PROC_REF(onMobNew))
RegisterSignal(SSdcs, COMSIG_WORLD_MAXZ_INCREMENTING, PROC_REF(beforeLevelIncrement))
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/chunks/proc/beforeLevelIncrement(datum/source)
SIGNAL_HANDLER
diff --git a/code/controllers/subsystems/craft.dm b/code/controllers/subsystems/craft.dm
index a5169553ccb..33dbdc344e7 100644
--- a/code/controllers/subsystems/craft.dm
+++ b/code/controllers/subsystems/craft.dm
@@ -11,19 +11,17 @@ SUBSYSTEM_DEF(craft)
/datum/controller/subsystem/craft/Initialize(timeofday)
categories = list()
cat_names = list()
- for(var/path in subtypesof(/datum/craft_recipe))
- var/datum/craft_recipe/CR = path
- if(!initial(CR.name))
+ for(var/datum/craft_recipe/CR as anything in subtypesof(/datum/craft_recipe))
+ if(!initial(CR?.name))
continue
CR = new CR
cat_names |= CR.category
- if(!CR.steps.len)
+ if(!length(CR.steps))
if(CR.name)
- world.log << "ERROR: empty steps for craft recipe [CR.type]"
+ log_world("ERROR: empty steps for craft recipe [CR.type]")
qdel(CR)
if(!(CR.category in categories))
categories[CR.category] = list()
categories[CR.category] += CR
- CHECK_TICK
- return ..()
+ return SS_INIT_SUCCESS
diff --git a/code/controllers/subsystems/dbcore.dm b/code/controllers/subsystems/dbcore.dm
index 592d754b2cb..41784527b7f 100644
--- a/code/controllers/subsystems/dbcore.dm
+++ b/code/controllers/subsystems/dbcore.dm
@@ -57,7 +57,8 @@ SUBSYSTEM_DEF(dbcore)
if(2)
message_admins("Could not get schema version from database")
- return ..()
+ return SS_INIT_SUCCESS
+
/datum/controller/subsystem/dbcore/OnConfigLoad()
. = ..()
diff --git a/code/controllers/subsystems/economy.dm b/code/controllers/subsystems/economy.dm
index 334d6b90d79..5ec81b5db7b 100644
--- a/code/controllers/subsystems/economy.dm
+++ b/code/controllers/subsystems/economy.dm
@@ -5,15 +5,12 @@
*/
SUBSYSTEM_DEF(economy)
name = "Economy"
- init_order = INIT_ORDER_LATELOAD
+ flags = SS_NO_INIT
wait = 300 //Ticks once per 30 seconds
var/payday_interval = 30 MINUTES
var/next_payday = 30 MINUTES
-/datum/controller/subsystem/economy/Initialize()
- .=..()
-
/datum/controller/subsystem/economy/fire()
if (world.time >= next_payday)
next_payday = world.time + payday_interval
diff --git a/code/controllers/subsystems/evac.dm b/code/controllers/subsystems/evac.dm
index 27317d5774c..17eb3ab1d6d 100644
--- a/code/controllers/subsystems/evac.dm
+++ b/code/controllers/subsystems/evac.dm
@@ -8,7 +8,7 @@ SUBSYSTEM_DEF(evac)
/datum/controller/subsystem/evac/Initialize(start_timeofday)
evacuation_controller = new /datum/evacuation_controller/starship()
evacuation_controller.set_up()
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/evac/fire()
evacuation_controller.Process()
diff --git a/code/controllers/subsystems/event.dm b/code/controllers/subsystems/event.dm
index 5d21f2a1d9f..eb793bdd119 100644
--- a/code/controllers/subsystems/event.dm
+++ b/code/controllers/subsystems/event.dm
@@ -27,7 +27,7 @@ SUBSYSTEM_DEF(event)
if(!all_events)
all_events = subtypesof(/datum/event)
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/event/Recover()
active_events = SSevent.active_events
diff --git a/code/controllers/subsystems/explosions.dm b/code/controllers/subsystems/explosions.dm
index 52690828ce6..b80c78a67ea 100644
--- a/code/controllers/subsystems/explosions.dm
+++ b/code/controllers/subsystems/explosions.dm
@@ -66,7 +66,7 @@ SUBSYSTEM_DEF(explosions)
available_hash_lists = new /list(SPARE_HASH_LISTS)
for(var/i = 1,i <= SPARE_HASH_LISTS,i++)
available_hash_lists[i] = new /list(HASH_MODULO)
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/explosions/proc/retrieveHashList()
var/i = length(SSexplosions.available_hash_lists) + 1
diff --git a/code/controllers/subsystems/holomaps.dm b/code/controllers/subsystems/holomaps.dm
index 0ea35f89282..78ef3e930a0 100644
--- a/code/controllers/subsystems/holomaps.dm
+++ b/code/controllers/subsystems/holomaps.dm
@@ -16,7 +16,8 @@ SUBSYSTEM_DEF(holomaps)
/datum/controller/subsystem/holomaps/Initialize(timeofday)
generateHoloMinimaps()
- . = ..()
+ return SS_INIT_SUCCESS
+
/datum/controller/subsystem/holomaps/stat_entry(msg)
if (!GLOB.Debug2)
diff --git a/code/controllers/subsystems/initialization/character_setup.dm b/code/controllers/subsystems/initialization/character_setup.dm
index 885a6fdd95c..0fdd120971f 100644
--- a/code/controllers/subsystems/initialization/character_setup.dm
+++ b/code/controllers/subsystems/initialization/character_setup.dm
@@ -22,4 +22,4 @@ SUBSYSTEM_DEF(character_setup)
error("Prefs failed to setup (SS): [prefs.client_ckey]")
prefs.setup()
- . = ..()
+ return SS_INIT_SUCCESS
diff --git a/code/controllers/subsystems/inventory.dm b/code/controllers/subsystems/inventory.dm
index b6553605891..4c7e25cfe9f 100644
--- a/code/controllers/subsystems/inventory.dm
+++ b/code/controllers/subsystems/inventory.dm
@@ -11,13 +11,13 @@ SUBSYSTEM_DEF(inventory)
if(!initial(S.id))
continue
S = new S()
- if(S.id > slots.len)
+ if(S.id > length(slots))
slots.len = S.id
slots[S.id] = S
- . = ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/inventory/proc/get_slot_datum(slot)
- return slots.len >= slot ? slots[slot] : null
+ return length(slots) >= slot ? slots[slot] : null
/datum/controller/subsystem/inventory/proc/update_mob(mob/living/target, slot, redraw)
var/datum/inventory_slot/IS = get_slot_datum(slot)
diff --git a/code/controllers/subsystems/jamming.dm b/code/controllers/subsystems/jamming.dm
index 00a76230d6b..c867473a06e 100644
--- a/code/controllers/subsystems/jamming.dm
+++ b/code/controllers/subsystems/jamming.dm
@@ -11,7 +11,7 @@ SUBSYSTEM_DEF(jamming)
active_jammers = new /list(world.maxz)
for(var/i = 1; i < world.maxz; i++)
active_jammers[i] = list()
- . = ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/jamming/proc/IsPositionJammed(turf/location, signalStrength)
for(var/thing in active_jammers[location.z])
diff --git a/code/controllers/subsystems/jobs.dm b/code/controllers/subsystems/jobs.dm
index 339d70e9b34..41a4ab09c8d 100644
--- a/code/controllers/subsystems/jobs.dm
+++ b/code/controllers/subsystems/jobs.dm
@@ -70,7 +70,7 @@ SUBSYSTEM_DEF(job)
if(!length(occupations))
SetupOccupations()
LoadJobs("config/jobs.txt")
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/job/fire(resumed)
for(var/key in queries_by_key)
diff --git a/code/controllers/subsystems/language.dm b/code/controllers/subsystems/language.dm
index d5aee060854..042f9c261fb 100644
--- a/code/controllers/subsystems/language.dm
+++ b/code/controllers/subsystems/language.dm
@@ -16,4 +16,5 @@ SUBSYSTEM_DEF(language)
GLOB.language_keys[lowertext(L.key)] = L
GLOB.language_types_by_name[initial(L.name)] = L.type
- . = ..()
+ return SS_INIT_SUCCESS
+
diff --git a/code/controllers/subsystems/library.dm b/code/controllers/subsystems/library.dm
index cf1984b69b6..97f6aa2f699 100644
--- a/code/controllers/subsystems/library.dm
+++ b/code/controllers/subsystems/library.dm
@@ -22,7 +22,7 @@ SUBSYSTEM_DEF(library)
prepare_official_posters()
prepare_library_areas()
load_shelves()
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/library/proc/load_shelves()
var/list/datum/callback/load_callbacks = list()
diff --git a/code/controllers/subsystems/lighting.dm b/code/controllers/subsystems/lighting.dm
index 42b6610ba32..f3810e93e6e 100644
--- a/code/controllers/subsystems/lighting.dm
+++ b/code/controllers/subsystems/lighting.dm
@@ -24,7 +24,8 @@ SUBSYSTEM_DEF(lighting)
/datum/controller/subsystem/lighting/Initialize(timeofday)
create_all_lighting_overlays()
- . = ..()
+ return SS_INIT_SUCCESS
+
/datum/controller/subsystem/lighting/fire(resumed=FALSE)
if (resuming_stage == 0 || !resumed)
diff --git a/code/controllers/subsystems/liquids/effect.dm b/code/controllers/subsystems/liquids/effect.dm
index 4ef38abd352..ddd87bb4580 100644
--- a/code/controllers/subsystems/liquids/effect.dm
+++ b/code/controllers/subsystems/liquids/effect.dm
@@ -68,7 +68,6 @@
//. += mutable_appearance(icon, icon_state, plane = EMISSIVE_PLANE)
/obj/effect/abstract/liquid_turf/Initialize(mapload, datum/liquid_group/group_to_add)
- . = ..()
if(!small_fire)
small_fire = new
if(!medium_fire)
@@ -107,6 +106,7 @@
if(!turf.liquids)
continue
turf.liquids.update_icon()
+ return SS_INIT_SUCCESS
/obj/effect/abstract/liquid_turf/Destroy(force)
GLOB.entered_event.unregister(my_turf, src, /obj/effect/abstract/liquid_turf/proc/movable_entered)
diff --git a/code/controllers/subsystems/machines.dm b/code/controllers/subsystems/machines.dm
index da6fa71c3c3..bea5e85cdef 100644
--- a/code/controllers/subsystems/machines.dm
+++ b/code/controllers/subsystems/machines.dm
@@ -11,7 +11,8 @@ SUBSYSTEM_DEF(machines)
/datum/controller/subsystem/machines/Initialize()
makepowernets()
fire()
- return ..()
+ return SS_INIT_SUCCESS
+
/datum/temp_counter_debug
var/associated_typepath = null
diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm
index 17e84e11ee6..db8347d1779 100644
--- a/code/controllers/subsystems/mapping.dm
+++ b/code/controllers/subsystems/mapping.dm
@@ -67,7 +67,7 @@ SUBSYSTEM_DEF(mapping)
sortAssoc(ghostteleportlocs)
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/mapping/proc/build_pulsar()
world.incrementMaxZ()
diff --git a/code/controllers/subsystems/memory_stats.dm b/code/controllers/subsystems/memory_stats.dm
index 25890c4712a..6065b588510 100644
--- a/code/controllers/subsystems/memory_stats.dm
+++ b/code/controllers/subsystems/memory_stats.dm
@@ -9,9 +9,9 @@ SUBSYSTEM_DEF(memory_stats)
/datum/controller/subsystem/memory_stats/Initialize()
if(!rustg_file_exists(MEMORYSTATS_DLL_PATH))
flags |= SS_NO_FIRE
- return ..()
+ return SS_INIT_NO_NEED
fire()
- return ..()
+ return SS_INIT_SUCCESS
// TODO: Figure out why this still being fired even if it doesnt exist. onlinx
/datum/controller/subsystem/memory_stats/fire(resumed)
diff --git a/code/controllers/subsystems/migration.dm b/code/controllers/subsystems/migration.dm
index 20bd0f0d6b4..7e88d59ca16 100755
--- a/code/controllers/subsystems/migration.dm
+++ b/code/controllers/subsystems/migration.dm
@@ -42,11 +42,11 @@ SUBSYSTEM_DEF(migration)
On initialize, the migration system generates a large number of burrows spread across the ship
*/
/datum/controller/subsystem/migration/Initialize()
- . = ..()
for (var/i = 0; i < roundstart_burrows; i++)
var/area/A = random_ship_area(FALSE, FALSE, FALSE)
var/turf/T = A.random_space() //Lets make sure the selected area is valid
create_burrow(T)
+ return SS_INIT_SUCCESS
diff --git a/code/controllers/subsystems/misc.dm b/code/controllers/subsystems/misc.dm
index cf6e9d87d94..ee768251e61 100644
--- a/code/controllers/subsystems/misc.dm
+++ b/code/controllers/subsystems/misc.dm
@@ -12,7 +12,7 @@ SUBSYSTEM_DEF(misc)
initialize_cursors()
// build_exoplanets() // 18/09/2022 Commented till lag gets better overhaul
build_junk_field()
- return ..()
+ return SS_INIT_SUCCESS
GLOBAL_LIST_INIT(cursor_icons, list()) //list of icon files, which point to lists of offsets, which point to icons
diff --git a/code/controllers/subsystems/ping.dm b/code/controllers/subsystems/ping.dm
index d69803eca32..efe9040d66f 100644
--- a/code/controllers/subsystems/ping.dm
+++ b/code/controllers/subsystems/ping.dm
@@ -11,7 +11,6 @@ SUBSYSTEM_DEF(ping)
msg += "P:[LAZYLEN(GLOB.clients)]"
return ..()
-
/datum/controller/subsystem/ping/fire(resumed = FALSE)
// Prepare the new batch of clients
if (!resumed)
diff --git a/code/controllers/subsystems/plants.dm b/code/controllers/subsystems/plants.dm
index c82aee0e1bd..a1e0e94101d 100644
--- a/code/controllers/subsystems/plants.dm
+++ b/code/controllers/subsystems/plants.dm
@@ -97,7 +97,7 @@ SUBSYSTEM_DEF(plants)
used_masks += gene_mask
plant_traits -= gene_tag
gene_tag_masks[gene_tag] = gene_mask
- ..()
+ return SS_INIT_SUCCESS
// Proc for creating a random seed type.
/datum/controller/subsystem/plants/proc/create_random_seed(survive_on_station)
diff --git a/code/controllers/subsystems/plexora/_plexora.dm b/code/controllers/subsystems/plexora/_plexora.dm
index 0ac72e64413..1d6289bd042 100644
--- a/code/controllers/subsystems/plexora/_plexora.dm
+++ b/code/controllers/subsystems/plexora/_plexora.dm
@@ -46,14 +46,14 @@ SUBSYSTEM_DEF(plexora)
if(!CONFIG_GET(string/plexora_url) && !load_old_plexora_config())
enabled = FALSE
flags |= SS_NO_FIRE
- return ..()
+ return SS_INIT_NO_NEED
var/comms_key = CONFIG_GET(string/comms_key)
if (!comms_key)
stack_trace("SSplexora is enabled BUT there is no configured comms key! Please make sure to set one and update Plexora's server config.")
enabled = FALSE
flags |= SS_NO_FIRE
- return FALSE
+ return SS_INIT_FAILURE
base_url = CONFIG_GET(string/plexora_url)
@@ -70,7 +70,7 @@ SUBSYSTEM_DEF(plexora)
RegisterSignal(SSticker, COMSIG_TICKER_ROUND_STARTING, PROC_REF(roundstarted))
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/plexora/Recover()
flags |= SS_NO_INIT // Make extra sure we don't initialize twice.
diff --git a/code/controllers/subsystems/pollution/pollution.dm b/code/controllers/subsystems/pollution/pollution.dm
index 1891174d77a..b38bb13e230 100644
--- a/code/controllers/subsystems/pollution/pollution.dm
+++ b/code/controllers/subsystems/pollution/pollution.dm
@@ -30,7 +30,7 @@ SUBSYSTEM_DEF(pollution)
if(!length(pollutant_cast::name))
continue
singletons[type] = new type()
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/pollution/fire(resumed = FALSE)
var/list/current_run_cache = current_run
diff --git a/code/controllers/subsystems/processing/cwj.dm b/code/controllers/subsystems/processing/cwj.dm
index 21aa8080a09..60ffad1d039 100644
--- a/code/controllers/subsystems/processing/cwj.dm
+++ b/code/controllers/subsystems/processing/cwj.dm
@@ -6,7 +6,7 @@ PROCESSING_SUBSYSTEM_DEF(cwj)
/datum/controller/subsystem/processing/cwj/Initialize(timeofday)
initialize_cooking_recipes()
create_catalogs()
- ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/processing/cwj/proc/initialize_cooking_recipes()
INIT_EMPTY_GLOBLIST(cwj_recipe_dictionary)
diff --git a/code/controllers/subsystems/processing/reagents.dm b/code/controllers/subsystems/processing/reagents.dm
index 9e182ca1ae0..2114fdd25c7 100644
--- a/code/controllers/subsystems/processing/reagents.dm
+++ b/code/controllers/subsystems/processing/reagents.dm
@@ -6,7 +6,7 @@ PROCESSING_SUBSYSTEM_DEF(reagents)
/datum/controller/subsystem/processing/reagents/Initialize(timeofday)
initialize_chemical_reactions()
- ..()
+ return SS_INIT_SUCCESS
//Chemical Reactions - Initialises all /datum/chemical_reaction into a list
// It is filtered into multiple lists within a list.
diff --git a/code/controllers/subsystems/radio.dm b/code/controllers/subsystems/radio.dm
index 37bd5cb0239..739608c90b2 100644
--- a/code/controllers/subsystems/radio.dm
+++ b/code/controllers/subsystems/radio.dm
@@ -1,13 +1,9 @@
SUBSYSTEM_DEF(radio)
name = "Radio"
- flags = SS_NO_FIRE
+ flags = SS_NO_FIRE | SS_NO_INIT
var/list/datum/radio_frequency/frequencies = list()
-/datum/controller/subsystem/radio/Initialize()
- . = ..()
- GLOB.announcer = new /obj/item/device/radio/intercom(null)
-
/datum/controller/subsystem/radio/proc/add_object(obj/device, new_frequency as num, filter = null as text|null)
var/f_text = num2text(new_frequency)
var/datum/radio_frequency/frequency = frequencies[f_text]
diff --git a/code/controllers/subsystems/research.dm b/code/controllers/subsystems/research.dm
index cce27e53708..e24fe68915f 100644
--- a/code/controllers/subsystems/research.dm
+++ b/code/controllers/subsystems/research.dm
@@ -61,8 +61,7 @@ SUBSYSTEM_DEF(research)
initialize_design_file(file)
design_files_to_init = list()
- return ..()
-
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/research/proc/initialize_design_file(datum/computer_file/binary/design/design_file)
// If designs are already generated, initialized right away.
diff --git a/code/controllers/subsystems/sanity.dm b/code/controllers/subsystems/sanity.dm
deleted file mode 100644
index 3bf5413bf8f..00000000000
--- a/code/controllers/subsystems/sanity.dm
+++ /dev/null
@@ -1,29 +0,0 @@
-SUBSYSTEM_DEF(sanity)
- var/list/levels = list(
- 10 = "Divine",
- 9 = "Hallowed",
- 8 = "Holy",
- 7 = "Enlightening",
- 6 = "Blessed",
- 5 = "Inspirational",
- 4 = "Lovely",
- 3 = "Charming",
- 2 = "Pleasant",
- 1 = "Hospitable",
- 0 = "Neutral",
- -1 = "Moody",
- -2 = "Melancholic",
- -3 = "Depressing",
- -4 = "Unsetting",
- -5 = "Ominous",
- -6 = "Haunted",
- -7 = "Intense",
- -8 = "Dreadful",
- -9 = "Accursed",
- -10 = "Soul Crushing",
- )
-
-
-
-
-
diff --git a/code/controllers/subsystems/server_maint.dm b/code/controllers/subsystems/server_maint.dm
index d86a5832486..3144320f584 100644
--- a/code/controllers/subsystems/server_maint.dm
+++ b/code/controllers/subsystems/server_maint.dm
@@ -34,7 +34,7 @@ SUBSYSTEM_DEF(server_maint)
"dead_mob_list" = GLOB.dead_mob_list,
// "keyloop_list" = GLOB.keyloop_list, //A null here will cause new clients to be unable to move. totally unacceptable
)
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/server_maint/fire(resumed = FALSE)
if(!resumed)
diff --git a/code/controllers/subsystems/shuttle.dm b/code/controllers/subsystems/shuttle.dm
index 0633323d898..1abc798b013 100644
--- a/code/controllers/subsystems/shuttle.dm
+++ b/code/controllers/subsystems/shuttle.dm
@@ -14,7 +14,7 @@ SUBSYSTEM_DEF(shuttle)
/datum/controller/subsystem/shuttle/Initialize()
last_landmark_registration_time = world.time
initialize_shuttles()
- . = ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/shuttle/fire(resumed = FALSE)
if (!resumed)
diff --git a/code/controllers/subsystems/spawn_data.dm b/code/controllers/subsystems/spawn_data.dm
index 03434122064..ed635b9fd04 100644
--- a/code/controllers/subsystems/spawn_data.dm
+++ b/code/controllers/subsystems/spawn_data.dm
@@ -5,15 +5,467 @@ SUBSYSTEM_DEF(spawn_data)
var/list/lowkeyrandom_tags = list()
var/list/all_spawn_by_tag = list()
var/list/all_accompanying_obj_by_path = list()
+ var/list/cached_valid_candidates = list() // Cache by parameter hash
+ var/list/cached_prices = list() // Cache prices by path
+ var/list/cached_values = list() // Cache spawn values by path
+ var/list/cached_blacklist = list() // Pre-filtered blacklist
+ var/list/paths_by_price_range = list() // Pre-sorted by price ranges
/datum/controller/subsystem/spawn_data/Recover()
flags |= SS_NO_INIT // Make extra sure we don't initialize twice.
-/datum/controller/subsystem/spawn_data/Initialize(timeofday)
+/datum/controller/subsystem/spawn_data/Initialize()
generate_data()
- . = ..()
+ precompute_caches()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/spawn_data/stat_entry(msg)
if (!GLOB.Debug2)
return // Only show up in stat panel if debugging is enabled.
. = ..()
+
+///datum/controller/subsystem/spawn_data
+ //var/list/all_spawn_bad_paths = list()//hard
+ //var/list/all_spawn_blacklist = list()//soft
+ //var/list/all_spawn_by_price = list()
+ //var/list/all_price_by_path = list()
+ //var/list/all_spawn_by_frequency = list()
+ //var/list/all_spawn_frequency_by_path = list()
+ //var/list/all_spawn_by_rarity = list()
+ //var/list/all_spawn_rarity_by_path = list()
+ //var/list/all_spawn_value_by_path = list()
+ //var/list/all_spawn_by_tag = list()
+ //var/list/all_accompanying_obj_by_path = list()
+
+
+
+/datum/controller/subsystem/spawn_data/proc/precompute_caches()
+ // Cache all prices upfront
+ for(var/tag in all_spawn_by_tag)
+ for(var/path in all_spawn_by_tag[tag])
+ if(!(path in cached_prices))
+ cached_prices[path] = get_spawn_price(path)
+ if(!(path in cached_values))
+ cached_values[path] = get_spawn_value(path)
+
+ // Pre-filter blacklisted items
+ var/atom/movable/A
+ for(var/tag in all_spawn_by_tag)
+ for(var/path in all_spawn_by_tag[tag])
+ A = path
+ if(initial(A.spawn_blacklisted))
+ cached_blacklist[path] = TRUE
+
+ // Pre-sort into price ranges for faster filtering
+ var/list/price_brackets = list(0, 50, 100, 250, 500, 1000, 2500, 5000, 10000)
+ for(var/tag in all_spawn_by_tag)
+ for(var/path in all_spawn_by_tag[tag])
+ var/price = cached_prices[path]
+ for(var/i = 1 to price_brackets.len - 1)
+ var/low = price_brackets[i]
+ var/high = price_brackets[i + 1]
+ if(price >= low && price < high)
+ var/bracket_key = "[low]-[high]"
+ if(!paths_by_price_range[bracket_key])
+ paths_by_price_range[bracket_key] = list()
+ paths_by_price_range[bracket_key] += path
+ break
+
+
+/datum/controller/subsystem/spawn_data/proc/generate_data()
+ var/list/paths = list()
+ var/list/spawn_tags = list()
+ var/list/accompanying_objs = list()
+ var/generate_files = CONFIG_GET(flag/generate_loot_data)
+ var/file_dir = "strings/loot_data"
+ var/source_dir = file("[file_dir]/")
+ var/loot_data = file("[file_dir]/all_spawn_data.txt")
+ var/loot_data_paths = file("[file_dir]/all_spawn_paths.txt")
+ var/hard_blacklist_data = file("[file_dir]/hard_blacklist.txt")
+ var/blacklist_paths_data = file("[file_dir]/blacklist.txt")
+ var/file_dir_tags = "[file_dir]/tags/"
+ if(generate_files)
+ fdel(source_dir)
+ loot_data << "paths spawn_tags blacklisted spawn_value spawn_price prob_accompanying_obj all_accompanying_obj"
+
+ paths = subtypesof(/obj/item) - typesof(/obj/item/projectile)
+ paths += subtypesof(/mob/living)
+ paths += subtypesof(/obj/machinery)
+ paths += subtypesof(/obj/structure)
+ paths += subtypesof(/obj/spawner)
+ paths += subtypesof(/obj/effect)
+
+ for(var/path in paths)
+ var/atom/movable/A = path
+ if(path == initial(A.bad_type))
+ if(generate_files)
+ hard_blacklist_data << "[path]"
+ continue
+
+ spawn_tags = params2list(initial(A.spawn_tags))
+ if(!spawn_tags.len)
+ if(generate_files)
+ hard_blacklist_data << "[path]"
+ continue
+
+ if(initial(A.spawn_frequency) <= 0)
+ if(generate_files)
+ hard_blacklist_data << "[path]"
+ continue
+
+ accompanying_objs = initial(A.accompanying_object)
+ if(istext(accompanying_objs))
+ accompanying_objs = splittext(accompanying_objs, ",")
+ if(accompanying_objs.len)
+ var/list/temp_list = accompanying_objs
+ accompanying_objs = list()
+ for(var/obj_text in temp_list)
+ accompanying_objs += text2path(obj_text)
+ else if(ispath(accompanying_objs))
+ accompanying_objs = list(accompanying_objs)
+ if(islist(accompanying_objs) && accompanying_objs.len)
+ for(var/obj_path in accompanying_objs)
+ if(!ispath(obj_path))
+ continue
+ all_accompanying_obj_by_path[path] += list(obj_path)
+ if(ispath(path, /obj/item/gun/energy))
+ var/obj/item/gun/energy/E = A
+ if(!initial(E.use_external_power) && !initial(E.self_recharge))
+ all_accompanying_obj_by_path[path] += list(initial(E.suitable_cell))
+ else if(ispath(path, /obj/item/gun/projectile))
+ var/obj/item/gun/projectile/P = A
+ if(initial(P.magazine_type) && ((initial(P.load_method) & MAGAZINE) || (initial(P.load_method) & SPEEDLOADER)))
+ all_accompanying_obj_by_path[path] += list(initial(P.magazine_type))
+ else if(initial(P.ammo_type) && (initial(P.max_shells)) && (initial(P.load_method) & SINGLE_CASING))
+ for(var/i in 1 to min(initial(P.max_shells),10))
+ all_accompanying_obj_by_path[path] += list(initial(P.ammo_type))
+
+ var/price = get_spawn_price(path)
+ var/spawn_value = get_spawn_value(path)
+
+ for(var/tag in spawn_tags)
+ all_spawn_by_tag[tag] += list(path)
+ if(ispath(path, /obj/item) && tag != SPAWN_OBJ &&!initial(A.density) && ISINRANGE(price, 1, CHEAP_ITEM_PRICE) && !lowkeyrandom_tags.Find(tag))
+ lowkeyrandom_tags += list(tag)
+ if(generate_files)
+ var/tag_data_i = file("[file_dir_tags][tag].txt")
+ tag_data_i << "[path] blacklisted=[initial(A.spawn_blacklisted)] spawn_value=[spawn_value] spawn_price=[price] prob_accompanying_obj=[initial(A.prob_aditional_object)] accompanying_objs=[all_accompanying_obj_by_path[path] ? english_list(all_accompanying_obj_by_path[path], "nothing", ",") : "nothing"]"
+ if(generate_files)
+ loot_data << "[path] [initial(A.spawn_tags)] blacklisted=[initial(A.spawn_blacklisted)] spawn_value=[spawn_value] spawn_price=[price] prob_accompanying_obj=[initial(A.prob_aditional_object)] accompanying_objs=[all_accompanying_obj_by_path[path] ? english_list(all_accompanying_obj_by_path[path], "nothing", ",") : "nothing"]"
+ loot_data_paths << "[path]"
+ if(initial(A.spawn_blacklisted))
+ blacklist_paths_data << "[path]"
+
+/*get_spawn_value()
+this proc calculates the spawn value of the objects based on factors such as
+their frequency, their rarity value, their price and
+the data returned by the get_special_rarity_value() proc
+*/
+/datum/controller/subsystem/spawn_data/proc/get_spawn_value(npath)
+ if(npath in cached_values)
+ return cached_values[npath]
+
+ var/atom/movable/A = npath
+ var/value
+ if(ispath(npath, /obj/item/gun))
+ value = 10 * initial(A.spawn_frequency)/(get_special_rarity_value(npath)+(get_spawn_price(A)/GUN_PRICE_DIVISOR))
+ else if(ispath(npath, /obj/item/clothing))
+ value = 10 * initial(A.spawn_frequency)/(get_special_rarity_value(npath) + (get_spawn_price(A)/CLOTH_PRICE_DIVISOR))
+ else
+ value = 10 * initial(A.spawn_frequency)/(get_special_rarity_value(npath) + log(10,max(get_spawn_price(A),2)))
+
+ cached_values[npath] = value
+ return value
+
+/*get_special_rarity_value()
+increases the rarity value of items
+depending on certain determining factors,
+for example, the rarity value of power cells increases with their max_charge,
+the value of stock parts increases with the rating.
+*/
+
+/datum/controller/subsystem/spawn_data/proc/get_special_rarity_value(npath)
+ var/atom/movable/A = npath
+ . = initial(A.rarity_value)
+ if(ispath(npath, /obj/item/cell))
+ var/obj/item/cell/C = npath
+ var/bonus = 0
+ var/autorecharging_factor = 3.7
+ if(ispath(npath, /obj/item/cell/large))
+ bonus += (initial(C.maxcharge)/CELL_LARGE_BASE_CHARGE)**1.2
+ else if(ispath(npath, /obj/item/cell/medium))
+ bonus += (initial(C.maxcharge)/CELL_MEDIUM_BASE_CHARGE)**3.6
+ autorecharging_factor += 3
+ else if(ispath(npath, /obj/item/cell/small))
+ bonus += (initial(C.maxcharge)/CELL_SMALL_BASE_CHARGE)**1.9
+ autorecharging_factor += 2
+ if(initial(C.autorecharging))
+ bonus *= autorecharging_factor * (initial(C.autorecharge_rate)/BASE_AUTORECHARGE_RATE) * (initial(C.recharge_time)/BASE_RECHARGE_TIME)
+ . += bonus
+ else if(ispath(npath, /obj/item/stock_parts))
+ var/obj/item/stock_parts/SP = npath
+ . *= initial(SP.rating)**1.5
+
+/datum/controller/subsystem/spawn_data/proc/get_spawn_price(path, with_accompaying_obj = TRUE)
+ if(with_accompaying_obj && (path in cached_prices))
+ return cached_prices[path]
+
+ var/atom/movable/A = path
+ . = initial(A.price_tag)
+ if(with_accompaying_obj && all_accompanying_obj_by_path[path])
+ for(var/a_obj in all_accompanying_obj_by_path[path])
+ . += get_spawn_price(a_obj, FALSE)
+ if(ispath(path, /obj/item))
+ if(ispath(path, /obj/item/stock_parts))
+ var/obj/item/stock_parts/S = path
+ . *= initial(S.rating)
+ else if(ispath(path, /obj/item/stack))
+ var/obj/item/stack/S = path
+ . *= initial(S.amount)
+ if(ispath(path, /obj/item/stack/medical))
+ var/obj/item/stack/medical/M = path
+ . += initial(M.heal_brute) + initial(M.heal_burn)
+ else if(ispath(path, /obj/item/ammo_casing))
+ var/obj/item/ammo_casing/AC = path
+ . *= initial(AC.amount)
+ else if(ispath(path, /obj/item/handcuffs))
+ var/obj/item/handcuffs/H = path
+ . += initial(H.breakouttime) / 20
+ else if(ispath(path, /obj/structure/reagent_dispensers))
+ var/obj/structure/reagent_dispensers/R = path
+ . += initial(R.contents_cost)
+ else if(ispath(path, /obj/item/ammo_magazine))
+ var/obj/item/ammo_magazine/M = path
+ var/amount = initial(M.initial_ammo)
+ if(isnull(amount))
+ amount = initial(M.max_ammo)
+ . += amount * get_spawn_price(initial(M.ammo_type))
+ else if(ispath(path, /obj/item/tool))
+ var/obj/item/tool/T = path
+ if(initial(T.suitable_cell))
+ . += get_spawn_price(initial(T.suitable_cell))
+ else if(ispath(path, /obj/item/storage))
+ if(ispath(path, /obj/item/storage/box))
+ var/obj/item/storage/box/B = path
+ if(initial(B.prespawned_content_amount) > 0 && initial(B.prespawned_content_type))
+ . += initial(B.prespawned_content_amount) * get_spawn_price(initial(B.prespawned_content_type))
+ else if(ispath(path, /obj/item/storage/fancy))
+ var/obj/item/storage/fancy/F = path
+ if(initial(F.item_obj) && initial(F.storage_slots))
+ . += initial(F.storage_slots) * get_spawn_price(initial(F.item_obj))
+ else if(ispath(path, /obj/item/storage/pill_bottle))
+ var/obj/item/storage/pill_bottle/PB = path
+ if(initial(PB.prespawned_content_amount) && initial(PB.prespawned_content_type))
+ . += initial(PB.prespawned_content_amount) * get_spawn_price(initial(PB.prespawned_content_type))
+ else if(ispath(path, /obj/item/storage/firstaid))
+ var/obj/item/storage/firstaid/F = path
+ . += initial(F.prespawned_content_amount) * get_spawn_price(initial(F.prespawned_content_type))
+ else if(ispath(path, /obj/item/clothing))
+ var/obj/item/clothing/C = path
+ . += 5 * initial(C.style)
+ . += 10 * (1 - initial(C.siemens_coefficient))
+ if(ispath(path, /obj/item/clothing/suit/space/void))
+ var/obj/item/clothing/suit/space/void/V = A
+ if(initial(V.tank))
+ . += get_spawn_price(initial(V.tank))
+ if(initial(V.boots))
+ . += get_spawn_price(initial(V.boots))
+ if(initial(V.helmet))
+ . += get_spawn_price(initial(V.helmet))
+ else if(ispath(path, /obj/item/device))
+ if(. == 0)
+ . += 1
+ var/obj/item/device/D = path
+ if(initial(D.starting_cell) && initial(D.suitable_cell))
+ . += get_spawn_price(initial(D.suitable_cell))
+ else if(ispath(path, /obj/item/reagent_containers/glass))
+ var/obj/item/reagent_containers/glass/G = path
+ . += initial(G.volume)/100
+ else if(ispath(path, /obj/item/computer_hardware/hard_drive/portable/design))
+ var/obj/item/computer_hardware/hard_drive/portable/design/D = path
+ if(initial(D.license) > 0)
+ . += initial(D.license) * 2
+
+/datum/controller/subsystem/spawn_data/proc/spawn_by_tag(list/tags)
+ var/list/things = list()
+ for(var/tag in tags)
+ things |= all_spawn_by_tag["[tag]"]
+ return things
+
+/datum/controller/subsystem/spawn_data/proc/spawns_lower_price(list/paths, price)
+ var/list/things = list()
+ for(var/path in paths)
+ if(cached_prices[path] < price)
+ things += path
+ return things
+
+/datum/controller/subsystem/spawn_data/proc/spawns_upper_price(list/paths, price)
+ var/list/things = list()
+ for(var/path in paths)
+ if(cached_prices[path] > price)
+ things += path
+ return things
+
+/datum/controller/subsystem/spawn_data/proc/filter_densty(list/paths)
+ var/list/things = list()
+ for(var/path in paths)
+ var/atom/movable/AM = path
+ if(!initial(AM.density))
+ things += path
+ return things
+
+/datum/controller/subsystem/spawn_data/proc/only_top_candidates(list/paths, top=7)
+ if(paths.len <= top)
+ return paths
+ var/list/valid_spawn_value = list()
+ var/max_value = 0
+ var/list/things = list()
+ for(var/j=1 to top)
+ var/low = INFINITY
+ for(var/path in paths)
+ var/sapwn_value = cached_values[path] || get_spawn_value(path)
+ if((sapwn_value < low) && !(sapwn_value in valid_spawn_value))
+ low = sapwn_value
+ valid_spawn_value += low
+ for(var/value in valid_spawn_value)
+ if(value > max_value)
+ max_value = value
+ for(var/path in paths)
+ var/spawn_val = cached_values[path] || get_spawn_value(path)
+ if(spawn_val <= max_value)
+ things += path
+ return things
+
+/datum/controller/subsystem/spawn_data/proc/pick_spawn(list/paths, invert_value=FALSE)
+ var/list/things = list()
+ var/list/values = list()
+ for(var/path in paths)
+ var/spawn_value = cached_values[path] || get_spawn_value(path)
+ if(!(spawn_value in values) && spawn_value > 0)
+ values += spawn_value
+ if(invert_value)
+ spawn_value = 1/spawn_value
+ things[path] = spawn_value
+ var/spawn_value = pickweight(things, 0)
+ spawn_value = cached_values[spawn_value] || get_spawn_value(spawn_value)
+ things = list()
+ for(var/path in paths)
+ var/path_value = cached_values[path] || get_spawn_value(path)
+ if(path_value == spawn_value)
+ things += path
+ if(!length(things))
+ return
+ return pick(things)
+
+/datum/controller/subsystem/spawn_data/proc/take_tags(list/paths, list/exclude)
+ var/list/local_tags = list()
+ var/atom/movable/A
+ for(var/path in paths)
+ A = path
+ var/list/spawn_tags = params2list(initial(A.spawn_tags))
+ for(var/tag in spawn_tags)
+ if(tag in local_tags)
+ continue
+ local_tags += list(tag)
+ local_tags -= exclude
+ return local_tags
+
+/datum/controller/subsystem/spawn_data/proc/get_cache_key(list/tags, list/bad_tags, allow_blacklist, low_price, top_price, filter_density, list/include, list/exclude)
+ // Create a unique string key from all parameters
+ var/key = "[english_list(tags, "NONE")]|[english_list(bad_tags, "NONE")]|[allow_blacklist]|[low_price]|[top_price]|[filter_density]|[english_list(include, "NONE")]|[english_list(exclude, "NONE")]"
+ return key
+
+/datum/controller/subsystem/spawn_data/proc/valid_candidates(
+ list/tags,
+ list/bad_tags,
+ allow_blacklist=FALSE,
+ low_price=0,
+ top_price=0,
+ filter_density=FALSE,
+ list/include,
+ list/exclude,
+ list/should_be_include_tags
+)
+ var/cache_key = get_cache_key(tags, bad_tags, allow_blacklist, low_price, top_price, filter_density, include, exclude)
+ if(cache_key in cached_valid_candidates)
+ return cached_valid_candidates[cache_key]
+
+ var/list/candidates = list()
+
+ // Get paths for all required tags at once
+ for(var/tag in tags)
+ if(!length(candidates))
+ candidates = astype(all_spawn_by_tag[tag], /list).Copy()
+ else
+ candidates &= all_spawn_by_tag[tag] // Intersection
+
+ // Remove bad tags
+ for(var/tag in bad_tags)
+ candidates -= all_spawn_by_tag[tag]
+
+ if(!allow_blacklist)
+ for(var/path in candidates)
+ if(cached_blacklist[path])
+ candidates -= path
+
+ if(low_price || top_price)
+ var/list/price_filtered = list()
+ for(var/path in candidates)
+ var/price = cached_prices[path]
+ if(low_price && price < low_price)
+ continue
+ if(top_price && price > top_price)
+ continue
+ price_filtered += path
+ candidates = price_filtered
+
+ // Density filter
+ if(filter_density)
+ candidates = filter_densty(candidates)
+
+ // Apply include/exclude
+ candidates -= exclude
+ candidates |= include
+ candidates = removeNullsFromList(candidates)
+
+ cached_valid_candidates[cache_key] = candidates
+
+ return candidates
+
+/datum/controller/subsystem/spawn_data/proc/sort_paths_by_rarity(list/paths, invert_value=FALSE)
+ var/list/copy_paths = paths.Copy()
+ var/list/things = list()
+ for(var/path in paths)
+ var/max_value = -INFINITY
+ if(invert_value)
+ max_value = INFINITY
+ var/selected_path
+ if(!copy_paths.len)
+ break
+ for(var/actual_path in copy_paths)
+ var/actual_value = cached_values[actual_path] || get_spawn_value(actual_path)
+ if((!invert_value && actual_value > max_value) || (invert_value && (actual_value < max_value)))
+ max_value = actual_value
+ selected_path = actual_path
+ copy_paths -= selected_path
+ things += selected_path
+ return things
+
+/datum/controller/subsystem/spawn_data/proc/sort_paths_by_price(list/paths, invert_value=FALSE)
+ var/list/copy_paths = paths.Copy()
+ var/list/things = list()
+ for(var/path in paths)
+ var/max_value = INFINITY
+ if(invert_value)
+ max_value = -INFINITY
+ var/selected_path
+ if(!copy_paths.len)
+ break
+ for(var/actual_path in copy_paths)
+ var/actual_value = cached_prices[actual_path] || get_spawn_price(actual_path)
+ if((!invert_value && actual_value < max_value) || (invert_value && (actual_value > max_value)))
+ max_value = actual_value
+ selected_path = actual_path
+ copy_paths -= selected_path
+ things += selected_path
+ return things
diff --git a/code/controllers/subsystems/statpanel.dm b/code/controllers/subsystems/statpanel.dm
index cc60ca3f538..f7e55da586b 100644
--- a/code/controllers/subsystems/statpanel.dm
+++ b/code/controllers/subsystems/statpanel.dm
@@ -13,8 +13,8 @@ SUBSYSTEM_DEF(statpanels)
var/list/cached_images = list()
/datum/controller/subsystem/statpanels/Initialize()
- . = ..()
fire()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/statpanels/fire(resumed = FALSE)
if(!resumed)
diff --git a/code/controllers/subsystems/throwing.dm b/code/controllers/subsystems/throwing.dm
index b3d92f93699..b829c8abbd2 100644
--- a/code/controllers/subsystems/throwing.dm
+++ b/code/controllers/subsystems/throwing.dm
@@ -19,6 +19,7 @@ SUBSYSTEM_DEF(throwing)
name = "throwing"
wait = 1 // very small
priority = FIRE_PRIORITY_THROWING
+ flags = SS_NO_INIT
var/list/throwing_queue = list()
var/list/current_throwing_queue = list()
diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm
index b9067b9d62e..a19406a8bec 100644
--- a/code/controllers/subsystems/ticker.dm
+++ b/code/controllers/subsystems/ticker.dm
@@ -82,10 +82,9 @@ SUBSYSTEM_DEF(ticker)
setup_objects()
setup_huds()
- // TODO: make this a config option
- start_at = world.time + (5 MINUTES)
+ start_at = world.time + (CONFIG_GET(number/lobby_countdown) * 10)
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/ticker/proc/setup_objects()
populate_antag_type_list() // Set up antagonists. Do these first since character setup will rely on them
diff --git a/code/controllers/subsystems/tickets/mentor_tickets.dm b/code/controllers/subsystems/tickets/mentor_tickets.dm
index 7da2143292a..af654c465df 100644
--- a/code/controllers/subsystems/tickets/mentor_tickets.dm
+++ b/code/controllers/subsystems/tickets/mentor_tickets.dm
@@ -21,7 +21,7 @@ GLOBAL_REAL(SSmentor_tickets, /datum/controller/subsystem/tickets/mentor_tickets
close_messages = list("- [ticket_name] Closed -",
span_boldnotice("Please try to be as descriptive as possible in mentor helps. Mentors do not know the full situation you're in and need more information to give you a helpful response."),
"Your [ticket_name] has now been closed.")
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/tickets/mentor_tickets/message_staff(msg, prefix_type = NONE, important = FALSE)
message_mentorTicket(msg, important)
diff --git a/code/controllers/subsystems/tickets/tickets.dm b/code/controllers/subsystems/tickets/tickets.dm
index feef1eab69d..9fe60debdc5 100644
--- a/code/controllers/subsystems/tickets/tickets.dm
+++ b/code/controllers/subsystems/tickets/tickets.dm
@@ -30,7 +30,7 @@ SUBSYSTEM_DEF(tickets)
close_messages = list("- [ticket_name] Rejected! -",
span_boldnotice("Please try to be calm, clear, and descriptive in admin helps, do not assume the staff member has seen any related events, and clearly state the names of anybody you are reporting. If you asked a question, please ensure it was clear what you were asking."),
"Your [ticket_name] has now been closed.")
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/tickets/fire()
var/stales = checkStaleness()
diff --git a/code/controllers/subsystems/time_track.dm b/code/controllers/subsystems/time_track.dm
index 1408e6f03b6..e931f22809f 100644
--- a/code/controllers/subsystems/time_track.dm
+++ b/code/controllers/subsystems/time_track.dm
@@ -75,7 +75,7 @@ SUBSYSTEM_DEF(time_track)
"queries_standby"
) + sendmaps_headers
)
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/time_track/fire()
diff --git a/code/controllers/subsystems/tips_manager.dm b/code/controllers/subsystems/tips_manager.dm
index 6e723b39e91..c5c57766570 100644
--- a/code/controllers/subsystems/tips_manager.dm
+++ b/code/controllers/subsystems/tips_manager.dm
@@ -77,7 +77,7 @@ SUBSYSTEM_DEF(tips)
for(var/path in subtypesof(/tipsAndTricks/gameplay))
var/tipsAndTricks/gameplay/T = new path()
GLOB.gameplayTips += T
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/tips/proc/getRandomTip()
var/list/allTips = list()
diff --git a/code/controllers/subsystems/trade.dm b/code/controllers/subsystems/trade.dm
index 2da91da8858..98ddf2ff8b5 100644
--- a/code/controllers/subsystems/trade.dm
+++ b/code/controllers/subsystems/trade.dm
@@ -50,7 +50,7 @@ SUBSYSTEM_DEF(trade)
/datum/controller/subsystem/trade/Initialize()
InitStations()
- . = ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/trade/proc/ReInitStations()
DeInitStations()
diff --git a/code/datums/craft/recipe.dm b/code/datums/craft/recipe.dm
index ad73adf800c..ee11c114a16 100644
--- a/code/datums/craft/recipe.dm
+++ b/code/datums/craft/recipe.dm
@@ -21,7 +21,7 @@
steps += new /datum/craft_step(i, src)
/datum/craft_recipe/proc/is_compelete(step)
- return steps.len < step
+ return length(steps) < step
/datum/craft_recipe/proc/spawn_result(obj/item/craft/C, mob/living/user)
var/atom/movable/M = new result(get_turf(C))
diff --git a/code/datums/loot_spawner.dm b/code/datums/loot_spawner.dm
deleted file mode 100644
index ea4452e2ca4..00000000000
--- a/code/datums/loot_spawner.dm
+++ /dev/null
@@ -1,456 +0,0 @@
-///datum/controller/subsystem/spawn_data
- //var/list/all_spawn_bad_paths = list()//hard
- //var/list/all_spawn_blacklist = list()//soft
- //var/list/all_spawn_by_price = list()
- //var/list/all_price_by_path = list()
- //var/list/all_spawn_by_frequency = list()
- //var/list/all_spawn_frequency_by_path = list()
- //var/list/all_spawn_by_rarity = list()
- //var/list/all_spawn_rarity_by_path = list()
- //var/list/all_spawn_value_by_path = list()
- //var/list/all_spawn_by_tag = list()
- //var/list/all_accompanying_obj_by_path = list()
-
-/datum/controller/subsystem/spawn_data/Initialize()
- ..()
- generate_data()
- precompute_caches()
-
-/datum/controller/subsystem/spawn_data
- var/list/cached_valid_candidates = list() // Cache by parameter hash
- var/list/cached_prices = list() // Cache prices by path
- var/list/cached_values = list() // Cache spawn values by path
- var/list/cached_blacklist = list() // Pre-filtered blacklist
- var/list/paths_by_price_range = list() // Pre-sorted by price ranges
-
-
-/datum/controller/subsystem/spawn_data/proc/precompute_caches()
- // Cache all prices upfront
- for(var/tag in all_spawn_by_tag)
- for(var/path in all_spawn_by_tag[tag])
- if(!(path in cached_prices))
- cached_prices[path] = get_spawn_price(path)
- if(!(path in cached_values))
- cached_values[path] = get_spawn_value(path)
-
- // Pre-filter blacklisted items
- var/atom/movable/A
- for(var/tag in all_spawn_by_tag)
- for(var/path in all_spawn_by_tag[tag])
- A = path
- if(initial(A.spawn_blacklisted))
- cached_blacklist[path] = TRUE
-
- // Pre-sort into price ranges for faster filtering
- var/list/price_brackets = list(0, 50, 100, 250, 500, 1000, 2500, 5000, 10000)
- for(var/tag in all_spawn_by_tag)
- for(var/path in all_spawn_by_tag[tag])
- var/price = cached_prices[path]
- for(var/i = 1 to price_brackets.len - 1)
- var/low = price_brackets[i]
- var/high = price_brackets[i + 1]
- if(price >= low && price < high)
- var/bracket_key = "[low]-[high]"
- if(!paths_by_price_range[bracket_key])
- paths_by_price_range[bracket_key] = list()
- paths_by_price_range[bracket_key] += path
- break
-
-
-/datum/controller/subsystem/spawn_data/proc/generate_data()
- var/list/paths = list()
- var/list/spawn_tags = list()
- var/list/accompanying_objs = list()
- var/generate_files = CONFIG_GET(flag/generate_loot_data)
- var/file_dir = "strings/loot_data"
- var/source_dir = file("[file_dir]/")
- var/loot_data = file("[file_dir]/all_spawn_data.txt")
- var/loot_data_paths = file("[file_dir]/all_spawn_paths.txt")
- var/hard_blacklist_data = file("[file_dir]/hard_blacklist.txt")
- var/blacklist_paths_data = file("[file_dir]/blacklist.txt")
- var/file_dir_tags = "[file_dir]/tags/"
- if(generate_files)
- fdel(source_dir)
- loot_data << "paths spawn_tags blacklisted spawn_value spawn_price prob_accompanying_obj all_accompanying_obj"
-
- paths = subtypesof(/obj/item) - typesof(/obj/item/projectile)
- paths += subtypesof(/mob/living)
- paths += subtypesof(/obj/machinery)
- paths += subtypesof(/obj/structure)
- paths += subtypesof(/obj/spawner)
- paths += subtypesof(/obj/effect)
-
- for(var/path in paths)
- var/atom/movable/A = path
- if(path == initial(A.bad_type))
- if(generate_files)
- hard_blacklist_data << "[path]"
- continue
-
- spawn_tags = params2list(initial(A.spawn_tags))
- if(!spawn_tags.len)
- if(generate_files)
- hard_blacklist_data << "[path]"
- continue
-
- if(initial(A.spawn_frequency) <= 0)
- if(generate_files)
- hard_blacklist_data << "[path]"
- continue
-
- accompanying_objs = initial(A.accompanying_object)
- if(istext(accompanying_objs))
- accompanying_objs = splittext(accompanying_objs, ",")
- if(accompanying_objs.len)
- var/list/temp_list = accompanying_objs
- accompanying_objs = list()
- for(var/obj_text in temp_list)
- accompanying_objs += text2path(obj_text)
- else if(ispath(accompanying_objs))
- accompanying_objs = list(accompanying_objs)
- if(islist(accompanying_objs) && accompanying_objs.len)
- for(var/obj_path in accompanying_objs)
- if(!ispath(obj_path))
- continue
- all_accompanying_obj_by_path[path] += list(obj_path)
- if(ispath(path, /obj/item/gun/energy))
- var/obj/item/gun/energy/E = A
- if(!initial(E.use_external_power) && !initial(E.self_recharge))
- all_accompanying_obj_by_path[path] += list(initial(E.suitable_cell))
- else if(ispath(path, /obj/item/gun/projectile))
- var/obj/item/gun/projectile/P = A
- if(initial(P.magazine_type) && ((initial(P.load_method) & MAGAZINE) || (initial(P.load_method) & SPEEDLOADER)))
- all_accompanying_obj_by_path[path] += list(initial(P.magazine_type))
- else if(initial(P.ammo_type) && (initial(P.max_shells)) && (initial(P.load_method) & SINGLE_CASING))
- for(var/i in 1 to min(initial(P.max_shells),10))
- all_accompanying_obj_by_path[path] += list(initial(P.ammo_type))
-
- var/price = get_spawn_price(path)
- var/spawn_value = get_spawn_value(path)
-
- for(var/tag in spawn_tags)
- all_spawn_by_tag[tag] += list(path)
- if(ispath(path, /obj/item) && tag != SPAWN_OBJ &&!initial(A.density) && ISINRANGE(price, 1, CHEAP_ITEM_PRICE) && !lowkeyrandom_tags.Find(tag))
- lowkeyrandom_tags += list(tag)
- if(generate_files)
- var/tag_data_i = file("[file_dir_tags][tag].txt")
- tag_data_i << "[path] blacklisted=[initial(A.spawn_blacklisted)] spawn_value=[spawn_value] spawn_price=[price] prob_accompanying_obj=[initial(A.prob_aditional_object)] accompanying_objs=[all_accompanying_obj_by_path[path] ? english_list(all_accompanying_obj_by_path[path], "nothing", ",") : "nothing"]"
- if(generate_files)
- loot_data << "[path] [initial(A.spawn_tags)] blacklisted=[initial(A.spawn_blacklisted)] spawn_value=[spawn_value] spawn_price=[price] prob_accompanying_obj=[initial(A.prob_aditional_object)] accompanying_objs=[all_accompanying_obj_by_path[path] ? english_list(all_accompanying_obj_by_path[path], "nothing", ",") : "nothing"]"
- loot_data_paths << "[path]"
- if(initial(A.spawn_blacklisted))
- blacklist_paths_data << "[path]"
-
-/*get_spawn_value()
-this proc calculates the spawn value of the objects based on factors such as
-their frequency, their rarity value, their price and
-the data returned by the get_special_rarity_value() proc
-*/
-/datum/controller/subsystem/spawn_data/proc/get_spawn_value(npath)
- if(npath in cached_values)
- return cached_values[npath]
-
- var/atom/movable/A = npath
- var/value
- if(ispath(npath, /obj/item/gun))
- value = 10 * initial(A.spawn_frequency)/(get_special_rarity_value(npath)+(get_spawn_price(A)/GUN_PRICE_DIVISOR))
- else if(ispath(npath, /obj/item/clothing))
- value = 10 * initial(A.spawn_frequency)/(get_special_rarity_value(npath) + (get_spawn_price(A)/CLOTH_PRICE_DIVISOR))
- else
- value = 10 * initial(A.spawn_frequency)/(get_special_rarity_value(npath) + log(10,max(get_spawn_price(A),2)))
-
- cached_values[npath] = value
- return value
-
-/*get_special_rarity_value()
-increases the rarity value of items
-depending on certain determining factors,
-for example, the rarity value of power cells increases with their max_charge,
-the value of stock parts increases with the rating.
-*/
-
-/datum/controller/subsystem/spawn_data/proc/get_special_rarity_value(npath)
- var/atom/movable/A = npath
- . = initial(A.rarity_value)
- if(ispath(npath, /obj/item/cell))
- var/obj/item/cell/C = npath
- var/bonus = 0
- var/autorecharging_factor = 3.7
- if(ispath(npath, /obj/item/cell/large))
- bonus += (initial(C.maxcharge)/CELL_LARGE_BASE_CHARGE)**1.2
- else if(ispath(npath, /obj/item/cell/medium))
- bonus += (initial(C.maxcharge)/CELL_MEDIUM_BASE_CHARGE)**3.6
- autorecharging_factor += 3
- else if(ispath(npath, /obj/item/cell/small))
- bonus += (initial(C.maxcharge)/CELL_SMALL_BASE_CHARGE)**1.9
- autorecharging_factor += 2
- if(initial(C.autorecharging))
- bonus *= autorecharging_factor * (initial(C.autorecharge_rate)/BASE_AUTORECHARGE_RATE) * (initial(C.recharge_time)/BASE_RECHARGE_TIME)
- . += bonus
- else if(ispath(npath, /obj/item/stock_parts))
- var/obj/item/stock_parts/SP = npath
- . *= initial(SP.rating)**1.5
-
-/datum/controller/subsystem/spawn_data/proc/get_spawn_price(path, with_accompaying_obj = TRUE)
- if(with_accompaying_obj && (path in cached_prices))
- return cached_prices[path]
-
- var/atom/movable/A = path
- . = initial(A.price_tag)
- if(with_accompaying_obj && all_accompanying_obj_by_path[path])
- for(var/a_obj in all_accompanying_obj_by_path[path])
- . += get_spawn_price(a_obj, FALSE)
- if(ispath(path, /obj/item))
- if(ispath(path, /obj/item/stock_parts))
- var/obj/item/stock_parts/S = path
- . *= initial(S.rating)
- else if(ispath(path, /obj/item/stack))
- var/obj/item/stack/S = path
- . *= initial(S.amount)
- if(ispath(path, /obj/item/stack/medical))
- var/obj/item/stack/medical/M = path
- . += initial(M.heal_brute) + initial(M.heal_burn)
- else if(ispath(path, /obj/item/ammo_casing))
- var/obj/item/ammo_casing/AC = path
- . *= initial(AC.amount)
- else if(ispath(path, /obj/item/handcuffs))
- var/obj/item/handcuffs/H = path
- . += initial(H.breakouttime) / 20
- else if(ispath(path, /obj/structure/reagent_dispensers))
- var/obj/structure/reagent_dispensers/R = path
- . += initial(R.contents_cost)
- else if(ispath(path, /obj/item/ammo_magazine))
- var/obj/item/ammo_magazine/M = path
- var/amount = initial(M.initial_ammo)
- if(isnull(amount))
- amount = initial(M.max_ammo)
- . += amount * get_spawn_price(initial(M.ammo_type))
- else if(ispath(path, /obj/item/tool))
- var/obj/item/tool/T = path
- if(initial(T.suitable_cell))
- . += get_spawn_price(initial(T.suitable_cell))
- else if(ispath(path, /obj/item/storage))
- if(ispath(path, /obj/item/storage/box))
- var/obj/item/storage/box/B = path
- if(initial(B.prespawned_content_amount) > 0 && initial(B.prespawned_content_type))
- . += initial(B.prespawned_content_amount) * get_spawn_price(initial(B.prespawned_content_type))
- else if(ispath(path, /obj/item/storage/fancy))
- var/obj/item/storage/fancy/F = path
- if(initial(F.item_obj) && initial(F.storage_slots))
- . += initial(F.storage_slots) * get_spawn_price(initial(F.item_obj))
- else if(ispath(path, /obj/item/storage/pill_bottle))
- var/obj/item/storage/pill_bottle/PB = path
- if(initial(PB.prespawned_content_amount) && initial(PB.prespawned_content_type))
- . += initial(PB.prespawned_content_amount) * get_spawn_price(initial(PB.prespawned_content_type))
- else if(ispath(path, /obj/item/storage/firstaid))
- var/obj/item/storage/firstaid/F = path
- . += initial(F.prespawned_content_amount) * get_spawn_price(initial(F.prespawned_content_type))
- else if(ispath(path, /obj/item/clothing))
- var/obj/item/clothing/C = path
- . += 5 * initial(C.style)
- . += 10 * (1 - initial(C.siemens_coefficient))
- if(ispath(path, /obj/item/clothing/suit/space/void))
- var/obj/item/clothing/suit/space/void/V = A
- if(initial(V.tank))
- . += get_spawn_price(initial(V.tank))
- if(initial(V.boots))
- . += get_spawn_price(initial(V.boots))
- if(initial(V.helmet))
- . += get_spawn_price(initial(V.helmet))
- else if(ispath(path, /obj/item/device))
- if(. == 0)
- . += 1
- var/obj/item/device/D = path
- if(initial(D.starting_cell) && initial(D.suitable_cell))
- . += get_spawn_price(initial(D.suitable_cell))
- else if(ispath(path, /obj/item/reagent_containers/glass))
- var/obj/item/reagent_containers/glass/G = path
- . += initial(G.volume)/100
- else if(ispath(path, /obj/item/computer_hardware/hard_drive/portable/design))
- var/obj/item/computer_hardware/hard_drive/portable/design/D = path
- if(initial(D.license) > 0)
- . += initial(D.license) * 2
-
-/datum/controller/subsystem/spawn_data/proc/spawn_by_tag(list/tags)
- var/list/things = list()
- for(var/tag in tags)
- things |= all_spawn_by_tag["[tag]"]
- return things
-
-/datum/controller/subsystem/spawn_data/proc/spawns_lower_price(list/paths, price)
- var/list/things = list()
- for(var/path in paths)
- if(cached_prices[path] < price)
- things += path
- return things
-
-/datum/controller/subsystem/spawn_data/proc/spawns_upper_price(list/paths, price)
- var/list/things = list()
- for(var/path in paths)
- if(cached_prices[path] > price)
- things += path
- return things
-
-/datum/controller/subsystem/spawn_data/proc/filter_densty(list/paths)
- var/list/things = list()
- for(var/path in paths)
- var/atom/movable/AM = path
- if(!initial(AM.density))
- things += path
- return things
-
-/datum/controller/subsystem/spawn_data/proc/only_top_candidates(list/paths, top=7)
- if(paths.len <= top)
- return paths
- var/list/valid_spawn_value = list()
- var/max_value = 0
- var/list/things = list()
- for(var/j=1 to top)
- var/low = INFINITY
- for(var/path in paths)
- var/sapwn_value = cached_values[path] || get_spawn_value(path)
- if((sapwn_value < low) && !(sapwn_value in valid_spawn_value))
- low = sapwn_value
- valid_spawn_value += low
- for(var/value in valid_spawn_value)
- if(value > max_value)
- max_value = value
- for(var/path in paths)
- var/spawn_val = cached_values[path] || get_spawn_value(path)
- if(spawn_val <= max_value)
- things += path
- return things
-
-/datum/controller/subsystem/spawn_data/proc/pick_spawn(list/paths, invert_value=FALSE)
- var/list/things = list()
- var/list/values = list()
- for(var/path in paths)
- var/spawn_value = cached_values[path] || get_spawn_value(path)
- if(!(spawn_value in values) && spawn_value > 0)
- values += spawn_value
- if(invert_value)
- spawn_value = 1/spawn_value
- things[path] = spawn_value
- var/spawn_value = pickweight(things, 0)
- spawn_value = cached_values[spawn_value] || get_spawn_value(spawn_value)
- things = list()
- for(var/path in paths)
- var/path_value = cached_values[path] || get_spawn_value(path)
- if(path_value == spawn_value)
- things += path
- if(!length(things))
- return
- return pick(things)
-
-/datum/controller/subsystem/spawn_data/proc/take_tags(list/paths, list/exclude)
- var/list/local_tags = list()
- var/atom/movable/A
- for(var/path in paths)
- A = path
- var/list/spawn_tags = params2list(initial(A.spawn_tags))
- for(var/tag in spawn_tags)
- if(tag in local_tags)
- continue
- local_tags += list(tag)
- local_tags -= exclude
- return local_tags
-
-/datum/controller/subsystem/spawn_data/proc/get_cache_key(list/tags, list/bad_tags, allow_blacklist, low_price, top_price, filter_density, list/include, list/exclude)
- // Create a unique string key from all parameters
- var/key = "[english_list(tags, "NONE")]|[english_list(bad_tags, "NONE")]|[allow_blacklist]|[low_price]|[top_price]|[filter_density]|[english_list(include, "NONE")]|[english_list(exclude, "NONE")]"
- return key
-
-/datum/controller/subsystem/spawn_data/proc/valid_candidates(
- list/tags,
- list/bad_tags,
- allow_blacklist=FALSE,
- low_price=0,
- top_price=0,
- filter_density=FALSE,
- list/include,
- list/exclude,
- list/should_be_include_tags
-)
- var/cache_key = get_cache_key(tags, bad_tags, allow_blacklist, low_price, top_price, filter_density, include, exclude)
- if(cache_key in cached_valid_candidates)
- return cached_valid_candidates[cache_key]
-
- var/list/candidates = list()
-
- // Get paths for all required tags at once
- for(var/tag in tags)
- if(!length(candidates))
- candidates = astype(all_spawn_by_tag[tag], /list).Copy()
- else
- candidates &= all_spawn_by_tag[tag] // Intersection
-
- // Remove bad tags
- for(var/tag in bad_tags)
- candidates -= all_spawn_by_tag[tag]
-
- if(!allow_blacklist)
- for(var/path in candidates)
- if(cached_blacklist[path])
- candidates -= path
-
- if(low_price || top_price)
- var/list/price_filtered = list()
- for(var/path in candidates)
- var/price = cached_prices[path]
- if(low_price && price < low_price)
- continue
- if(top_price && price > top_price)
- continue
- price_filtered += path
- candidates = price_filtered
-
- // Density filter
- if(filter_density)
- candidates = filter_densty(candidates)
-
- // Apply include/exclude
- candidates -= exclude
- candidates |= include
- candidates = removeNullsFromList(candidates)
-
- cached_valid_candidates[cache_key] = candidates
-
- return candidates
-
-/datum/controller/subsystem/spawn_data/proc/sort_paths_by_rarity(list/paths, invert_value=FALSE)
- var/list/copy_paths = paths.Copy()
- var/list/things = list()
- for(var/path in paths)
- var/max_value = -INFINITY
- if(invert_value)
- max_value = INFINITY
- var/selected_path
- if(!copy_paths.len)
- break
- for(var/actual_path in copy_paths)
- var/actual_value = cached_values[actual_path] || get_spawn_value(actual_path)
- if((!invert_value && actual_value > max_value) || (invert_value && (actual_value < max_value)))
- max_value = actual_value
- selected_path = actual_path
- copy_paths -= selected_path
- things += selected_path
- return things
-
-/datum/controller/subsystem/spawn_data/proc/sort_paths_by_price(list/paths, invert_value=FALSE)
- var/list/copy_paths = paths.Copy()
- var/list/things = list()
- for(var/path in paths)
- var/max_value = INFINITY
- if(invert_value)
- max_value = -INFINITY
- var/selected_path
- if(!copy_paths.len)
- break
- for(var/actual_path in copy_paths)
- var/actual_value = cached_prices[actual_path] || get_spawn_price(actual_path)
- if((!invert_value && actual_value < max_value) || (invert_value && (actual_value > max_value)))
- max_value = actual_value
- selected_path = actual_path
- copy_paths -= selected_path
- things += selected_path
- return things
diff --git a/code/game/gamemodes/events/weather.dm b/code/game/gamemodes/events/weather.dm
index f6e08182c3a..a2166b79137 100644
--- a/code/game/gamemodes/events/weather.dm
+++ b/code/game/gamemodes/events/weather.dm
@@ -169,7 +169,7 @@ SUBSYSTEM_DEF(weather)
for(var/z in levels_by_trait)
LAZYINITLIST(eligible_zlevels["[z]"])
eligible_zlevels["[z]"][W] = probability
- return ..()
+ return SS_INIT_SUCCESS
/datum/controller/subsystem/weather/proc/run_weather(datum/weather/weather_datum_type)
if (istext(weather_datum_type))
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index be0ae8481b3..01ccd217c51 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -57,6 +57,7 @@ GLOBAL_LIST_INIT(admin_verbs_debug, list(
/client/proc/debug_controller,
/client/proc/restart_controller,
/client/proc/debug_antagonist_template,
+ /client/proc/cmd_controller_view_ui,
/client/proc/cmd_display_init_log,
/client/proc/cmd_display_init_costs,
/client/proc/kill_air,
diff --git a/code/modules/admin/sql_message_system.dm b/code/modules/admin/sql_message_system.dm
index a00c34fb302..3d2ddc673d0 100644
--- a/code/modules/admin/sql_message_system.dm
+++ b/code/modules/admin/sql_message_system.dm
@@ -103,7 +103,6 @@
log_admin_private(pm)
message_admins("[header]:
[text]")
// admin_ticket_log(target_ckey, "[header]: [text]")
- // Monkestation edit start - plexora
var/datum/client_interface/mock_player = new(target_ckey)
mock_player.prefs = new /datum/preferences(mock_player)
@@ -123,7 +122,6 @@
plexora_note["total_playtime"] = mock_player.get_exp_living()
SSplexora.new_note(plexora_note)
- // Monkestation edit end
if(browse)
browse_messages("[type]")
else
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index 5422d84e391..760d8509cb4 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -487,6 +487,7 @@
return data
/mob/new_player/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ SHOULD_CALL_PARENT(FALSE)
// Don't call parent - we don't need the default checks for new_player
// Parent would check UI_INTERACTIVE status which may fail for new_player
diff --git a/code/modules/text_to_speech/tts_main.dm b/code/modules/text_to_speech/tts_main.dm
index 3296fae597c..d94153c0502 100644
--- a/code/modules/text_to_speech/tts_main.dm
+++ b/code/modules/text_to_speech/tts_main.dm
@@ -21,7 +21,7 @@ var/list/tts_seeds = list()
GLOB.tts_bearer = "Bearer [CONFIG_GET(string/tts_key)]"
if(!fexists("config/tts_seeds.txt"))
- return
+ return FALSE
for(var/i in file2list("config/tts_seeds.txt"))
if(!LAZYLEN(i) || (copytext(i, 1, 2) == "#"))
@@ -39,8 +39,9 @@ var/list/tts_seeds = list()
tts_seeds += seed_name
tts_seeds[seed_name] = list("value" = seed_value, "category" = seed_category, "gender" = seed_gender_restriction)
- LIBCALL(RUST_G, "file_write")("[seed_value]", "sound/tts_cache/[seed_name]/seed.txt")
- LIBCALL(RUST_G, "file_write")("[seed_value]", "sound/tts_scrambled/[seed_name]/seed.txt")
+ rustg_file_write("[seed_value]", "sound/tts_cache/[seed_name]/seed.txt")
+ rustg_file_write("[seed_value]", "sound/tts_scrambled/[seed_name]/seed.txt")
+ return TRUE
/proc/get_tts(message, seed = TTS_SEED_DEFAULT_MALE)
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 6c2d6ab90b2..2536af2b8be 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -41,6 +41,11 @@
/// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)`
#define TEST_FOCUS(test_path) ##test_path { focus = TRUE; }
+/// Logs a noticable message on GitHub, but will not mark as an error.
+/// Use this when something shouldn't happen and is of note, but shouldn't block CI.
+/// Does not mark the test as failed.
+#define TEST_NOTICE(source, message) source.log_for_test((##message), "notice", __FILE__, __LINE__)
+
/// Constants indicating unit test completion status
#define UNIT_TEST_PASSED 0
#define UNIT_TEST_FAILED 1
@@ -78,6 +83,7 @@
// #include "connect_loc.dm"
// #include "confusion.dm"
// #include "crayons.dm"
+#include "crafting.dm"
// #include "designs.dm"
// #include "dynamic_ruleset_sanity.dm"
// #include "emoting.dm"
diff --git a/code/modules/unit_tests/crafting.dm b/code/modules/unit_tests/crafting.dm
new file mode 100644
index 00000000000..88b1ce080bc
--- /dev/null
+++ b/code/modules/unit_tests/crafting.dm
@@ -0,0 +1,11 @@
+/datum/unit_test/crafting
+
+/datum/unit_test/crafting/Run()
+ for(var/datum/craft_recipe/CR as anything in subtypesof(/datum/craft_recipe))
+ if(!initial(CR?.name))
+ continue
+ CR = new CR
+ if(!length(CR.steps))
+ if(CR.name)
+ TEST_FAIL("ERROR: empty steps for craft recipe [CR.type]")
+ qdel(CR)
diff --git a/code/modules/unit_tests/subsystem_init.dm b/code/modules/unit_tests/subsystem_init.dm
index c377302ba6a..82c9fc956fb 100644
--- a/code/modules/unit_tests/subsystem_init.dm
+++ b/code/modules/unit_tests/subsystem_init.dm
@@ -1,7 +1,23 @@
+/// Tests that all subsystems that need to properly initialize.
+/datum/unit_test/subsystem_init
+
/datum/unit_test/subsystem_init/Run()
- for(var/i in Master.subsystems)
- var/datum/controller/subsystem/ss = i
- if(ss.flags & SS_NO_INIT)
+ for(var/datum/controller/subsystem/subsystem as anything in Master.subsystems)
+ if(subsystem.flags & SS_NO_INIT)
continue
- if(!ss.initialized)
- TEST_FAIL("[ss]([ss.type]) is a subsystem meant to initialize but doesn't get set as initialized.")
+ if(subsystem.initialized)
+ continue
+
+ var/should_fail = !(subsystem.flags & SS_OK_TO_FAIL_INIT)
+ var/list/message_strings = list("[subsystem] ([subsystem.type]) is a subsystem meant to initialize but could not get initialized.")
+
+ if(!isnull(subsystem.initialization_failure_message))
+ message_strings += "The subsystem reported the following: [subsystem.initialization_failure_message]"
+
+ if(should_fail)
+ TEST_FAIL(jointext(message_strings, "\n"))
+ continue
+
+ message_strings += "This subsystem is marked as SS_OK_TO_FAIL_INIT. This is still a bug, but it is non-blocking."
+ TEST_NOTICE(src, jointext(message_strings, "\n"))
+
diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm
index 50a874fa44c..a86f8a580ac 100644
--- a/code/modules/unit_tests/unit_test.dm
+++ b/code/modules/unit_tests/unit_test.dm
@@ -112,6 +112,16 @@ GLOBAL_LIST_EMPTY(unit_test_mapping_logs)
log_test("[path_prefix]_[name] was put in data/screenshots_new")
+/// Logs a test message. Will use GitHub action syntax found at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
+/datum/unit_test/proc/log_for_test(text, priority, file, line)
+ var/map_name = GLOB.maps_data.path
+
+ // Need to escape the text to properly support newlines.
+ var/annotation_text = replacetext(text, "%", "%25")
+ annotation_text = replacetext(annotation_text, "\n", "%0A")
+
+ log_world("::[priority] file=[file],line=[line],title=[map_name]: [type]::[annotation_text]")
+
/proc/RunUnitTest(test_path, list/test_results)
var/datum/unit_test/test = new test_path
diff --git a/config/example/config.txt b/config/example/config.txt
index 22274563839..45ea6168049 100644
--- a/config/example/config.txt
+++ b/config/example/config.txt
@@ -29,6 +29,9 @@ HOSTEDBY yournamehere
## If the game appears on the hub or not
HUB
+## Lobby time: This is the amount of time between rounds that players have to setup their characters and be ready.
+LOBBY_COUNTDOWN 300
+
## Pop requirement for the server to be removed from the hub.
# MAX_HUB_POP 100
@@ -73,8 +76,6 @@ MOD_TEMPBAN_MAX 1440
## Max amount of time mods can tempban for, in minutes
MOD_JOB_TEMPBAN_MAX 1440
-## Uncomment to set the number of /world/Reboot()s before the DreamDaemon restarts itself. 0 means restart every round. Requires tgstation server tools.
-#ROUNDS_UNTIL_HARD_RESTART 10
## Unhash this entry to have certain jobs require your account to be at least a certain number of days old to select. You can configure the exact age requirement for different jobs by editing
## the minimal_player_age variable in the files in folder /code/game/jobs/job/.. for the job you want to edit. Set minimal_player_age to 0 to disable age requirement for that job.
@@ -371,6 +372,38 @@ IPR_MINIMUM_AGE 5
## Uncomment to enable debugging admin hrefs
#DEBUG_ADMIN_HREFS
+###Master Controller High Pop Mode###
+
+##The Master Controller(MC) is the primary system controlling timed tasks and events in SS13 (lobby timer, game checks, lighting updates, atmos, etc)
+##Default base MC tick rate (1 = process every "byond tick" (see: tick_lag/fps config settings), 2 = process every 2 byond ticks, etc)
+## Setting this to 0 will prevent the Master Controller from ticking
+BASE_MC_TICK_RATE 1
+
+##High population MC tick rate
+## Byond rounds timer values UP, but the tick rate is modified with heuristics during lag spites so setting this to something like 2
+## will make it run every 2 byond ticks, but will also double the effect of anti-lag heuristics. You can instead set it to something like
+## 1.1 to make it run every 2 byond ticks, but only increase the effect of anti-lag heuristics by 10%. or 1.5 for 50%.
+## (As an aside, you could in theory also reduce the effect of anti-lag heuristics in the base tick rate by setting it to something like 0.5)
+HIGH_POP_MC_TICK_RATE 1.1
+
+##Engage high pop mode if player count raises above this (Player in this context means any connected user. Lobby, ghost or in-game all count)
+HIGH_POP_MC_MODE_AMOUNT 65
+
+##Disengage high pop mode if player count drops below this
+DISABLE_HIGH_POP_MC_MODE_AMOUNT 60
+
+## Uncomment to prevent the world from sleeping while no players are connected after initializations
+#RESUME_AFTER_INITIALIZATIONS
+
+## Enable automatic profiling - Byond 513.1506 and newer only.
+#AUTO_PROFILE
+
+## Threshold (in deciseconds) for real time between ticks before we start dumping profiles
+DRIFT_DUMP_THRESHOLD 40
+
+## How long to wait (in deciseconds) after a profile dump before logging another tickdrift sourced one
+DRIFT_PROFILE_DELAY 150
+
## Causes configuration errors to spit out runtimes
CONFIG_ERRORS_RUNTIME
diff --git a/tgui/packages/tgui/interfaces/AdminBookViewer.tsx b/tgui/packages/tgui/interfaces/AdminBookViewer.tsx
index 994f045d66f..b7ed905fb7c 100644
--- a/tgui/packages/tgui/interfaces/AdminBookViewer.tsx
+++ b/tgui/packages/tgui/interfaces/AdminBookViewer.tsx
@@ -9,7 +9,7 @@ type ViewerData = {
view_raw: boolean;
};
-export const AdminBookViewer = (_: any) => {
+export const AdminBookViewer = () => {
const { data } = useBackend();
return (
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/OverviewSection.tsx b/tgui/packages/tgui/interfaces/ControllerOverview/OverviewSection.tsx
new file mode 100644
index 00000000000..79c42564f44
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/OverviewSection.tsx
@@ -0,0 +1,75 @@
+import { Button, LabeledList, Section, Stack } from 'tgui-core/components';
+
+import { useBackend } from '../../backend';
+import type { ControllerData } from './types';
+
+export const OverviewSection = (props) => {
+ const { act, data } = useBackend();
+ const {
+ fast_update,
+ rolling_length,
+ map_cpu,
+ subsystems = [],
+ world_time,
+ } = data;
+
+ let avgUsage = 0;
+ let overallOverrun = 0;
+ for (let i = 0; i < subsystems.length; i++) {
+ avgUsage += subsystems[i].usage_per_tick;
+ overallOverrun += subsystems[i].overtime;
+ }
+
+ return (
+
+
+ {
+ act('set_rolling_length', {
+ rolling_length: value,
+ });
+ }}
+ />
+ >
+ }
+ >
+
+
+
+
+ {world_time.toFixed(1)}
+
+
+ {map_cpu.toFixed(2)}%
+
+
+
+
+
+
+ {avgUsage.toFixed(2)}%
+
+
+ {overallOverrun.toFixed(2)}%
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemDialog.tsx b/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemDialog.tsx
new file mode 100644
index 00000000000..d02d353091d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemDialog.tsx
@@ -0,0 +1,76 @@
+import {
+ Box,
+ Button,
+ Divider,
+ LabeledList,
+ Modal,
+ Stack,
+} from 'tgui-core/components';
+
+import type { SubsystemData } from './types';
+
+type Props = {
+ subsystem: SubsystemData;
+ onClose: () => void;
+};
+
+export const SubsystemDialog = (props: Props) => {
+ const { subsystem, onClose } = props;
+ const {
+ cost_ms,
+ init_order,
+ initialization_failure_message,
+ last_fire,
+ name,
+ next_fire,
+ overtime,
+ tick_usage,
+ usage_per_tick,
+ } = subsystem;
+
+ return (
+
+
+
+ {name}
+
+
+
+
+
+
+
+
+ {init_order}
+ {last_fire}
+ {next_fire}
+
+ {cost_ms.toFixed(2)}ms
+
+
+ {tick_usage.toFixed(2)}%
+
+
+ {usage_per_tick.toFixed(2)}%
+
+
+ {overtime.toFixed(2)}%
+
+ {initialization_failure_message && (
+
+ {initialization_failure_message}
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemRow.tsx b/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemRow.tsx
new file mode 100644
index 00000000000..1019f0e0be4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemRow.tsx
@@ -0,0 +1,106 @@
+import {
+ Button,
+ Icon,
+ ProgressBar,
+ Stack,
+ Table,
+ Tooltip,
+} from 'tgui-core/components';
+
+import { useBackend } from '../../backend';
+import { SORTING_TYPES } from './contants';
+import { SortType, type SubsystemData } from './types';
+
+type Props = {
+ max: number;
+ setSelected: (newValue: SubsystemData) => void;
+ showBars: boolean;
+ sortType: SortType;
+ subsystem: SubsystemData;
+};
+
+export const SubsystemRow = (props: Props) => {
+ const { act } = useBackend();
+ const { max, setSelected, showBars, sortType, subsystem } = props;
+ const { can_fire, doesnt_fire, initialized, name, ref } = subsystem;
+
+ const { propName } = SORTING_TYPES[sortType];
+ const value = subsystem[propName];
+
+ let icon = 'play';
+ let color = 'good';
+ let tooltip = 'Operational';
+ if (!initialized) {
+ icon = 'circle-exclamation';
+ color = 'darkgreen';
+ tooltip = 'Not initialized';
+ } else if (doesnt_fire) {
+ icon = 'check';
+ color = 'grey';
+ tooltip = 'Does not fire';
+ } else if (!can_fire) {
+ icon = 'pause';
+ color = 'grey';
+ tooltip = 'Paused';
+ }
+
+ let valueDisplay = '';
+ let rangeDisplay = {};
+ if (showBars) {
+ if (sortType === SortType.Cost) {
+ valueDisplay = `${value.toFixed(2)}ms`;
+ rangeDisplay = {
+ average: [75, 124.99],
+ bad: [125, Infinity],
+ };
+ } else {
+ valueDisplay = `${value.toFixed(2)}%`;
+ rangeDisplay = {
+ average: [10, 24.99],
+ bad: [25, Infinity],
+ };
+ }
+ } else {
+ valueDisplay = value;
+ }
+
+ return (
+
+
+
+
+
+
+ setSelected(subsystem)}>
+ {showBars ? (
+
+ {name} {valueDisplay}
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemViews.tsx b/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemViews.tsx
new file mode 100644
index 00000000000..a491f763769
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/SubsystemViews.tsx
@@ -0,0 +1,116 @@
+import { Button, Section, Stack, Table } from 'tgui-core/components';
+
+import { useBackend, useLocalState } from '../../backend';
+import { SORTING_TYPES } from './contants';
+import type { FilterState } from './filters';
+import { SubsystemRow } from './SubsystemRow';
+import type { ControllerData, SubsystemData } from './types';
+
+type Props = {
+ filterOpts: FilterState;
+ setSelected: (newSelected: SubsystemData | undefined) => void;
+};
+
+let lastInDeciseconds: boolean | undefined;
+
+export const SubsystemViews = (props: Props) => {
+ const { data } = useBackend();
+ const { subsystems } = data;
+
+ const { filterOpts, setSelected } = props;
+ const { ascending, inactive, query, smallValues, sortType } = filterOpts;
+ const { propName, inDeciseconds } = SORTING_TYPES[sortType];
+
+ const [bars, setBars] = useLocalState('bars', inDeciseconds);
+
+ const sorted = subsystems
+ .filter((subsystem) => {
+ // Matches search
+ const nameMatchesQuery = subsystem.name
+ .toLowerCase()
+ .includes(query?.toLowerCase());
+
+ // Filters out based on inactive
+ if (inactive && (!!subsystem.doesnt_fire || !subsystem.can_fire)) {
+ return false;
+ }
+
+ // Filters out based on small values
+ if (smallValues && subsystem[propName] < 1) {
+ return false;
+ }
+
+ return nameMatchesQuery;
+ })
+ .sort((a, b) => {
+ if (ascending) {
+ return a[propName] > b[propName] ? 1 : -1;
+ } else {
+ return b[propName] > a[propName] ? 1 : -1;
+ }
+ });
+
+ // Gets our totals for bar display
+ const totals: number[] = [];
+ let currentMax = 0;
+ if (inDeciseconds) {
+ for (let i = 0; i < sorted.length; i++) {
+ const value = sorted[i][propName];
+ if (typeof value !== 'number') {
+ continue;
+ }
+
+ totals.push(value);
+ }
+
+ currentMax = Math.max(...totals);
+ }
+
+ // Toggles default bar view for valid cases
+ if (inDeciseconds !== lastInDeciseconds) {
+ lastInDeciseconds = inDeciseconds;
+ if (inDeciseconds && !bars) {
+ setBars(true);
+ } else if (!inDeciseconds && bars) {
+ setBars(false);
+ }
+ }
+
+ return (
+
+
+ ({sorted.length} / {subsystems.length})
+
+
+
+
+
+ }
+ >
+
+ {sorted.map((subsystem) => (
+
+ ))}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/contants.ts b/tgui/packages/tgui/interfaces/ControllerOverview/contants.ts
new file mode 100644
index 00000000000..accad503412
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/contants.ts
@@ -0,0 +1,48 @@
+type SortType = {
+ label: string;
+ propName: string;
+ inDeciseconds: boolean;
+};
+
+export const SORTING_TYPES: readonly SortType[] = [
+ {
+ label: 'Alphabetical',
+ propName: 'name',
+ inDeciseconds: false,
+ },
+ {
+ label: 'Cost',
+ propName: 'cost_ms',
+ inDeciseconds: true,
+ },
+ {
+ label: 'Init Order',
+ propName: 'init_order',
+ inDeciseconds: false,
+ },
+ {
+ label: 'Last Fire',
+ propName: 'last_fire',
+ inDeciseconds: false,
+ },
+ {
+ label: 'Next Fire',
+ propName: 'next_fire',
+ inDeciseconds: false,
+ },
+ {
+ label: 'Tick Usage',
+ propName: 'tick_usage',
+ inDeciseconds: true,
+ },
+ {
+ label: 'Avg Usage Per Tick',
+ propName: 'usage_per_tick',
+ inDeciseconds: true,
+ },
+ {
+ label: 'Subsystem Overtime',
+ propName: 'overtime',
+ inDeciseconds: true,
+ },
+];
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/filters.ts b/tgui/packages/tgui/interfaces/ControllerOverview/filters.ts
new file mode 100644
index 00000000000..5f1a8c470d5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/filters.ts
@@ -0,0 +1,48 @@
+import type { SortType } from './types';
+
+export type FilterState = {
+ ascending: boolean;
+ inactive: boolean;
+ query: string;
+ smallValues: boolean;
+ sortType: SortType;
+};
+
+export enum FilterAction {
+ Ascending = 'SET_SORT_ASCENDING',
+ Inactive = 'SET_FILTER_INACTIVE',
+ SmallValues = 'SET_FILTER_SMALL_VALUES',
+ SortType = 'SET_SORT_TYPE',
+ Query = 'SET_FILTER_QUERY',
+ Update = 'UPDATE_FILTER',
+}
+
+type Action =
+ | { type: FilterAction.Ascending; payload: boolean }
+ | { type: FilterAction.Inactive; payload: boolean }
+ | { type: FilterAction.SmallValues; payload: boolean }
+ | { type: FilterAction.SortType; payload: SortType }
+ | { type: FilterAction.Query; payload: string }
+ | { type: FilterAction.Update; payload: Partial };
+
+export const filterReducer = (
+ state: FilterState,
+ action: Action,
+): FilterState => {
+ switch (action.type) {
+ case FilterAction.Inactive:
+ return { ...state, inactive: action.payload };
+ case FilterAction.SmallValues:
+ return { ...state, smallValues: action.payload };
+ case FilterAction.Ascending:
+ return { ...state, ascending: action.payload };
+ case FilterAction.SortType:
+ return { ...state, sortType: action.payload };
+ case FilterAction.Query:
+ return { ...state, query: action.payload };
+ case FilterAction.Update:
+ return { ...state, ...action.payload };
+ default:
+ return state;
+ }
+};
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/index.tsx b/tgui/packages/tgui/interfaces/ControllerOverview/index.tsx
new file mode 100644
index 00000000000..f295200f500
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/index.tsx
@@ -0,0 +1,159 @@
+import { Button, Dropdown, Input, Section, Stack } from 'tgui-core/components';
+
+import { useLocalState } from '../../backend';
+import { Window } from '../../layouts';
+import { SORTING_TYPES } from './contants';
+import { FilterAction, filterReducer, type FilterState } from './filters';
+import { OverviewSection } from './OverviewSection';
+import { SubsystemDialog } from './SubsystemDialog';
+import { SubsystemViews } from './SubsystemViews';
+import { SortType, type SubsystemData } from './types';
+
+export const ControllerOverview = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const ControllerContent = () => {
+ const [state, setState] = useLocalState('controllerFilter', {
+ ascending: true,
+ inactive: true,
+ query: '',
+ smallValues: false,
+ sortType: SortType.Name,
+ });
+
+ const [selected, setSelected] = useLocalState(
+ 'selected',
+ undefined,
+ );
+
+ const { label, inDeciseconds } =
+ SORTING_TYPES?.[state.sortType] || SORTING_TYPES[0];
+
+ const dispatch = (action: { type: FilterAction; payload: any }) => {
+ setState(filterReducer(state, action));
+ };
+
+ const onSelectionHandler = (value: string) => {
+ const updates: Partial = {
+ sortType: SORTING_TYPES.findIndex((type) => type.label === value),
+ };
+
+ if (updates.sortType === undefined) return;
+
+ const { inDeciseconds } = SORTING_TYPES[updates.sortType];
+
+ updates.ascending = !inDeciseconds;
+ updates.smallValues = inDeciseconds;
+
+ dispatch({ type: FilterAction.Update, payload: updates });
+ };
+
+ return (
+
+ {selected && (
+ setSelected(undefined)}
+ subsystem={selected}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ControllerOverview/types.ts b/tgui/packages/tgui/interfaces/ControllerOverview/types.ts
new file mode 100644
index 00000000000..9dc5d56543b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ControllerOverview/types.ts
@@ -0,0 +1,35 @@
+import type { BooleanLike } from 'common/react';
+
+export type SubsystemData = {
+ can_fire: BooleanLike;
+ cost_ms: number;
+ doesnt_fire: BooleanLike;
+ init_order: number;
+ initialization_failure_message: string | undefined;
+ initialized: BooleanLike;
+ last_fire: number;
+ name: string;
+ next_fire: number;
+ ref: string;
+ overtime: number;
+ tick_usage: number;
+ usage_per_tick: number;
+};
+
+export type ControllerData = {
+ world_time: number;
+ fast_update: BooleanLike;
+ rolling_length: number;
+ map_cpu: number;
+ subsystems: SubsystemData[];
+};
+
+export enum SortType {
+ Name,
+ Cost,
+ InitOrder,
+ LastFire,
+ NextFire,
+ TickUsage,
+ TickOverrun,
+}
diff --git a/tools/ticked_file_enforcement/schemas/cev_eris_dme.json b/tools/ticked_file_enforcement/schemas/cev_eris_dme.json
index 3b98d87245e..74cd1c6770c 100644
--- a/tools/ticked_file_enforcement/schemas/cev_eris_dme.json
+++ b/tools/ticked_file_enforcement/schemas/cev_eris_dme.json
@@ -6,7 +6,6 @@
"__HELPERS/logging/antagonists.dm",
"__HELPERS/russian.dm",
"modules/library/old_book.dm",
- "controllers/subsystems/sanity.dm",
"datums/components/ai_like_control.dm",
"datums/craft/recipes/_todo.dm",
"datums/craft/recipes/testing.dm",