There were [station_vault] credits collected by crew this shift.
"
+ parts += "
General Statistics:"
+ parts += "There were [station_vault] credits collected by crew this shift.
"
if(total_players > 0)
parts += "An average of [station_vault/total_players] credits were collected.
"
// log_econ("Roundend credit total: [station_vault] credits. Average Credits: [station_vault/total_players]")
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index 64b4129024..1aca8959c2 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -1,7 +1,7 @@
-//#define TESTING //By using the testing("message") proc you can create debug-feedback for people with this
+//#define TESTING //By using the testing("message") proc you can create debug-feedback for people with this
//uncommented, but not visible in the release version)
-//#define DATUMVAR_DEBUGGING_MODE //Enables the ability to cache datum vars and retrieve later for debugging which vars changed.
+//#define DATUMVAR_DEBUGGING_MODE //Enables the ability to cache datum vars and retrieve later for debugging which vars changed.
// Comment this out if you are debugging problems that might be obscured by custom error handling in world/Error
#ifdef DEBUG
@@ -11,34 +11,44 @@
#ifdef TESTING
#define DATUMVAR_DEBUGGING_MODE
-/*
-* Enables extools-powered reference tracking system, letting you see what is referencing objects that refuse to hard delete.
-*
-* * Requires TESTING to be defined to work.
-*/
+///Used to find the sources of harddels, quite laggy, don't be surpised if it freezes your client for a good while
//#define REFERENCE_TRACKING
+#ifdef REFERENCE_TRACKING
-///Method of tracking references without using extools. Slower, kept to avoid over-reliance on extools.
-//#define LEGACY_REFERENCE_TRACKING
-#ifdef LEGACY_REFERENCE_TRACKING
+///alternate to reftracking, extool variant
+//#define EXTOOLS_REFERENCE_TRACKING
-///Use the legacy reference on things hard deleting by default.
+///Should we be logging our findings or not
+#define REFERENCE_TRACKING_LOG
+
+///Used for doing dry runs of the reference finder, to test for feature completeness
+//#define REFERENCE_TRACKING_DEBUG
+
+///Run a lookup on things hard deleting by default.
//#define GC_FAILURE_HARD_LOOKUP
#ifdef GC_FAILURE_HARD_LOOKUP
#define FIND_REF_NO_CHECK_TICK
#endif //ifdef GC_FAILURE_HARD_LOOKUP
-#endif //ifdef LEGACY_REFERENCE_TRACKING
+#endif //ifdef REFERENCE_TRACKING
-//#define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green
+/*
+* Enables debug messages for every single reaction step. This is 1 message per 0.5s for a SINGLE reaction. Useful for tracking down bugs/asking me for help in the main reaction handiler (equilibrium.dm).
+*
+* * Requires TESTING to be defined to work.
+*/
+//#define REAGENTS_TESTING
+// #define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green
+// #define TRACK_MAX_SHARE //Allows max share tracking, for use in the atmos debugging ui
#endif //ifdef TESTING
-//#define UNIT_TESTS //Enables unit tests via TEST_RUN_PARAMETER
-#ifndef PRELOAD_RSC //set to:
-#define PRELOAD_RSC 2 // 0 to allow using external resources or on-demand behaviour;
-#endif // 1 to use the default behaviour;
- // 2 for preloading absolutely everything;
+//#define UNIT_TESTS //If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between
+
+#ifndef PRELOAD_RSC //set to:
+#define PRELOAD_RSC 2 // 0 to allow using external resources or on-demand behaviour;
+#endif // 1 to use the default behaviour;
+ // 2 for preloading absolutely everything;
#ifdef LOWMEMORYMODE
#define FORCE_MAP "_maps/runtimestation.json"
@@ -47,7 +57,7 @@
//Update this whenever you need to take advantage of more recent byond features
#define MIN_COMPILER_VERSION 513
#define MIN_COMPILER_BUILD 1514
-#if DM_VERSION < MIN_COMPILER_VERSION || DM_BUILD < MIN_COMPILER_BUILD
+#if (DM_VERSION < MIN_COMPILER_VERSION || DM_BUILD < MIN_COMPILER_BUILD) && !defined(SPACEMAN_DMM)
//Don't forget to update this part
#error Your version of BYOND is too out-of-date to compile this project. Go to https://secure.byond.com/download and update.
#error You need version 513.1514 or higher
@@ -58,10 +68,6 @@
#warn compiling in TESTING mode. testing() debug messages will be visible.
#endif
-#ifdef GC_FAILURE_HARD_LOOKUP
-#define FIND_REF_NO_CHECK_TICK
-#endif
-
#ifdef CIBUILDING
#define UNIT_TESTS
#endif
@@ -70,6 +76,24 @@
#define TESTING
#endif
+#if defined(UNIT_TESTS)
+//Hard del testing defines
+#define REFERENCE_TRACKING
+#define REFERENCE_TRACKING_DEBUG
+#define FIND_REF_NO_CHECK_TICK
+#endif
+
+#ifdef TGS
+// TGS performs its own build of dm.exe, but includes a prepended TGS define.
+#define CBT
+#endif
+
// A reasonable number of maximum overlays an object needs
// If you think you need more, rethink it
#define MAX_ATOM_OVERLAYS 100
+
+#if !defined(CBT) && !defined(SPACEMAN_DMM)
+#warn Building with Dream Maker is no longer supported and will result in errors.
+#warn In order to build, run BUILD.bat in the root directory.
+#warn Consider switching to VSCode editor instead, where you can press Ctrl+Shift+B to build.
+#endif
diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm
index 28f657828b..64c96cae3b 100644
--- a/code/_globalvars/lists/flavor_misc.dm
+++ b/code/_globalvars/lists/flavor_misc.dm
@@ -6,6 +6,7 @@ GLOBAL_LIST_EMPTY(hair_styles_female_list) //stores only hair names
GLOBAL_LIST_EMPTY(facial_hair_styles_list) //stores /datum/sprite_accessory/facial_hair indexed by name
GLOBAL_LIST_EMPTY(facial_hair_styles_male_list) //stores only hair names
GLOBAL_LIST_EMPTY(facial_hair_styles_female_list) //stores only hair names
+GLOBAL_LIST_EMPTY(hair_gradients_list) //stores /datum/sprite_accessory/hair_gradient indexed by name
//Underwear
GLOBAL_LIST_EMPTY_TYPED(underwear_list, /datum/sprite_accessory/underwear/bottom) //stores bottoms indexed by name
GLOBAL_LIST_EMPTY(underwear_m) //stores only underwear name
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index 9321427387..1b72cc71b1 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -63,6 +63,8 @@
return ShiftClickOn(A)
if(modifiers["alt"]) // alt and alt-gr (rightalt)
return AltClickOn(A)
+ if(modifiers["ctrl"] && modifiers["right"]) //CIT CHANGE - right click ctrl for a new form of dropping items
+ return CtrlRightClickOn(A, params) //CIT CHANGE
if(modifiers["ctrl"])
return CtrlClickOn(A)
@@ -295,6 +297,46 @@
if(!(flags & COMPONENT_DENY_EXAMINATE) && user.client && (user.client.eye == user || user.client.eye == user.loc || flags & COMPONENT_ALLOW_EXAMINATE))
user.examinate(src)
+/*
+ Ctrl + Right click
+ Combat mode feature
+ Drop item in hand at position.
+*/
+/atom/proc/CtrlRightClickOn(atom/A, params)
+ if(isliving(src) && Adjacent(A)) //honestly only humans can do this given it's combat mode but if it's implemented for any other mobs...
+ var/mob/living/L = src
+ if(L.incapacitated())
+ return
+ var/obj/item/I = L.get_active_held_item()
+ var/turf/T = get_turf(A)
+ if(T)
+ if(I) //drop item at cursor.
+ if(T.density) //no, you can't use your funny blue cube or red cube to clip into the fucking wall.
+ return
+ for(var/atom/C in T.contents) //nor can you clip into a window or a door/false wall that's not open.
+ if(C.opacity || (((C.flags_1 & PREVENT_CLICK_UNDER_1) > 0) != (istype(C,/obj/machinery/door) && !C.density))) //XOR operation within because doors always have PREVENT_CLICK_UNDER_1 flag enabled. Dumb, I know.
+ return
+ if(L.transferItemToLoc(I, T))
+ var/list/click_params = params2list(params)
+ //Center the icon where the user clicked. (shamelessly stole code from tables)
+ if(!click_params || !click_params["icon-x"] || !click_params["icon-y"])
+ return
+ //Clamp it so that the icon never moves more than 16 pixels in either direction
+ I.pixel_x = clamp(text2num(click_params["icon-x"]) - 16, -(world.icon_size/2), world.icon_size/2)
+ I.pixel_y = clamp(text2num(click_params["icon-y"]) - 16, -(world.icon_size/2), world.icon_size/2)
+ return TRUE
+ else if(isitem(A) && L.has_active_hand()) //if they have an open hand they'll rotate the item instead.
+ var/obj/item/I2 = A
+ if(!I2.anchored)
+ var/matrix/ntransform = matrix(I2.transform)
+ ntransform.Turn(15)
+ animate(I2, transform = ntransform, time = 2)
+ return TRUE
+ else
+ A.CtrlClick(src)
+
+
+
/*
Ctrl click
For most objects, pull
diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm
index cf3f45e196..743d75b557 100644
--- a/code/_onclick/cyborg.dm
+++ b/code/_onclick/cyborg.dm
@@ -43,7 +43,7 @@
INVOKE_ASYNC(aicamera, /obj/item/camera.proc/captureimage, A, usr)
return
- var/obj/item/W = get_active_held_item()
+ var/obj/item/W = get_active_held_item(TRUE)
if(!W && A.Adjacent(src) && (isobj(A) || ismob(A)))
var/atom/movable/C = A
diff --git a/code/controllers/admin.dm b/code/controllers/admin.dm
index 19fef28597..5c767ecb1b 100644
--- a/code/controllers/admin.dm
+++ b/code/controllers/admin.dm
@@ -1,14 +1,25 @@
// Clickable stat() button.
/obj/effect/statclick
name = "Initializing..."
+ // blocks_emissive = NONE
var/target
INITIALIZE_IMMEDIATE(/obj/effect/statclick)
-/obj/effect/statclick/Initialize(mapload, text, target) //Don't port this to Initialize it's too critical
+/obj/effect/statclick/Initialize(mapload, text, target)
. = ..()
name = text
src.target = target
+ if(istype(target, /datum)) //Harddel man bad
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/cleanup)
+
+/obj/effect/statclick/Destroy()
+ target = null
+ return ..()
+
+/obj/effect/statclick/proc/cleanup()
+ SIGNAL_HANDLER
+ qdel(src)
/obj/effect/statclick/proc/update(text)
name = text
@@ -51,3 +62,30 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick)
SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Failsafe Controller")
message_admins("Admin [key_name_admin(usr)] has restarted the [controller] controller.")
+
+/client/proc/debug_controller()
+ set category = "Debug"
+ set name = "Debug Controller"
+ set desc = "Debug the various periodic loop controllers for the game (be careful!)"
+
+ if(!holder)
+ return
+
+ var/list/controllers = list()
+ var/list/controller_choices = list()
+
+ for (var/datum/controller/controller in world)
+ if (istype(controller, /datum/controller/subsystem))
+ continue
+ controllers["[controller] (controller.type)"] = controller //we use an associated list to ensure clients can't hold references to controllers
+ controller_choices += "[controller] (controller.type)"
+
+ var/datum/controller/controller_string = input("Select controller to debug", "Debug Controller") as null|anything in controller_choices
+ var/datum/controller/controller = controllers[controller_string]
+
+ if (!istype(controller))
+ return
+ debug_variables(controller)
+
+ SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Failsafe Controller")
+ message_admins("Admin [key_name_admin(usr)] is debugging the [controller] controller.")
diff --git a/code/controllers/configuration/entries/admin.dm b/code/controllers/configuration/entries/admin.dm
index 3b5b437742..1f9b5d460f 100644
--- a/code/controllers/configuration/entries/admin.dm
+++ b/code/controllers/configuration/entries/admin.dm
@@ -39,7 +39,9 @@
/datum/config_entry/flag/announce_admin_login
-/datum/config_entry/string/centcom_ban_db // URL for the CentCom Galactic Ban DB API
+/datum/config_entry/string/centcom_ban_db // URL for the CentCom Galactic Ban DB API
+
+/datum/config_entry/string/centcom_source_whitelist
/datum/config_entry/flag/autoadmin // if autoadmin is enabled
protection = CONFIG_ENTRY_LOCKED
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index 8ecd7c0c60..d828457fa7 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -333,3 +333,12 @@
config_entry_value = 0.333
min_val = 0
integer = FALSE
+
+/datum/config_entry/number/hard_deletes_overrun_threshold
+ integer = FALSE
+ min_val = 0
+ default = 0.5
+
+/datum/config_entry/number/hard_deletes_overrun_limit
+ default = 0
+ min_val = 0
diff --git a/code/controllers/failsafe.dm b/code/controllers/failsafe.dm
index a15056e442..04b88eb3be 100644
--- a/code/controllers/failsafe.dm
+++ b/code/controllers/failsafe.dm
@@ -15,7 +15,7 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
// The alert level. For every failed poke, we drop a DEFCON level. Once we hit DEFCON 1, restart the MC.
var/defcon = 5
//the world.time of the last check, so the mc can restart US if we hang.
- // (Real friends look out for *eachother*)
+ // (Real friends look out for *eachother*)
var/lasttick = 0
// Track the MC iteration to make sure its still on track.
@@ -33,6 +33,22 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
/datum/controller/failsafe/Initialize()
set waitfor = FALSE
Failsafe.Loop()
+ if (!Master || defcon == 0) //Master is gone/not responding and Failsafe just exited its loop
+ defcon = 3 //Reset defcon level as its used inside the emergency loop
+ while (defcon > 0)
+ var/recovery_result = emergency_loop()
+ if (recovery_result == 1) //Exit emergency loop and delete self if it was able to recover MC
+ break
+ else if (defcon == 1) //Exit Failsafe if we weren't able to recover the MC in the last stage
+ log_game("FailSafe: Failed to recover MC while in emergency state. Failsafe exiting.")
+ message_admins(span_boldannounce("Failsafe failed criticaly while trying to recreate broken MC. Please manually fix the MC or reboot the server. Failsafe exiting now."))
+ message_admins(span_boldannounce("You can try manually calling these two procs:."))
+ message_admins(span_boldannounce("/proc/recover_all_SS_and_recreate_master: Most stuff should still function but expect instability/runtimes/broken stuff."))
+ message_admins(span_boldannounce("/proc/delete_all_SS_and_recreate_master: Most stuff will be broken but basic stuff like movement and chat should still work."))
+ else if (recovery_result == -1) //Failed to recreate MC
+ defcon--
+ sleep(initial(processing_interval)) //Wait a bit until the next try
+
if(!QDELETED(src))
qdel(src) //when Loop() returns, we delete ourselves and let the mc recreate us
@@ -45,42 +61,56 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
while(running)
lasttick = world.time
if(!Master)
- // Replace the missing Master! This should never, ever happen.
- new /datum/controller/master()
+ // Break out of the main loop so we go into emergency state
+ break
// Only poke it if overrides are not in effect.
if(processing_interval > 0)
if(Master.processing && Master.iteration)
+ if (defcon > 1 && (!Master.stack_end_detector || !Master.stack_end_detector.check()))
+
+ to_chat(GLOB.admins, span_boldannounce("ERROR: The Master Controller code stack has exited unexpectedly, Restarting..."))
+ defcon = 0
+ var/rtn = Recreate_MC()
+ if(rtn > 0)
+ master_iteration = 0
+ to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
+ else if(rtn < 0)
+ log_game("FailSafe: Could not restart MC, runtime encountered. Entering defcon 0")
+ to_chat(GLOB.admins, span_boldannounce("ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying."))
// Check if processing is done yet.
if(Master.iteration == master_iteration)
switch(defcon)
if(4,5)
--defcon
- if(3)
- message_admins("
Notice: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks.")
- --defcon
- if(2)
- to_chat(GLOB.admins, "
Warning: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks. Automatic restart in [processing_interval] ticks.")
- --defcon
- if(1)
- to_chat(GLOB.admins, "
Warning: DEFCON [defcon_pretty()]. The Master Controller has still not fired within the last [(5-defcon) * processing_interval] ticks. Killing and restarting...")
+ if(3)
+ message_admins(span_adminnotice("Notice: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks."))
+ --defcon
+
+ if(2)
+ to_chat(GLOB.admins, span_boldannounce("Warning: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks. Automatic restart in [processing_interval] ticks."))
+ --defcon
+
+ if(1)
+ to_chat(GLOB.admins, span_boldannounce("Warning: DEFCON [defcon_pretty()]. The Master Controller has still not fired within the last [(5-defcon) * processing_interval] ticks. Killing and restarting..."))
--defcon
var/rtn = Recreate_MC()
if(rtn > 0)
defcon = 4
master_iteration = 0
- to_chat(GLOB.admins, "
MC restarted successfully")
+ to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
else if(rtn < 0)
log_game("FailSafe: Could not restart MC, runtime encountered. Entering defcon 0")
- to_chat(GLOB.admins, "
ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying.")
+ to_chat(GLOB.admins, span_boldannounce("ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying."))
//if the return number was 0, it just means the mc was restarted too recently, and it just needs some time before we try again
//no need to handle that specially when defcon 0 can handle it
+
if(0) //DEFCON 0! (mc failed to restart)
var/rtn = Recreate_MC()
if(rtn > 0)
defcon = 4
master_iteration = 0
- to_chat(GLOB.admins, "
MC restarted successfully")
+ to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
else
defcon = min(defcon + 1,5)
master_iteration = Master.iteration
@@ -92,6 +122,57 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
defcon = 5
sleep(initial(processing_interval))
+//Emergency loop used when Master got deleted or the main loop exited while Defcon == 0
+//Loop is driven externally so runtimes only cancel the current recovery attempt
+/datum/controller/failsafe/proc/emergency_loop()
+ //The code in this proc should be kept as simple as possible, anything complicated like to_chat might rely on master existing and runtime
+ //The goal should always be to get a new Master up and running before anything else
+ . = -1
+ switch (defcon) //The lower defcon goes the harder we try to fix the MC
+ if (2 to 3) //Try to normally recreate the MC two times
+ . = Recreate_MC()
+ if (1) //Delete the old MC first so we don't transfer any info, in case that caused any issues
+ del(Master)
+ . = Recreate_MC()
+
+ if (. == 1) //We were able to create a new master
+ master_iteration = 0
+ SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
+ Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
+ to_chat(GLOB.admins, span_adminnotice("Failsafe recovered MC while in emergency state [defcon_pretty()]"))
+ else
+ log_game("FailSafe: Failsafe in emergency state and was unable to recreate MC while in defcon state [defcon_pretty()].")
+ message_admins(span_boldannounce("Failsafe in emergency state and master down, trying to recreate MC while in defcon level [defcon_pretty()] failed."))
+
+///Recreate all SSs which will still cause data survive due to Recover(), the new Master will then find and take them from global.vars
+/proc/recover_all_SS_and_recreate_master()
+ del(Master)
+ var/list/subsytem_types = subtypesof(/datum/controller/subsystem)
+ sortTim(subsytem_types, /proc/cmp_subsystem_init)
+ for(var/I in subsytem_types)
+ new I
+ . = Recreate_MC()
+ if (. == 1) //We were able to create a new master
+ SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
+ Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
+ to_chat(GLOB.admins, span_adminnotice("MC successfully recreated after recovering all subsystems!"))
+ else
+ message_admins(span_boldannounce("Failed to create new MC!"))
+
+///Delete all existing SS to basically start over
+/proc/delete_all_SS_and_recreate_master()
+ del(Master)
+ for(var/global_var in global.vars)
+ if (istype(global.vars[global_var], /datum/controller/subsystem))
+ del(global.vars[global_var])
+ . = Recreate_MC()
+ if (. == 1) //We were able to create a new master
+ SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
+ Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
+ to_chat(GLOB.admins, span_adminnotice("MC successfully recreated after deleting and recreating all subsystems!"))
+ else
+ message_admins(span_boldannounce("Failed to create new MC!"))
+
/datum/controller/failsafe/proc/defcon_pretty()
return defcon
diff --git a/code/controllers/globals.dm b/code/controllers/globals.dm
index 21f022acfd..7b5cc94d36 100644
--- a/code/controllers/globals.dm
+++ b/code/controllers/globals.dm
@@ -14,7 +14,7 @@ GLOBAL_REAL(GLOB, /datum/controller/global_vars)
var/datum/controller/exclude_these = new
gvars_datum_in_built_vars = exclude_these.vars + list(NAMEOF(src, gvars_datum_protected_varlist), NAMEOF(src, gvars_datum_in_built_vars), NAMEOF(src, gvars_datum_init_order))
- QDEL_IN(exclude_these, 0) //signal logging isn't ready
+ QDEL_IN(exclude_these, 0) //signal logging isn't ready
log_world("[vars.len - gvars_datum_in_built_vars.len] global variables")
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index 59ac68960c..055271fd86 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -18,15 +18,17 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
/datum/controller/master
name = "Master"
- // Are we processing (higher values increase the processing delay by n ticks)
+ /// Are we processing (higher values increase the processing delay by n ticks)
var/processing = TRUE
- // How many times have we ran
+ /// How many times have we ran
var/iteration = 0
+ /// Stack end detector to detect stack overflows that kill the mc's main loop
+ var/datum/stack_end_detector/stack_end_detector
- // world.time of last fire, for tracking lag outside of the mc
+ /// world.time of last fire, for tracking lag outside of the mc
var/last_run
- // List of subsystems to process().
+ /// List of subsystems to process().
var/list/subsystems
// Vars for keeping track of tick drift.
@@ -34,25 +36,27 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/init_time
var/tickdrift = 0
+ /// How long is the MC sleeping between runs, read only (set by Loop() based off of anti-tick-contention heuristics)
var/sleep_delta = 1
- ///Only run ticker subsystems for the next n ticks.
+ /// Only run ticker subsystems for the next n ticks.
var/skip_ticks = 0
+ /// makes the mc main loop runtime
var/make_runtime = FALSE
- var/initializations_finished_with_no_players_logged_in //I wonder what this could be?
+ var/initializations_finished_with_no_players_logged_in //I wonder what this could be?
- // The type of the last subsystem to be process()'d.
+ /// The type of the last subsystem to be fire()'d.
var/last_type_processed
- var/datum/controller/subsystem/queue_head //Start of queue linked list
- var/datum/controller/subsystem/queue_tail //End of queue linked list (used for appending to the list)
+ var/datum/controller/subsystem/queue_head //!Start of queue linked list
+ var/datum/controller/subsystem/queue_tail //!End of queue linked list (used for appending to the list)
var/queue_priority_count = 0 //Running total so that we don't have to loop thru the queue each run to split up the tick
var/queue_priority_count_bg = 0 //Same, but for background subsystems
- var/map_loading = FALSE //Are we loading in a new map?
+ var/map_loading = FALSE //!Are we loading in a new map?
- var/current_runlevel //for scheduling different subsystems for different stages of the round
+ var/current_runlevel //!for scheduling different subsystems for different stages of the round
var/sleep_offline_after_initializations = TRUE
var/static/restart_clear = 0
@@ -61,8 +65,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/static/random_seed
- //current tick limit, assigned before running a subsystem.
- //used by CHECK_TICK as well so that the procs subsystems call can obey that SS's tick limits
+ ///current tick limit, assigned before running a subsystem.
+ ///used by CHECK_TICK as well so that the procs subsystems call can obey that SS's tick limits
var/static/current_ticklimit = TICK_LIMIT_RUNNING
/datum/controller/master/New()
@@ -81,15 +85,27 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/list/_subsystems = list()
subsystems = _subsystems
if (Master != src)
- if (istype(Master))
+ if (istype(Master)) //If there is an existing MC take over his stuff and delete it
Recover()
qdel(Master)
+ Master = src
else
+ //Code used for first master on game boot or if existing master got deleted
+ Master = src
var/list/subsytem_types = subtypesof(/datum/controller/subsystem)
sortTim(subsytem_types, /proc/cmp_subsystem_init)
+ //Find any abandoned subsystem from the previous master (if there was any)
+ var/list/existing_subsystems = list()
+ for(var/global_var in global.vars)
+ if (istype(global.vars[global_var], /datum/controller/subsystem))
+ existing_subsystems += global.vars[global_var]
+ //Either init a new SS or if an existing one was found use that
for(var/I in subsytem_types)
- _subsystems += new I
- Master = src
+ var/datum/controller/subsystem/existing_subsystem = locate(I) in existing_subsystems
+ if (istype(existing_subsystem))
+ _subsystems += existing_subsystem
+ else
+ _subsystems += new I
if(!GLOB)
new /datum/controller/global_vars
@@ -109,7 +125,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
log_world("Shutdown complete")
// Returns 1 if we created a new mc, 0 if we couldn't due to a recent restart,
-// -1 if we encountered a runtime trying to recreate it
+// -1 if we encountered a runtime trying to recreate it
/proc/Recreate_MC()
. = -1 //so if we runtime, things know we failed
if (world.time < Master.restart_timeout)
@@ -120,7 +136,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/delay = 50 * ++Master.restart_count
Master.restart_timeout = world.time + delay
Master.restart_clear = world.time + (delay * 2)
- Master.processing = FALSE //stop ticking this one
+ if (Master) //Can only do this if master hasn't been deleted
+ Master.processing = FALSE //stop ticking this one
try
new/datum/controller/master()
catch
@@ -156,22 +173,22 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
msg = "The [BadBoy.name] subsystem seems to be destabilizing the MC and will be offlined."
BadBoy.flags |= SS_NO_FIRE
if(msg)
- to_chat(GLOB.admins, "
[msg]")
+ to_chat(GLOB.admins, span_boldannounce("[msg]"))
log_world(msg)
if (istype(Master.subsystems))
if(FireHim)
- Master.subsystems += new BadBoy.type //NEW_SS_GLOBAL will remove the old one
+ Master.subsystems += new BadBoy.type //NEW_SS_GLOBAL will remove the old one
subsystems = Master.subsystems
current_runlevel = Master.current_runlevel
StartProcessing(10)
else
- to_chat(world, "
The Master Controller is having some issues, we will need to re-initialize EVERYTHING")
+ to_chat(world, span_boldannounce("The Master Controller is having some issues, we will need to re-initialize EVERYTHING"))
Initialize(20, TRUE)
// Please don't stuff random bullshit here,
-// Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize()
+// Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize()
/datum/controller/master/Initialize(delay, init_sss, tgs_prime)
set waitfor = 0
@@ -181,7 +198,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if(init_sss)
init_subtypes(/datum/controller/subsystem, subsystems)
- to_chat(world, "
Initializing subsystems...")
+ to_chat(world, span_boldannounce("Initializing subsystems..."))
// Sort subsystems by init_order, so they initialize in the correct order.
sortTim(subsystems, /proc/cmp_subsystem_init)
@@ -190,7 +207,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
// Initialize subsystems.
current_ticklimit = CONFIG_GET(number/tick_limit_mc_init)
for (var/datum/controller/subsystem/SS in subsystems)
- if (SS.flags & SS_NO_INIT)
+ if (SS.flags & SS_NO_INIT || SS.initialized) //Don't init SSs with the correspondig flag or if they already are initialzized
continue
SS.Initialize(REALTIMEOFDAY)
CHECK_TICK
@@ -198,7 +215,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initializations complete within [time] second[time == 1 ? "" : "s"]!"
- to_chat(world, "
[msg]")
+ to_chat(world, span_boldannounce("[msg]"))
log_world(msg)
if (!current_runlevel)
@@ -255,11 +272,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
/datum/controller/master/proc/Loop()
. = -1
//Prep the loop (most of this is because we want MC restarts to reset as much state as we can, and because
- // local vars rock
+ // local vars rock
//all this shit is here so that flag edits can be refreshed by restarting the MC. (and for speed)
var/list/tickersubsystems = list()
- var/list/runlevel_sorted_subsystems = list(list()) //ensure we always have at least one runlevel
+ var/list/runlevel_sorted_subsystems = list(list()) //ensure we always have at least one runlevel
var/timer = world.time
for (var/thing in subsystems)
var/datum/controller/subsystem/SS = thing
@@ -305,8 +322,12 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/error_level = 0
var/sleep_delta = 1
var/list/subsystems_to_check
- //the actual loop.
+ //setup the stack overflow detector
+ stack_end_detector = new()
+ var/datum/stack_canary/canary = stack_end_detector.prime_canary()
+ canary.use_variable()
+ //the actual loop.
while (1)
tickdrift = max(0, MC_AVERAGE_FAST(tickdrift, (((REALTIMEOFDAY - init_timeofday) - (world.time - init_time)) / world.tick_lag)))
var/starting_tick_usage = TICK_USAGE
@@ -317,7 +338,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
//Anti-tick-contention heuristics:
//if there are mutiple sleeping procs running before us hogging the cpu, we have to run later.
- // (because sleeps are processed in the order received, longer sleeps are more likely to run first)
+ // (because sleeps are processed in the order received, longer sleeps are more likely to run first)
if (starting_tick_usage > TICK_LIMIT_MC) //if there isn't enough time to bother doing anything this tick, sleep a bit.
sleep_delta *= 2
current_ticklimit = TICK_LIMIT_RUNNING * 0.5
@@ -423,6 +444,10 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
continue
if ((SS_flags & (SS_TICKER|SS_KEEP_TIMING)) == SS_KEEP_TIMING && SS.last_fire + (SS.wait * 0.75) > world.time)
continue
+ if (SS.postponed_fires >= 1)
+ SS.postponed_fires--
+ SS.update_nextfire()
+ continue
SS.enqueue()
. = 1
@@ -439,12 +464,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/tick_precentage
var/tick_remaining
var/ran = TRUE //this is right
- var/ran_non_ticker = FALSE
var/bg_calc //have we swtiched current_tick_budget to background mode yet?
var/tick_usage
//keep running while we have stuff to run and we haven't gone over a tick
- // this is so subsystems paused eariler can use tick time that later subsystems never used
+ // this is so subsystems paused eariler can use tick time that later subsystems never used
while (ran && queue_head && TICK_USAGE < TICK_LIMIT_MC)
ran = FALSE
bg_calc = FALSE
@@ -459,20 +483,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if (!(queue_node_flags & SS_TICKER) && skip_ticks)
queue_node = queue_node.queue_next
continue
- //super special case, subsystems where we can't make them pause mid way through
- //if we can't run them this tick (without going over a tick)
- //we bump up their priority and attempt to run them next tick
- //(unless we haven't even ran anything this tick, since its unlikely they will ever be able run
- // in those cases, so we just let them run)
- if (queue_node_flags & SS_NO_TICK_CHECK)
- if (queue_node.tick_usage > TICK_LIMIT_RUNNING - TICK_USAGE && ran_non_ticker)
- if (!(queue_node_flags & SS_BACKGROUND))
- queue_node.queued_priority += queue_priority_count * 0.1
- queue_priority_count -= queue_node_priority
- queue_priority_count += queue_node.queued_priority
- current_tick_budget -= queue_node_priority
- queue_node = queue_node.queue_next
- continue
if (!bg_calc && (queue_node_flags & SS_BACKGROUND))
current_tick_budget = queue_priority_count_bg
@@ -481,7 +491,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
tick_remaining = TICK_LIMIT_RUNNING - TICK_USAGE
if (current_tick_budget > 0 && queue_node_priority > 0)
- tick_precentage = tick_remaining / (current_tick_budget / queue_node_priority)
+ //Give the subsystem a precentage of the remaining tick based on the remaning priority
+ tick_precentage = tick_remaining * (queue_node_priority / current_tick_budget)
else
tick_precentage = tick_remaining
@@ -489,8 +500,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
current_ticklimit = round(TICK_USAGE + tick_precentage)
- if (!(queue_node_flags & SS_TICKER))
- ran_non_ticker = TRUE
ran = TRUE
queue_node_paused = (queue_node.state == SS_PAUSED || queue_node.state == SS_PAUSING)
@@ -535,14 +544,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
queue_node.last_fire = world.time
queue_node.times_fired++
- if (queue_node_flags & SS_TICKER)
- queue_node.next_fire = world.time + (world.tick_lag * queue_node.wait)
- else if (queue_node_flags & SS_POST_FIRE_TIMING)
- queue_node.next_fire = world.time + queue_node.wait + (world.tick_lag * (queue_node.tick_overrun/100))
- else if (queue_node_flags & SS_KEEP_TIMING)
- queue_node.next_fire += queue_node.wait
- else
- queue_node.next_fire = queue_node.queued_time + queue_node.wait + (world.tick_lag * (queue_node.tick_overrun/100))
+ queue_node.update_nextfire()
queue_node.queued_time = 0
@@ -554,7 +556,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
. = 1
//resets the queue, and all subsystems, while filtering out the subsystem lists
-// called if any mc's queue procs runtime or exit improperly.
+// called if any mc's queue procs runtime or exit improperly.
/datum/controller/master/proc/SoftReset(list/ticker_SS, list/runlevel_SS)
. = 0
log_world("MC: SoftReset called, resetting MC queue state.")
diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm
index 12798f3863..db780ca05b 100644
--- a/code/controllers/subsystem.dm
+++ b/code/controllers/subsystem.dm
@@ -29,11 +29,11 @@
var/initialized = FALSE
/// Set to 0 to prevent fire() calls, mostly for admin use or subsystems that may be resumed later
- /// use the [SS_NO_FIRE] flag instead for systems that never fire to keep it from even being added to list that is checked every tick
+ /// use the [SS_NO_FIRE] flag instead for systems that never fire to keep it from even being added to list that is checked every tick
var/can_fire = TRUE
///Bitmap of what game states can this subsystem fire at. See [RUNLEVELS_DEFAULT] for more details.
- var/runlevels = RUNLEVELS_DEFAULT //points of the game at which the SS can fire
+ var/runlevels = RUNLEVELS_DEFAULT //points of the game at which the SS can fire
/*
* The following variables are managed by the MC and should not be modified directly.
@@ -69,6 +69,9 @@
/// Tracks the amount of completed runs for the subsystem
var/times_fired = 0
+ /// How many fires have we been requested to postpone
+ var/postponed_fires = 0
+
/// Time the subsystem entered the queue, (for timing and priority reasons)
var/queued_time = 0
@@ -122,12 +125,38 @@
dequeue()
can_fire = 0
flags |= SS_NO_FIRE
- Master.subsystems -= src
+ if (Master)
+ Master.subsystems -= src
return ..()
+
+/** Update next_fire for the next run.
+ * reset_time (bool) - Ignore things that would normally alter the next fire, like tick_overrun, and last_fire. (also resets postpone)
+ */
+/datum/controller/subsystem/proc/update_nextfire(reset_time = FALSE)
+ var/queue_node_flags = flags
+
+ if (reset_time)
+ postponed_fires = 0
+ if (queue_node_flags & SS_TICKER)
+ next_fire = world.time + (world.tick_lag * wait)
+ else
+ next_fire = world.time + wait
+ return
+
+ if (queue_node_flags & SS_TICKER)
+ next_fire = world.time + (world.tick_lag * wait)
+ else if (queue_node_flags & SS_POST_FIRE_TIMING)
+ next_fire = world.time + wait + (world.tick_lag * (tick_overrun/100))
+ else if (queue_node_flags & SS_KEEP_TIMING)
+ next_fire += wait
+ else
+ next_fire = queued_time + wait + (world.tick_lag * (tick_overrun/100))
+
+
//Queue it to run.
-// (we loop thru a linked list until we get to the end or find the right point)
-// (this lets us sort our run order correctly without having to re-sort the entire already sorted list)
+// (we loop thru a linked list until we get to the end or find the right point)
+// (this lets us sort our run order correctly without having to re-sort the entire already sorted list)
/datum/controller/subsystem/proc/enqueue()
var/SS_priority = priority
var/SS_flags = flags
@@ -191,9 +220,9 @@
queue_next.queue_prev = queue_prev
if (queue_prev)
queue_prev.queue_next = queue_next
- if (src == Master.queue_tail)
+ if (Master && (src == Master.queue_tail))
Master.queue_tail = queue_prev
- if (src == Master.queue_head)
+ if (Master && (src == Master.queue_head))
Master.queue_head = queue_next
queued_time = 0
if (state == SS_QUEUED)
@@ -217,10 +246,11 @@
//used to initialize the subsystem AFTER the map has loaded
/datum/controller/subsystem/Initialize(start_timeofday)
initialized = TRUE
+ // SEND_SIGNAL(src, COMSIG_SUBSYSTEM_POST_INITIALIZE, start_timeofday)
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!"
- to_chat(world, "
[msg]")
- log_subsystem("INIT", msg)
+ to_chat(world, span_boldannounce("[msg]"))
+ log_subsystem(msg)
return time
/datum/controller/subsystem/stat_entry(msg)
@@ -243,11 +273,10 @@
if (SS_IDLE)
. = " "
-//could be used to postpone a costly subsystem for (default one) var/cycles, cycles
-//for instance, during cpu intensive operations like explosions
+/// Causes the next "cycle" fires to be missed. Effect is accumulative but can reset by calling update_nextfire(reset_time = TRUE)
/datum/controller/subsystem/proc/postpone(cycles = 1)
- if(next_fire - world.time < wait)
- next_fire += (wait*cycles)
+ if (can_fire && cycles >= 1)
+ postponed_fires += cycles
//usually called via datum/controller/subsystem/New() when replacing a subsystem (i.e. due to a recurring crash)
//should attempt to salvage what it can from the old instance of subsystem
@@ -258,7 +287,7 @@
if (NAMEOF(src, can_fire))
//this is so the subsystem doesn't rapid fire to make up missed ticks causing more lag
if (var_value)
- next_fire = world.time + wait
+ update_nextfire(reset_time = TRUE)
if (NAMEOF(src, queued_priority)) //editing this breaks things.
return FALSE
. = ..()
diff --git a/code/controllers/subsystem/air.dm b/code/controllers/subsystem/air.dm
index 260a4c1b29..91e92feee9 100644
--- a/code/controllers/subsystem/air.dm
+++ b/code/controllers/subsystem/air.dm
@@ -30,6 +30,7 @@ SUBSYSTEM_DEF(air)
var/list/networks = list()
var/list/pipenets_needing_rebuilt = list()
var/list/deferred_airs = list()
+ var/cur_deferred_airs = 0
var/max_deferred_airs = 0
var/list/obj/machinery/atmos_machinery = list()
var/list/obj/machinery/atmos_air_machinery = list()
@@ -54,14 +55,22 @@ SUBSYSTEM_DEF(air)
var/equalize_turf_limit = 10
// Max number of turfs to look for a space turf, and max number of turfs that will be decompressed.
var/equalize_hard_turf_limit = 2000
- // Whether equalization should be enabled at all.
+ // Whether equalization is enabled. Can be disabled for performance reasons.
var/equalize_enabled = TRUE
+ // Whether equalization should be enabled.
+ var/should_do_equalization = TRUE
+ // When above 0, won't equalize; performance handling
+ var/eq_cooldown = 0
// Whether turf-to-turf heat exchanging should be enabled.
var/heat_enabled = FALSE
// Max number of times process_turfs will share in a tick.
var/share_max_steps = 3
+ // Target for share_max_steps; can go below this, if it determines the thread is taking too long.
+ var/share_max_steps_target = 3
// Excited group processing will try to equalize groups with total pressure difference less than this amount.
var/excited_group_pressure_goal = 1
+ // Target for excited_group_pressure_goal; can go below this, if it determines the thread is taking too long.
+ var/excited_group_pressure_goal_target = 1
// If this is set to 0, monstermos won't process planet atmos
var/planet_equalize_enabled = 0
@@ -221,6 +230,7 @@ SUBSYSTEM_DEF(air)
// This also happens to do all the commented out stuff below, all in a single separate thread. This is mostly so that the
// waiting is consistent.
if(currentpart == SSAIR_ACTIVETURFS)
+ run_delay_heuristics()
timer = TICK_USAGE_REAL
process_turfs(resumed)
cost_turfs = MC_AVERAGE(cost_turfs, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))
@@ -278,7 +288,8 @@ SUBSYSTEM_DEF(air)
pipenets_needing_rebuilt += atmos_machine
/datum/controller/subsystem/air/proc/process_deferred_airs(resumed = 0)
- max_deferred_airs = max(deferred_airs.len,max_deferred_airs)
+ cur_deferred_airs = deferred_airs.len
+ max_deferred_airs = max(cur_deferred_airs,max_deferred_airs)
while(deferred_airs.len)
var/list/cur_op = deferred_airs[deferred_airs.len]
deferred_airs.len--
@@ -378,6 +389,24 @@ SUBSYSTEM_DEF(air)
return
*/
+/datum/controller/subsystem/air/proc/run_delay_heuristics()
+ if(!equalize_enabled)
+ cost_equalize = 0
+ if(should_do_equalization)
+ eq_cooldown--
+ if(eq_cooldown <= 0)
+ equalize_enabled = TRUE
+ var/total_thread_time = cost_turfs + cost_equalize + cost_groups + cost_post_process
+ if(total_thread_time)
+ var/wait_ms = wait * 100
+ var/delay_threshold = 1-(total_thread_time/wait_ms + cur_deferred_airs / 50)
+ share_max_steps = max(1,round(share_max_steps_target * delay_threshold, 1))
+ eq_cooldown += (1-delay_threshold) * (cost_equalize / total_thread_time) * 2
+ if(eq_cooldown > 0.5)
+ equalize_enabled = FALSE
+ excited_group_pressure_goal = max(0,excited_group_pressure_goal_target * (1 - delay_threshold))
+
+
/datum/controller/subsystem/air/proc/process_turfs(resumed = 0)
if(process_turfs_auxtools(resumed,TICK_REMAINING_MS))
pause()
@@ -402,7 +431,7 @@ SUBSYSTEM_DEF(air)
pause()
/datum/controller/subsystem/air/proc/finish_turf_processing(resumed = 0)
- if(finish_turf_processing_auxtools(TICK_REMAINING_MS))
+ if(finish_turf_processing_auxtools(TICK_REMAINING_MS) || thread_running())
pause()
/datum/controller/subsystem/air/proc/post_process_turfs(resumed = 0)
diff --git a/code/controllers/subsystem/garbage.dm b/code/controllers/subsystem/garbage.dm
index 2d2fac1d13..7f7d301a1d 100644
--- a/code/controllers/subsystem/garbage.dm
+++ b/code/controllers/subsystem/garbage.dm
@@ -29,37 +29,35 @@ SUBSYSTEM_DEF(garbage)
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
init_order = INIT_ORDER_GARBAGE
- var/list/collection_timeout = list(2 MINUTES, 10 SECONDS) // deciseconds to wait before moving something up in the queue to the next level
+ var/list/collection_timeout = list(GC_FILTER_QUEUE, GC_DEL_QUEUE) // deciseconds to wait before moving something up in the queue to the next level
//Stat tracking
- var/delslasttick = 0 // number of del()'s we've done this tick
- var/gcedlasttick = 0 // number of things that gc'ed last tick
+ var/delslasttick = 0 // number of del()'s we've done this tick
+ var/gcedlasttick = 0 // number of things that gc'ed last tick
var/totaldels = 0
var/totalgcs = 0
- var/highest_del_time = 0
- var/highest_del_tickusage = 0
+ var/highest_del_ms = 0
+ var/highest_del_type_string = ""
var/list/pass_counts
var/list/fail_counts
- var/list/items = list() // Holds our qdel_item statistics datums
+ var/list/items = list() // Holds our qdel_item statistics datums
//Queue
var/list/queues
- #ifdef LEGACY_REFERENCE_TRACKING
+ #ifdef REFERENCE_TRACKING
var/list/reference_find_on_fail = list()
+ #ifdef REFERENCE_TRACKING_DEBUG
+ //Should we save found refs. Used for unit testing
+ var/should_save_refs = FALSE
+ #endif
#endif
/datum/controller/subsystem/garbage/PreInit()
- queues = new(GC_QUEUE_COUNT)
- pass_counts = new(GC_QUEUE_COUNT)
- fail_counts = new(GC_QUEUE_COUNT)
- for(var/i in 1 to GC_QUEUE_COUNT)
- queues[i] = list()
- pass_counts[i] = 0
- fail_counts[i] = 0
+ InitQueues()
/datum/controller/subsystem/garbage/stat_entry(msg)
var/list/counts = list()
@@ -90,13 +88,18 @@ SUBSYSTEM_DEF(garbage)
for(var/path in items)
var/datum/qdel_item/I = items[path]
dellog += "Path: [path]"
+ if (I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG)
+ dellog += "\tSUSPENDED FOR LAG"
if (I.failures)
dellog += "\tFailures: [I.failures]"
dellog += "\tqdel() Count: [I.qdels]"
dellog += "\tDestroy() Cost: [I.destroy_time]ms"
if (I.hard_deletes)
- dellog += "\tTotal Hard Deletes [I.hard_deletes]"
+ dellog += "\tTotal Hard Deletes: [I.hard_deletes]"
dellog += "\tTime Spent Hard Deleting: [I.hard_delete_time]ms"
+ dellog += "\tHighest Time Spent Hard Deleting: [I.hard_delete_max]ms"
+ if (I.hard_deletes_over_threshold)
+ dellog += "\tHard Deletes Over Threshold: [I.hard_deletes_over_threshold]"
if (I.slept_destroy)
dellog += "\tSleeps: [I.slept_destroy]"
if (I.no_respect_force)
@@ -122,6 +125,15 @@ SUBSYSTEM_DEF(garbage)
+/datum/controller/subsystem/garbage/proc/InitQueues()
+ if (isnull(queues)) // Only init the queues if they don't already exist, prevents overriding of recovered lists
+ queues = new(GC_QUEUE_COUNT)
+ pass_counts = new(GC_QUEUE_COUNT)
+ fail_counts = new(GC_QUEUE_COUNT)
+ for(var/i in 1 to GC_QUEUE_COUNT)
+ queues[i] = list()
+ pass_counts[i] = 0
+ fail_counts[i] = 0
/datum/controller/subsystem/garbage/proc/HandleQueue(level = GC_QUEUE_CHECK)
if (level == GC_QUEUE_CHECK)
@@ -153,7 +165,6 @@ SUBSYSTEM_DEF(garbage)
if(GCd_at_time > cut_off_time)
break // Everything else is newer, skip them
count++
-
var/refID = L[2]
var/datum/D
D = locate(refID)
@@ -162,8 +173,8 @@ SUBSYSTEM_DEF(garbage)
++gcedlasttick
++totalgcs
pass_counts[level]++
- #ifdef LEGACY_REFERENCE_TRACKING
- reference_find_on_fail -= refID //It's deleted we don't care anymore.
+ #ifdef REFERENCE_TRACKING
+ reference_find_on_fail -= refID //It's deleted we don't care anymore.
#endif
if (MC_TICK_CHECK)
return
@@ -171,35 +182,43 @@ SUBSYSTEM_DEF(garbage)
// Something's still referring to the qdel'd object.
fail_counts[level]++
+
+ #ifdef REFERENCE_TRACKING
+ var/ref_searching = FALSE
+ #endif
+
switch (level)
if (GC_QUEUE_CHECK)
#ifdef REFERENCE_TRACKING
- D.find_references()
- #elif defined(LEGACY_REFERENCE_TRACKING)
if(reference_find_on_fail[refID])
- D.find_references_legacy()
+ INVOKE_ASYNC(D, /datum/proc/find_references)
+ ref_searching = TRUE
#ifdef GC_FAILURE_HARD_LOOKUP
else
- D.find_references_legacy()
+ INVOKE_ASYNC(D, /datum/proc/find_references)
+ ref_searching = TRUE
#endif
reference_find_on_fail -= refID
#endif
var/type = D.type
var/datum/qdel_item/I = items[type]
- #ifdef TESTING
+
log_world("## TESTING: GC: -- \ref[D] | [type] was unable to be GC'd --")
+ #ifdef TESTING
for(var/c in GLOB.admins) //Using testing() here would fill the logs with ADMIN_VV garbage
var/client/admin = c
if(!check_rights_for(admin, R_ADMIN))
continue
to_chat(admin, "## TESTING: GC: -- [ADMIN_VV(D)] | [type] was unable to be GC'd --")
- testing("GC: -- \ref[src] | [type] was unable to be GC'd --")
- #endif
- #ifdef REFERENCE_TRACKING
- GLOB.deletion_failures += D //It should no longer be bothered by the GC, manual deletion only.
- continue
#endif
I.failures++
+
+ if (I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG)
+ #ifdef REFERENCE_TRACKING
+ if(ref_searching)
+ return //ref searching intentionally cancels all further fires while running so things that hold references don't end up getting deleted, so we want to return here instead of continue
+ #endif
+ continue
if (GC_QUEUE_HARDDELETE)
HardDelete(D)
if (MC_TICK_CHECK)
@@ -208,27 +227,17 @@ SUBSYSTEM_DEF(garbage)
Queue(D, level+1)
+ #ifdef REFERENCE_TRACKING
+ if(ref_searching)
+ return
+ #endif
+
if (MC_TICK_CHECK)
return
if (count)
queue.Cut(1,count+1)
count = 0
-#ifdef LEGACY_REFERENCE_TRACKING
-/datum/controller/subsystem/garbage/proc/add_type_to_findref(type)
- if(!ispath(type))
- return "NOT A VAILD PATH"
- reference_find_on_fail_types |= typecacheof(type)
-
-/datum/controller/subsystem/garbage/proc/remove_type_from_findref(type)
- if(!ispath(type))
- return "NOT A VALID PATH"
- reference_find_on_fail_types -= typesof(type)
-
-/datum/controller/subsystem/garbage/proc/clear_findref_types()
- reference_find_on_fail_types = list()
-#endif
-
/datum/controller/subsystem/garbage/proc/Queue(datum/D, level = GC_QUEUE_CHECK)
if (isnull(D))
return
@@ -238,63 +247,66 @@ SUBSYSTEM_DEF(garbage)
var/gctime = world.time
var/refid = "\ref[D]"
-#ifdef LEGACY_REFERENCE_TRACKING
- if(reference_find_on_fail_types[D.type])
- SSgarbage.reference_find_on_fail[REF(D)] = TRUE
-#endif
-
D.gc_destroyed = gctime
var/list/queue = queues[level]
+
queue[++queue.len] = list(gctime, refid) // not += for byond reasons
//this is mainly to separate things profile wise.
/datum/controller/subsystem/garbage/proc/HardDelete(datum/D)
- var/time = world.timeofday
- var/tick = TICK_USAGE
- var/ticktime = world.time
++delslasttick
++totaldels
var/type = D.type
var/refID = "\ref[D]"
+ var/tick_usage = TICK_USAGE
del(D)
-
- tick = (TICK_USAGE-tick+((world.time-ticktime)/world.tick_lag*100))
+ tick_usage = TICK_USAGE_TO_MS(tick_usage)
var/datum/qdel_item/I = items[type]
-
I.hard_deletes++
- I.hard_delete_time += TICK_DELTA_TO_MS(tick)
+ I.hard_delete_time += tick_usage
+ if (tick_usage > I.hard_delete_max)
+ I.hard_delete_max = tick_usage
+ if (tick_usage > highest_del_ms)
+ highest_del_ms = tick_usage
+ highest_del_type_string = "[type]"
+ var/time = MS2DS(tick_usage)
- if (tick > highest_del_tickusage)
- highest_del_tickusage = tick
- time = world.timeofday - time
- if (!time && TICK_DELTA_TO_MS(tick) > 1)
- time = TICK_DELTA_TO_MS(tick)/100
- if (time > highest_del_time)
- highest_del_time = time
- if (time > 10)
- log_game("Error: [type]([refID]) took longer than 1 second to delete (took [time/10] seconds to delete)")
- message_admins("Error: [type]([refID]) took longer than 1 second to delete (took [time/10] seconds to delete).")
+ if (time > 0.1 SECONDS)
postpone(time)
+ var/threshold = CONFIG_GET(number/hard_deletes_overrun_threshold)
+ if (threshold && (time > threshold SECONDS))
+ if (!(I.qdel_flags & QDEL_ITEM_ADMINS_WARNED))
+ log_game("Error: [type]([refID]) took longer than [threshold] seconds to delete (took [round(time/10, 0.1)] seconds to delete)")
+ message_admins("Error: [type]([refID]) took longer than [threshold] seconds to delete (took [round(time/10, 0.1)] seconds to delete).")
+ I.qdel_flags |= QDEL_ITEM_ADMINS_WARNED
+ I.hard_deletes_over_threshold++
+ var/overrun_limit = CONFIG_GET(number/hard_deletes_overrun_limit)
+ if (overrun_limit && I.hard_deletes_over_threshold >= overrun_limit)
+ I.qdel_flags |= QDEL_ITEM_SUSPENDED_FOR_LAG
/datum/controller/subsystem/garbage/Recover()
+ InitQueues() //We first need to create the queues before recovering data
if (istype(SSgarbage.queues))
for (var/i in 1 to SSgarbage.queues.len)
queues[i] |= SSgarbage.queues[i]
-
+/// Qdel Item: Holds statistics on each type that passes thru qdel
/datum/qdel_item
- var/name = ""
- var/qdels = 0 //Total number of times it's passed thru qdel.
- var/destroy_time = 0 //Total amount of milliseconds spent processing this type's Destroy()
- var/failures = 0 //Times it was queued for soft deletion but failed to soft delete.
- var/hard_deletes = 0 //Different from failures because it also includes QDEL_HINT_HARDDEL deletions
- var/hard_delete_time = 0//Total amount of milliseconds spent hard deleting this type.
- var/no_respect_force = 0//Number of times it's not respected force=TRUE
- var/no_hint = 0 //Number of times it's not even bother to give a qdel hint
- var/slept_destroy = 0 //Number of times it's slept in its destroy
+ var/name = "" //!Holds the type as a string for this type
+ var/qdels = 0 //!Total number of times it's passed thru qdel.
+ var/destroy_time = 0 //!Total amount of milliseconds spent processing this type's Destroy()
+ var/failures = 0 //!Times it was queued for soft deletion but failed to soft delete.
+ var/hard_deletes = 0 //!Different from failures because it also includes QDEL_HINT_HARDDEL deletions
+ var/hard_delete_time = 0 //!Total amount of milliseconds spent hard deleting this type.
+ var/hard_delete_max = 0 //!Highest time spent hard_deleting this in ms.
+ var/hard_deletes_over_threshold = 0 //!Number of times hard deletes took longer than the configured threshold
+ var/no_respect_force = 0 //!Number of times it's not respected force=TRUE
+ var/no_hint = 0 //!Number of times it's not even bother to give a qdel hint
+ var/slept_destroy = 0 //!Number of times it's slept in its destroy
+ var/qdel_flags = 0 //!Flags related to this type's trip thru qdel.
/datum/qdel_item/New(mytype)
name = "[mytype]"
@@ -307,12 +319,12 @@ SUBSYSTEM_DEF(garbage)
if(!istype(D))
del(D)
return
+
var/datum/qdel_item/I = SSgarbage.items[D.type]
if (!I)
I = SSgarbage.items[D.type] = new /datum/qdel_item(D.type)
I.qdels++
-
if(isnull(D.gc_destroyed))
if (SEND_SIGNAL(D, COMSIG_PARENT_PREQDELETED, force)) // Give the components a chance to prevent their parent from being deleted
return
@@ -328,12 +340,12 @@ SUBSYSTEM_DEF(garbage)
if(!D)
return
switch(hint)
- if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
+ if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
SSgarbage.Queue(D)
if (QDEL_HINT_IWILLGC)
D.gc_destroyed = world.time
return
- if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
+ if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
if(!force)
D.gc_destroyed = null //clear the gc variable (important!)
return
@@ -350,17 +362,17 @@ SUBSYSTEM_DEF(garbage)
I.no_respect_force++
SSgarbage.Queue(D)
- if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete
+ if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete
SSgarbage.Queue(D, GC_QUEUE_HARDDELETE)
- if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
+ if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
SSgarbage.HardDelete(D)
- #ifdef LEGACY_REFERENCE_TRACKING
- if (QDEL_HINT_FINDREFERENCE) //qdel will, if LEGACY_REFERENCE_TRACKING is enabled, display all references to this object, then queue the object for deletion.
+ #ifdef REFERENCE_TRACKING
+ if (QDEL_HINT_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled, display all references to this object, then queue the object for deletion.
SSgarbage.Queue(D)
- D.find_references_legacy()
- if (QDEL_HINT_IFFAIL_FINDREFERENCE)
+ D.find_references()
+ if (QDEL_HINT_IFFAIL_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled and the object fails to collect, display all references to this object.
SSgarbage.Queue(D)
- SSgarbage.reference_find_on_fail[REF(D)] = TRUE
+ SSgarbage.reference_find_on_fail["\ref[D]"] = TRUE
#endif
else
#ifdef TESTING
@@ -371,18 +383,3 @@ SUBSYSTEM_DEF(garbage)
SSgarbage.Queue(D)
else if(D.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
CRASH("[D.type] destroy proc was called multiple times, likely due to a qdel loop in the Destroy logic")
-
-#ifdef TESTING
-/proc/writeDatumCount()
- var/list/datums = list()
- for(var/datum/D in world)
- datums[D.type] += 1
- for(var/datum/D)
- datums[D.type] += 1
- datums = sortTim(datums, /proc/cmp_numeric_dsc, associative = TRUE)
- if(fexists("data/DATUMCOUNT.txt"))
- fdel("data/DATUMCOUNT.txt")
- var/outfile = file("data/DATUMCOUNT.txt")
- for(var/path in datums)
- outfile << "[datums[path]]\t\t\t\t\t[path]"
-#endif
diff --git a/code/controllers/subsystem/research.dm b/code/controllers/subsystem/research.dm
index 946980389c..410e21d419 100644
--- a/code/controllers/subsystem/research.dm
+++ b/code/controllers/subsystem/research.dm
@@ -366,10 +366,16 @@ SUBSYSTEM_DEF(research)
techweb_categories[I.category] = list(I.id = TRUE)
/datum/controller/subsystem/research/proc/techweb_node_by_id(id)
- return techweb_nodes[id] || error_node
+ if(techweb_nodes[id])
+ return techweb_nodes[id]
+ stack_trace("Attempted to access node ID [id] which didn't exist")
+ return error_node
/datum/controller/subsystem/research/proc/techweb_design_by_id(id)
- return techweb_designs[id] || error_design
+ if(techweb_designs[id])
+ return techweb_designs[id]
+ stack_trace("Attempted to access design ID [id] which didn't exist")
+ return error_design
/datum/controller/subsystem/research/proc/on_design_deletion(datum/design/D)
for(var/i in techweb_nodes)
diff --git a/code/datums/atmosphere/planetary.dm b/code/datums/atmosphere/planetary.dm
index 248873562f..a8b77f7394 100644
--- a/code/datums/atmosphere/planetary.dm
+++ b/code/datums/atmosphere/planetary.dm
@@ -12,9 +12,7 @@
GAS_CO2=10,
)
restricted_gases = list(
- GAS_PLASMA=0.1,
- GAS_BZ=1.2,
- GAS_METHANE=1.0,
+ GAS_BZ=0.1,
GAS_METHYL_BROMIDE=0.1,
)
restricted_chance = 30
@@ -23,7 +21,7 @@
maximum_pressure = LAVALAND_EQUIPMENT_EFFECT_PRESSURE - 1
minimum_temp = BODYTEMP_COLD_DAMAGE_LIMIT + 1
- maximum_temp = 350
+ maximum_temp = 320
/datum/atmosphere/icemoon
id = ICEMOON_DEFAULT_ATMOS
@@ -38,8 +36,6 @@
GAS_CO2=10,
)
restricted_gases = list(
- GAS_PLASMA=0.1,
- GAS_METHANE=1.0,
GAS_METHYL_BROMIDE=0.1,
)
restricted_chance = 10
diff --git a/code/datums/components/armor_plate.dm b/code/datums/components/armor_plate.dm
index f026c89c60..763aef6a70 100644
--- a/code/datums/components/armor_plate.dm
+++ b/code/datums/components/armor_plate.dm
@@ -71,6 +71,7 @@
R.update_icon()
to_chat(user, "
You strengthen [R], improving its resistance against melee, bullet and laser damage.")
else
+ SEND_SIGNAL(O, COMSIG_ARMOR_PLATED, amount, maxamount)
to_chat(user, "
You strengthen [O], improving its resistance against melee attacks.")
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 42580425ce..164cda63e0 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -57,9 +57,13 @@
*/
var/list/cooldowns
-#ifdef TESTING
+#ifdef REFERENCE_TRACKING
var/running_find_references
var/last_find_references = 0
+ #ifdef REFERENCE_TRACKING_DEBUG
+ ///Stores info about where refs are found, used for sanity checks and testing
+ var/list/found_refs
+ #endif
#endif
#ifdef DATUMVAR_DEBUGGING_MODE
diff --git a/code/datums/helper_datums/stack_end_detector.dm b/code/datums/helper_datums/stack_end_detector.dm
new file mode 100644
index 0000000000..0d621f51b3
--- /dev/null
+++ b/code/datums/helper_datums/stack_end_detector.dm
@@ -0,0 +1,32 @@
+/**
+ Stack End Detector.
+ Can detect if a given code stack has exited, used by the mc for stack overflow detection.
+
+ **/
+/datum/stack_end_detector
+ var/datum/weakref/_WF
+ var/datum/stack_canary/_canary
+
+/datum/stack_end_detector/New()
+ _canary = new()
+ _WF = WEAKREF(_canary)
+
+/** Prime the stack overflow detector.
+ Store the return value of this proc call in a proc level var.
+ Can only be called once.
+**/
+/datum/stack_end_detector/proc/prime_canary()
+ if (!_canary)
+ CRASH("Prime_canary called twice")
+ . = _canary
+ _canary = null
+
+/// Returns true if the stack is still going. Calling before the canary has been primed also returns true
+/datum/stack_end_detector/proc/check()
+ return !!_WF.resolve()
+
+/// Stack canary. Will go away if the stack it was primed by is ended by byond for return or stack overflow reasons.
+/datum/stack_canary
+
+/// empty proc to avoid warnings about unused variables. Call this proc on your canary in the stack it's watching.
+/datum/stack_canary/proc/use_variable()
diff --git a/code/datums/http.dm b/code/datums/http.dm
index 2a9b53f131..49b183fde6 100644
--- a/code/datums/http.dm
+++ b/code/datums/http.dm
@@ -6,10 +6,12 @@
var/body
var/headers
var/url
+ /// If present response body will be saved to this file.
+ var/output_file
var/_raw_response
-/datum/http_request/proc/prepare(method, url, body = "", list/headers)
+/datum/http_request/proc/prepare(method, url, body = "", list/headers, output_file)
if (!length(headers))
headers = ""
else
@@ -19,15 +21,16 @@
src.url = url
src.body = body
src.headers = headers
+ src.output_file = output_file
/datum/http_request/proc/execute_blocking()
- _raw_response = rustg_http_request_blocking(method, url, body, headers)
+ _raw_response = rustg_http_request_blocking(method, url, body, headers, build_options())
/datum/http_request/proc/begin_async()
if (in_progress)
CRASH("Attempted to re-use a request object.")
- id = rustg_http_request_async(method, url, body, headers)
+ id = rustg_http_request_async(method, url, body, headers, build_options())
if (isnull(text2num(id)))
stack_trace("Proc error: [id]")
@@ -35,6 +38,11 @@
else
in_progress = TRUE
+/datum/http_request/proc/build_options()
+ if(output_file)
+ return json_encode(list("output_filename"=output_file,"body_filename"=null))
+ return null
+
/datum/http_request/proc/is_complete()
if (isnull(id))
return TRUE
diff --git a/code/datums/traits/neutral.dm b/code/datums/traits/neutral.dm
index 2f0667d942..36712ea6bb 100644
--- a/code/datums/traits/neutral.dm
+++ b/code/datums/traits/neutral.dm
@@ -162,3 +162,20 @@
gain_text = "
You feel like munching on a can of soda."
lose_text = "
You no longer feel like you should be eating trash."
mob_trait = TRAIT_TRASHCAN
+
+/datum/quirk/colorist
+ name = "Colorist"
+ desc = "You like carrying around a hair dye spray to quickly apply color patterns to your hair."
+ value = 0
+ medical_record_text = "Patient enjoys dyeing their hair with pretty colors."
+
+/datum/quirk/colorist/on_spawn()
+ var/mob/living/carbon/human/H = quirk_holder
+ var/obj/item/dyespray/spraycan = new(get_turf(quirk_holder))
+ H.equip_to_slot(spraycan, SLOT_IN_BACKPACK)
+ H.regenerate_icons()
+
+/datum/quirk/colorist/post_add()
+ var/mob/living/carbon/human/H = quirk_holder
+ SEND_SIGNAL(H.back, COMSIG_TRY_STORAGE_SHOW, H)
+ to_chat(quirk_holder, "
You brought some extra dye with you! It's in your bag if you forgot.")
diff --git a/code/game/gamemodes/clock_cult/clock_cult.dm b/code/game/gamemodes/clock_cult/clock_cult.dm
index d8ebf6f20c..64ca226c2a 100644
--- a/code/game/gamemodes/clock_cult/clock_cult.dm
+++ b/code/game/gamemodes/clock_cult/clock_cult.dm
@@ -151,14 +151,14 @@ Credit where due:
var/datum/team/clockcult/main_clockcult
/datum/game_mode/clockwork_cult/pre_setup() //Gamemode and job code is pain. Have fun codediving all of that stuff, whoever works on this next - Delta
- /*var/list/errorList = list()
+ var/list/errorList = list()
var/list/reebes = SSmapping.LoadGroup(errorList, "Reebe", "map_files/generic", "City_of_Cogs.dmm", default_traits = ZTRAITS_REEBE, silent = TRUE)
if(errorList.len) // reebe failed to load
message_admins("Reebe failed to load!")
log_game("Reebe failed to load!")
return FALSE
for(var/datum/parsed_map/PM in reebes) //Temporarily commented because of z-level loading reliably segfaulting the server.
- PM.initTemplateBounds()*/
+ PM.initTemplateBounds()
if(CONFIG_GET(flag/protect_roles_from_antagonist))
restricted_jobs += protected_jobs
if(CONFIG_GET(flag/protect_assistant_from_antagonist))
@@ -275,7 +275,7 @@ Credit where due:
ears = /obj/item/radio/headset
gloves = /obj/item/clothing/gloves/color/yellow
belt = /obj/item/storage/belt/utility/servant
- backpack_contents = list(/obj/item/storage/box/engineer = 1, \
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/clockwork/replica_fabricator = 1, /obj/item/stack/tile/brass/fifty = 1, /obj/item/reagent_containers/food/drinks/bottle/holyoil = 1)
id = /obj/item/pda
var/plasmaman //We use this to determine if we should activate internals in post_equip()
diff --git a/code/game/gamemodes/clown_ops/clown_ops.dm b/code/game/gamemodes/clown_ops/clown_ops.dm
index 659d2de105..fa73e2cca9 100644
--- a/code/game/gamemodes/clown_ops/clown_ops.dm
+++ b/code/game/gamemodes/clown_ops/clown_ops.dm
@@ -43,7 +43,7 @@
l_pocket = /obj/item/pinpointer/nuke/syndicate
r_pocket = /obj/item/bikehorn
id = /obj/item/card/id/syndicate
- backpack_contents = list(/obj/item/storage/box/syndie=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/syndie=1,\
/obj/item/kitchen/knife/combat/survival,
/obj/item/reagent_containers/spray/waterflower/lube)
implants = list(/obj/item/implant/sad_trombone)
diff --git a/code/game/gamemodes/nuclear/nuclear.dm b/code/game/gamemodes/nuclear/nuclear.dm
index dcf84e84db..a82abe9b3d 100644
--- a/code/game/gamemodes/nuclear/nuclear.dm
+++ b/code/game/gamemodes/nuclear/nuclear.dm
@@ -127,7 +127,7 @@
l_pocket = /obj/item/pinpointer/nuke/syndicate
id = /obj/item/card/id/syndicate
belt = /obj/item/gun/ballistic/automatic/pistol
- backpack_contents = list(/obj/item/storage/box/syndie=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/syndie=1,\
/obj/item/kitchen/knife/combat/survival)
var/tc = 25
@@ -173,7 +173,7 @@
internals_slot = SLOT_R_STORE
belt = /obj/item/storage/belt/military
r_hand = /obj/item/gun/ballistic/automatic/shotgun/bulldog
- backpack_contents = list(/obj/item/storage/box/syndie=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/syndie=1,\
/obj/item/tank/jetpack/oxygen/harness=1,\
/obj/item/gun/ballistic/automatic/pistol=1,\
/obj/item/kitchen/knife/combat/survival)
@@ -188,7 +188,7 @@
r_pocket = /obj/item/tank/internals/emergency_oxygen/engi
internals_slot = SLOT_R_STORE
belt = /obj/item/storage/belt/military
- backpack_contents = list(/obj/item/storage/box/syndie=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/syndie=1,\
/obj/item/tank/jetpack/oxygen/harness=1,\
/obj/item/gun/ballistic/automatic/pistol=1,\
/obj/item/kitchen/knife/combat/survival)
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index af7b69516d..7177249dbc 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -568,6 +568,12 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
if(usr.incapacitated() || !Adjacent(usr) || usr.lying)
return
+ if(iscyborg(usr))
+ var/obj/item/gripper/gripper = usr.get_active_held_item(TRUE)
+ if(istype(gripper))
+ gripper.pre_attack(src, usr, get_dist(src, usr))
+ return
+
if(usr.get_active_held_item() == null) // Let me know if this has any problems -Yota
usr.UnarmedAttack(src)
@@ -879,7 +885,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
/obj/item/MouseEntered(location, control, params)
SEND_SIGNAL(src, COMSIG_ITEM_MOUSE_ENTER, location, control, params)
- if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr.client.prefs.enable_tips && !QDELETED(src) || isobserver(usr))
+ if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr.client.prefs.enable_tips && !QDELETED(src))
var/timedelay = usr.client.prefs.tip_delay/100
var/user = usr
tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, user), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it.
@@ -900,7 +906,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
remove_outline()
/obj/item/proc/apply_outline(colour = null)
- if(!(item_flags & IN_INVENTORY || item_flags & IN_STORAGE) || QDELETED(src))
+ if(!(item_flags & IN_INVENTORY || item_flags & IN_STORAGE) || QDELETED(src) || isobserver(usr))
return
if(usr.client)
if(!usr.client.prefs.outline_enabled)
diff --git a/code/game/objects/items/dyekit.dm b/code/game/objects/items/dyekit.dm
new file mode 100644
index 0000000000..43a05c7939
--- /dev/null
+++ b/code/game/objects/items/dyekit.dm
@@ -0,0 +1,40 @@
+/obj/item/dyespray
+ name = "hair dye spray"
+ desc = "A spray to dye your hair any gradients you'd like."
+ icon = 'icons/obj/dyespray.dmi'
+ icon_state = "dyespray"
+
+/obj/item/dyespray/attack_self(mob/user)
+ dye(user)
+
+/obj/item/dyespray/pre_attack(atom/target, mob/living/user, params)
+ dye(target)
+ return ..()
+
+/**
+ * Applies a gradient and a gradient color to a mob.
+ *
+ * Arguments:
+ * * target - The mob who we will apply the gradient and gradient color to.
+ */
+
+/obj/item/dyespray/proc/dye(mob/target)
+ if(!ishuman(target))
+ return
+ var/mob/living/carbon/human/human_target = target
+
+ var/new_grad_style = input(usr, "Choose a color pattern:", "Character Preference") as null|anything in GLOB.hair_gradients_list
+ if(!new_grad_style)
+ return
+
+ var/new_grad_color = input(usr, "Choose a secondary hair color:", "Character Preference","#"+human_target.grad_color) as color|null
+ if(!new_grad_color)
+ return
+
+ human_target.grad_style = new_grad_style
+ human_target.grad_color = sanitize_hexcolor(new_grad_color)
+ to_chat(human_target, "
You start applying the hair dye...")
+ if(!do_after(usr, 30, target = human_target))
+ return
+ playsound(src, 'sound/effects/spray.ogg', 5, TRUE, 5)
+ human_target.update_hair()
diff --git a/code/game/objects/items/melee/energy.dm b/code/game/objects/items/melee/energy.dm
index 1be570b62b..275536d370 100644
--- a/code/game/objects/items/melee/energy.dm
+++ b/code/game/objects/items/melee/energy.dm
@@ -184,7 +184,7 @@
hitcost = 75 //Costs more than a standard cyborg esword
w_class = WEIGHT_CLASS_NORMAL
sharpness = SHARP_EDGED
- light_color = "#40ceff"
+ light_color = LIGHT_COLOR_RED
tool_behaviour = TOOL_SAW
toolspeed = 0.7
diff --git a/code/game/objects/items/robot/robot_items.dm b/code/game/objects/items/robot/robot_items.dm
index 6513b53e1d..cbfdc85f6d 100644
--- a/code/game/objects/items/robot/robot_items.dm
+++ b/code/game/objects/items/robot/robot_items.dm
@@ -745,9 +745,9 @@
Grippers oh god oh fuck
***********************************************************************/
-/obj/item/weapon/gripper
+/obj/item/gripper
name = "engineering gripper"
- desc = "A simple grasping tool for interacting with various engineering related items, such as circuits, gas tanks, conveyer belts and more. Alt click to drop instead of use."
+ desc = "A simple grasping tool for interacting with various engineering related items, such as circuits, gas tanks, conveyer belts and more."
icon = 'icons/obj/device.dmi'
icon_state = "gripper"
@@ -776,18 +776,29 @@
var/obj/item/wrapped = null // Item currently being held.
-//Used to interact with UI's of held items, such as gas tanks and airlock electronics.
-/obj/item/weapon/gripper/AltClick(mob/user)
+// Used to drop whatever's in the gripper.
+/obj/item/gripper/proc/drop_held(silent = FALSE)
if(wrapped)
wrapped.forceMove(get_turf(wrapped))
- to_chat(user, "
You drop the [wrapped].")
+ if(!silent)
+ to_chat(usr, "
You drop the [wrapped].")
+ modify_appearance(wrapped, FALSE)
wrapped = null
- return ..()
+ update_appearance()
+ return TRUE
+ return FALSE
-/obj/item/weapon/gripper/pre_attack(var/atom/target, var/mob/living/silicon/robot/user, proximity, params)
+/obj/item/gripper/proc/takeitem(obj/item/item, silent = FALSE)
+ if(!silent)
+ to_chat(usr, "
You collect \the [item].")
+ item.loc = src
+ wrapped = item
+ update_appearance()
- if(!proximity)
- return
+/obj/item/gripper/pre_attack(atom/target, mob/living/silicon/robot/user, params)
+ var/proximity = get_dist(user, target)
+ if(proximity > 1)
+ return STOP_ATTACK_PROC_CHAIN
if(!wrapped)
for(var/obj/item/thing in src.contents)
@@ -795,21 +806,24 @@
break
if(wrapped) //Already have an item.
+ var/obj/item/item = wrapped
+ drop_held(TRUE)
//Temporary put wrapped into user so target's attackby() checks pass.
- wrapped.loc = user
+ item.loc = user
//Pass the attack on to the target. This might delete/relocate wrapped.
- var/resolved = target.attackby(wrapped,user)
- if(!resolved && wrapped && target)
- wrapped.afterattack(target,user,1)
+ var/resolved = target.attackby(item, user, params)
+ if(!resolved && item && target)
+ item.afterattack(target, user, proximity, params)
//If wrapped was neither deleted nor put into target, put it back into the gripper.
- if(wrapped && user && (wrapped.loc == user))
- wrapped.loc = src
- else
- wrapped = null
+ if(item && user && (item.loc == user))
+ takeitem(item, TRUE)
return
+ else
+ item = null
+ return STOP_ATTACK_PROC_CHAIN
- else if(istype(target,/obj/item))
+ else if(isitem(target))
var/obj/item/I = target
var/grab = 0
@@ -824,24 +838,94 @@
//We can grab the item, finally.
if(grab)
- to_chat(user, "
You collect \the [I].")
- I.loc = src
- wrapped = I
+ takeitem(I)
return
else
to_chat(user, "
Your gripper cannot hold \the [target].")
-/obj/item/weapon/gripper/mining
+// Rare cases - meant to be handled by code\modules\mob\living\silicon\robot\robot.dm:584 and the weirdness of get_active_held_item() of borgs.
+/obj/item/gripper/attack_self(mob/user)
+ if(wrapped)
+ wrapped.attack_self(user)
+ return
+ . = ..()
+
+// Splitable items
+/obj/item/gripper/AltClick(mob/user)
+ if(wrapped)
+ wrapped.AltClick(user)
+ return
+ . = ..()
+
+// Even rarer cases
+/obj/item/gripper/CtrlClick(mob/user)
+ if(wrapped)
+ wrapped.CtrlClick(user)
+ return
+ . = ..()
+
+// At this point you're just kidding me, but have this one as well.
+/obj/item/gripper/CtrlShiftClick(mob/user)
+ if(wrapped)
+ wrapped.CtrlShiftClick(user)
+ return
+ . = ..()
+
+// Make it clear what we can do with it.
+/obj/item/gripper/examine(mob/user)
+ . = ..()
+ if(wrapped)
+ . += "
It is holding [icon2html(wrapped, user)] [wrapped]."
+ . += "
Examine the little preview to examine it."
+ . += "
Attempting to drop the gripper will only drop [wrapped]."
+
+// Resets vis_contents and if holding something, add it to vis_contents.
+/obj/item/gripper/update_appearance(updates)
+ . = ..()
+ vis_contents = list()
+ if(wrapped)
+ modify_appearance(wrapped, TRUE)
+ vis_contents += wrapped
+
+// Generates the "minified" version of the item being held and adjust it's position.
+/obj/item/gripper/proc/modify_appearance(obj/item, minify = FALSE)
+ if(minify)
+ var/matrix/new_transform = new
+ new_transform.Scale(0.5, 0.5)
+ item.transform = new_transform
+ item.pixel_x = 8
+ item.pixel_y = -8
+ else
+ item.pixel_x = initial(pixel_x)
+ item.pixel_y = initial(pixel_y)
+ item.transform = new
+
+// I kind of wanted the item to be held in the gripper when stored as well, but i realized "store" is just drop as well, so i'll do this for now.
+// This will handle cases where the borg runs out of power or is damaged enough so the module is forcefully stored.
+/obj/item/gripper/cyborg_unequip(mob/user)
+ . = ..()
+ if(wrapped)
+ drop_held()
+
+// Clear references on being destroyed
+/obj/item/Destroy()
+ for(var/obj/item/gripper/gripper in vis_locs)
+ if(gripper.wrapped == src)
+ gripper.wrapped = null
+ gripper.update_appearance()
+ . = ..()
+
+/obj/item/gripper/mining
name = "shelter capsule deployer"
- desc = "A simple grasping tool for carrying and deploying shelter capsules. Alt click to drop instead of use."
+ desc = "A simple grasping tool for carrying and deploying shelter capsules."
icon_state = "gripper_mining"
can_hold = list(
/obj/item/survivalcapsule
)
-/obj/item/weapon/gripper/medical
+/obj/item/gripper/medical
name = "medical gripper"
- desc = "A simple grasping tool for interacting with medical equipment, such as beakers, blood bags, chem bags and more. Alt click to drop instead of use."
+ desc = "A simple grasping tool for interacting with medical equipment, such as beakers, blood bags, chem bags and more."
icon_state = "gripper_medical"
can_hold = list(
/obj/item/storage/bag/bio,
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 15153521ed..504bff9e29 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -502,6 +502,8 @@ GLOBAL_LIST_INIT(cardboard_recipes, list ( \
new /datum/stack_recipe("sterile masks box", /obj/item/storage/box/masks), \
new /datum/stack_recipe("body bag box", /obj/item/storage/box/bodybags), \
new /datum/stack_recipe("prescription glasses box", /obj/item/storage/box/rxglasses), \
+ new /datum/stack_recipe("oxygen tank box", /obj/item/storage/box/emergencytank), \
+ new /datum/stack_recipe("extended oxygen tank box", /obj/item/storage/box/engitank), \
null, \
new /datum/stack_recipe("disk box", /obj/item/storage/box/disks), \
new /datum/stack_recipe("light tubes box", /obj/item/storage/box/lights/tubes), \
diff --git a/code/game/objects/items/storage/boxes.dm b/code/game/objects/items/storage/boxes.dm
index ea40b49c5f..8b897cb6e5 100644
--- a/code/game/objects/items/storage/boxes.dm
+++ b/code/game/objects/items/storage/boxes.dm
@@ -102,12 +102,22 @@
new /obj/item/disk/nanite_program(src)
// Ordinary survival box
+/obj/item/storage/box/survival
+ name = "survival box"
+ desc = "A box with the bare essentials of ensuring the survival of you and others."
+ icon_state = "internals"
+ illustration = "emergencytank"
+ var/mask_type = /obj/item/clothing/mask/breath
+ var/internal_type = /obj/item/tank/internals/emergency_oxygen
+ var/medipen_type = /obj/item/reagent_containers/hypospray/medipen
+
/obj/item/storage/box/survival/PopulateContents()
- new /obj/item/clothing/mask/breath(src)
- new /obj/item/reagent_containers/hypospray/medipen(src)
+ new mask_type(src)
+ if(!isnull(medipen_type))
+ new medipen_type(src)
if(!isplasmaman(loc))
- new /obj/item/tank/internals/emergency_oxygen(src)
+ new internal_type(src)
else
new /obj/item/tank/internals/plasmaman/belt(src)
@@ -115,10 +125,13 @@
..() // we want the survival stuff too.
new /obj/item/radio/off(src)
-/obj/item/storage/box/survival_mining/PopulateContents()
- new /obj/item/clothing/mask/gas/explorer(src)
+// Mining survival box
+/obj/item/storage/box/survival/mining
+ mask_type = /obj/item/clothing/mask/gas/explorer
+
+/obj/item/storage/box/survival/mining/PopulateContents()
+ ..()
new /obj/item/crowbar/red(src)
- new /obj/item/reagent_containers/hypospray/medipen(src)
if(!isplasmaman(loc))
new /obj/item/tank/internals/emergency_oxygen(src)
@@ -126,39 +139,30 @@
new /obj/item/tank/internals/plasmaman/belt(src)
// Engineer survival box
-/obj/item/storage/box/engineer/PopulateContents()
- new /obj/item/clothing/mask/breath(src)
- new /obj/item/reagent_containers/hypospray/medipen(src)
+/obj/item/storage/box/survival/engineer
+ name = "extended-capacity survival box"
+ desc = "A box with the bare essentials of ensuring the survival of you and others. This one is labelled to contain an extended-capacity tank."
+ illustration = "extendedtank"
+ internal_type = /obj/item/tank/internals/emergency_oxygen/engi
- if(!isplasmaman(loc))
- new /obj/item/tank/internals/emergency_oxygen/engi(src)
- else
- new /obj/item/tank/internals/plasmaman/belt(src)
-
-/obj/item/storage/box/engineer/radio/PopulateContents()
+/obj/item/storage/box/survival/engineer/radio/PopulateContents()
..() // we want the regular items too.
new /obj/item/radio/off(src)
// Syndie survival box
-/obj/item/storage/box/syndie/PopulateContents()
- new /obj/item/clothing/mask/gas/syndicate(src)
-
- if(!isplasmaman(loc))
- new /obj/item/tank/internals/emergency_oxygen/engi(src)
- else
- new /obj/item/tank/internals/plasmaman/belt(src)
+/obj/item/storage/box/survival/syndie //why is this its own thing if it's just the engi box with a syndie mask and medipen?
+ name = "extended-capacity survival box"
+ desc = "A box with the bare essentials of ensuring the survival of you and others. This one is labelled to contain an extended-capacity tank."
+ illustration = "extendedtank"
+ mask_type = /obj/item/clothing/mask/gas/syndicate
+ internal_type = /obj/item/tank/internals/emergency_oxygen/engi
+ medipen_type = null
// Security survival box
-/obj/item/storage/box/security/PopulateContents()
- new /obj/item/clothing/mask/gas/sechailer(src)
- new /obj/item/reagent_containers/hypospray/medipen(src)
+/obj/item/storage/box/survival/security
+ mask_type = /obj/item/clothing/mask/gas/sechailer
- if(!isplasmaman(loc))
- new /obj/item/tank/internals/emergency_oxygen(src)
- else
- new /obj/item/tank/internals/plasmaman/belt(src)
-
-/obj/item/storage/box/security/radio/PopulateContents()
+/obj/item/storage/box/survival/security/radio/PopulateContents()
..() // we want the regular stuff too
new /obj/item/radio/off(src)
@@ -1244,6 +1248,26 @@
icon_state = "box_pink"
illustration = null
+/obj/item/storage/box/emergencytank
+ name = "emergency oxygen tank box"
+ desc = "A box of emergency oxygen tanks."
+ illustration = "emergencytank"
+
+/obj/item/storage/box/emergencytank/PopulateContents()
+ ..()
+ for(var/i in 1 to 7)
+ new /obj/item/tank/internals/emergency_oxygen(src) //in case anyone ever wants to do anything with spawning them, apart from crafting the box
+
+/obj/item/storage/box/engitank
+ name = "extended-capacity emergency oxygen tank box"
+ desc = "A box of extended-capacity emergency oxygen tanks."
+ illustration = "extendedtank"
+
+/obj/item/storage/box/engitank/PopulateContents()
+ ..()
+ for(var/i in 1 to 7)
+ new /obj/item/tank/internals/emergency_oxygen/engi(src) //in case anyone ever wants to do anything with spawning them, apart from crafting the box
+
/obj/item/storage/box/mre //base MRE type.
name = "Nanotrasen MRE Ration Kit Menu 0"
desc = "A package containing food suspended in an outdated bluespace pocket which lasts for centuries. If you're lucky you may even be able to enjoy the meal without getting food poisoning."
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/security.dm b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
index 1d23903d03..cd10f9209c 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/security.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
@@ -7,7 +7,7 @@
new /obj/item/clothing/neck/petcollar(src) //I considered removing the pet stuff too but eh, who knows. We might get Renault back. Plus I guess you could use that collar for... other means. Aren't you supposed to be guarding the disk?
new /obj/item/pet_carrier(src)
new /obj/item/clothing/suit/armor/vest/capcarapace(src)
- new /obj/item/clothing/suit/armor/vest/capcarapace/alt(src)
+ new /obj/item/clothing/suit/toggle/captains_parade(src)
new /obj/item/clothing/head/crown/fancy(src)
new /obj/item/cartridge/captain(src)
new /obj/item/storage/box/silver_ids(src)
@@ -56,6 +56,7 @@
/obj/structure/closet/secure_closet/hos/PopulateContents()
..()
new /obj/item/clothing/neck/cloak/hos(src)
+ new /obj/item/clothing/suit/toggle/armor/hos/hos_formal(src)
new /obj/item/cartridge/hos(src)
new /obj/item/radio/headset/heads/hos(src)
new /obj/item/clothing/under/rank/security/head_of_security/parade/female(src)
diff --git a/code/game/world.dm b/code/game/world.dm
index f466913346..14a85bb619 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -16,7 +16,7 @@ GLOBAL_LIST(topic_status_cache)
call(debug_server, "auxtools_init")()
enable_debugging()
AUXTOOLS_CHECK(AUXMOS)
-#ifdef REFERENCE_TRACKING
+#ifdef EXTOOLS_REFERENCE_TRACKING
enable_reference_tracking()
#endif
world.Profile(PROFILE_START)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 22bbf03014..437c74991c 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -195,7 +195,7 @@ GLOBAL_PROTECT(admin_verbs_debug)
// #endif
/datum/admins/proc/create_or_modify_area,
/datum/admins/proc/fixcorruption,
-#ifdef REFERENCE_TRACKING
+#ifdef EXTOOLS_REFERENCE_TRACKING
/datum/admins/proc/view_refs,
/datum/admins/proc/view_del_failures,
#endif
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 953835bff0..4224b406b3 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -2862,7 +2862,7 @@
return
if(!CONFIG_GET(string/centcom_ban_db))
- to_chat(usr, "
Centcom Galactic Ban DB is disabled!")
+ to_chat(usr, span_warning("Centcom Galactic Ban DB is disabled!"))
return
var/ckey = href_list["centcomlookup"]
@@ -2889,8 +2889,19 @@
dat += "
0 bans detected for [ckey]"
else
bans = json_decode(response["body"])
- dat += "
[bans.len] ban\s detected for [ckey]"
+
+ //Ignore bans from non-whitelisted sources, if a whitelist exists
+ var/list/valid_sources
+ if(CONFIG_GET(string/centcom_source_whitelist))
+ valid_sources = splittext(CONFIG_GET(string/centcom_source_whitelist), ",")
+ dat += "
Bans detected for [ckey]"
+ else
+ //Ban count is potentially inaccurate if they're using a whitelist
+ dat += "
[bans.len] ban\s detected for [ckey]"
+
for(var/list/ban in bans)
+ if(valid_sources && !(ban["sourceName"] in valid_sources))
+ continue
dat += "
Server: [sanitize(ban["sourceName"])]
"
dat += "
RP Level: [sanitize(ban["sourceRoleplayLevel"])]
"
dat += "
Type: [sanitize(ban["type"])]
"
diff --git a/code/modules/admin/view_variables/reference_tracking.dm b/code/modules/admin/view_variables/reference_tracking.dm
index 279654eec1..714b54cd45 100644
--- a/code/modules/admin/view_variables/reference_tracking.dm
+++ b/code/modules/admin/view_variables/reference_tracking.dm
@@ -1,4 +1,4 @@
-#ifdef REFERENCE_TRACKING
+#ifdef EXTOOLS_REFERENCE_TRACKING
GLOBAL_LIST_EMPTY(deletion_failures)
@@ -102,29 +102,21 @@ GLOBAL_LIST_EMPTY(deletion_failures)
#endif
-#ifdef LEGACY_REFERENCE_TRACKING
+#ifdef REFERENCE_TRACKING
-/datum/verb/legacy_find_refs()
- set category = "Debug"
- set name = "Find References"
- set src in world
-
- find_references_legacy(FALSE)
-
-
-/datum/proc/find_references_legacy(skip_alert)
+/datum/proc/find_references(skip_alert)
running_find_references = type
if(usr?.client)
if(usr.client.running_find_references)
- testing("CANCELLED search for references to a [usr.client.running_find_references].")
+ log_reftracker("CANCELLED search for references to a [usr.client.running_find_references].")
usr.client.running_find_references = null
running_find_references = null
//restart the garbage collector
SSgarbage.can_fire = TRUE
- SSgarbage.next_fire = world.time + world.tick_lag
+ SSgarbage.update_nextfire(reset_time = TRUE)
return
- if(!skip_alert && alert("Running this will lock everything up for about 5 minutes. Would you like to begin the search?", "Find References", "Yes", "No") != "Yes")
+ if(!skip_alert && tgui_alert(usr,"Running this will lock everything up for about 5 minutes. Would you like to begin the search?", "Find References", list("Yes", "No")) != "Yes")
running_find_references = null
return
@@ -134,92 +126,122 @@ GLOBAL_LIST_EMPTY(deletion_failures)
if(usr?.client)
usr.client.running_find_references = type
- testing("Beginning search for references to a [type].")
- last_find_references = world.time
+ log_reftracker("Beginning search for references to a [type].")
+
+ var/starting_time = world.time
+
+ //Time to search the whole game for our ref
+ DoSearchVar(GLOB, "GLOB") //globals
+ log_reftracker("Finished searching globals")
- DoSearchVar(GLOB) //globals
for(var/datum/thing in world) //atoms (don't beleive its lies)
- DoSearchVar(thing, "World -> [thing]")
+ DoSearchVar(thing, "World -> [thing.type]", search_time = starting_time)
+ log_reftracker("Finished searching atoms")
for(var/datum/thing) //datums
- DoSearchVar(thing, "World -> [thing]")
+ DoSearchVar(thing, "Datums -> [thing.type]", search_time = starting_time)
+ log_reftracker("Finished searching datums")
+ //Warning, attempting to search clients like this will cause crashes if done on live. Watch yourself
for(var/client/thing) //clients
- DoSearchVar(thing, "World -> [thing]")
+ DoSearchVar(thing, "Clients -> [thing.type]", search_time = starting_time)
+ log_reftracker("Finished searching clients")
+
+ log_reftracker("Completed search for references to a [type].")
- testing("Completed search for references to a [type].")
if(usr?.client)
usr.client.running_find_references = null
running_find_references = null
//restart the garbage collector
SSgarbage.can_fire = TRUE
- SSgarbage.next_fire = world.time + world.tick_lag
+ SSgarbage.update_nextfire(reset_time = TRUE)
+/datum/proc/DoSearchVar(potential_container, container_name, recursive_limit = 64, search_time = world.time)
+ #ifdef REFERENCE_TRACKING_DEBUG
+ if(!found_refs && SSgarbage.should_save_refs)
+ found_refs = list()
+ #endif
-/datum/verb/qdel_then_find_references()
- set category = "Debug"
- set name = "qdel() then Find References"
- set src in world
-
- qdel(src, TRUE) //force a qdel
- if(!running_find_references)
- find_references_legacy(TRUE)
-
-
-/datum/verb/qdel_then_if_fail_find_references()
- set category = "Debug"
- set name = "qdel() then Find References if GC failure"
- set src in world
-
- qdel_and_find_ref_if_fail(src, TRUE)
-
-
-/datum/proc/DoSearchVar(potential_container, container_name, recursive_limit = 64)
if(usr?.client && !usr.client.running_find_references)
return
if(!recursive_limit)
+ log_reftracker("Recursion limit reached. [container_name]")
return
- if(istype(potential_container, /datum))
- var/datum/datum_container = potential_container
- if(datum_container.last_find_references == last_find_references)
- return
-
- datum_container.last_find_references = last_find_references
- var/list/vars_list = datum_container.vars
-
- for(var/varname in vars_list)
- if (varname == "vars")
- continue
- var/variable = vars_list[varname]
-
- if(variable == src)
- testing("Found [type] \ref[src] in [datum_container.type]'s [varname] var. [container_name]")
-
- else if(islist(variable))
- DoSearchVar(variable, "[container_name] -> list", recursive_limit - 1)
-
- else if(islist(potential_container))
- var/normal = IS_NORMAL_LIST(potential_container)
- for(var/element_in_list in potential_container)
- if(element_in_list == src)
- testing("Found [type] \ref[src] in list [container_name].")
-
- else if(element_in_list && !isnum(element_in_list) && normal && potential_container[element_in_list] == src)
- testing("Found [type] \ref[src] in list [container_name]\[[element_in_list]\]")
-
- else if(islist(element_in_list))
- DoSearchVar(element_in_list, "[container_name] -> list", recursive_limit - 1)
-
+ //Check each time you go down a layer. This makes it a bit slow, but it won't effect the rest of the game at all
#ifndef FIND_REF_NO_CHECK_TICK
CHECK_TICK
#endif
+ if(istype(potential_container, /datum))
+ var/datum/datum_container = potential_container
+ if(datum_container.last_find_references == search_time)
+ return
+
+ datum_container.last_find_references = search_time
+ var/list/vars_list = datum_container.vars
+
+ for(var/varname in vars_list)
+ #ifndef FIND_REF_NO_CHECK_TICK
+ CHECK_TICK
+ #endif
+ if (varname == "vars" || varname == "vis_locs") //Fun fact, vis_locs don't count for references
+ continue
+ var/variable = vars_list[varname]
+
+ if(variable == src)
+ #ifdef REFERENCE_TRACKING_DEBUG
+ if(SSgarbage.should_save_refs)
+ found_refs[varname] = TRUE
+ #endif
+ log_reftracker("Found [type] \ref[src] in [datum_container.type]'s \ref[datum_container] [varname] var. [container_name]")
+ continue
+
+ if(islist(variable))
+ DoSearchVar(variable, "[container_name] \ref[datum_container] -> [varname] (list)", recursive_limit - 1, search_time)
+
+ else if(islist(potential_container))
+ var/normal = IS_NORMAL_LIST(potential_container)
+ var/list/potential_cache = potential_container
+ for(var/element_in_list in potential_cache)
+ #ifndef FIND_REF_NO_CHECK_TICK
+ CHECK_TICK
+ #endif
+ //Check normal entrys
+ if(element_in_list == src)
+ #ifdef REFERENCE_TRACKING_DEBUG
+ if(SSgarbage.should_save_refs)
+ found_refs[potential_cache] = TRUE
+ #endif
+ log_reftracker("Found [type] \ref[src] in list [container_name].")
+ continue
+
+ var/assoc_val = null
+ if(!isnum(element_in_list) && normal)
+ assoc_val = potential_cache[element_in_list]
+ //Check assoc entrys
+ if(assoc_val == src)
+ #ifdef REFERENCE_TRACKING_DEBUG
+ if(SSgarbage.should_save_refs)
+ found_refs[potential_cache] = TRUE
+ #endif
+ log_reftracker("Found [type] \ref[src] in list [container_name]\[[element_in_list]\]")
+ continue
+ //We need to run both of these checks, since our object could be hiding in either of them
+ //Check normal sublists
+ if(islist(element_in_list))
+ DoSearchVar(element_in_list, "[container_name] -> [element_in_list] (list)", recursive_limit - 1, search_time)
+ //Check assoc sublists
+ if(islist(assoc_val))
+ DoSearchVar(potential_container[element_in_list], "[container_name]\[[element_in_list]\] -> [assoc_val] (list)", recursive_limit - 1, search_time)
/proc/qdel_and_find_ref_if_fail(datum/thing_to_del, force = FALSE)
- SSgarbage.reference_find_on_fail[REF(thing_to_del)] = TRUE
- qdel(thing_to_del, force)
+ thing_to_del.qdel_and_find_ref_if_fail(force)
+
+/datum/proc/qdel_and_find_ref_if_fail(force = FALSE)
+ SSgarbage.reference_find_on_fail["\ref[src]"] = TRUE
+ qdel(src, force)
#endif
diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm
index 01b5536948..043c50173d 100644
--- a/code/modules/admin/view_variables/topic_basic.dm
+++ b/code/modules/admin/view_variables/topic_basic.dm
@@ -47,7 +47,7 @@
usr.client.debug_variables(src)
return
- #ifdef REFERENCE_TRACKING
+ #ifdef EXTOOLS_REFERENCE_TRACKING
if(href_list[VV_HK_VIEW_REFERENCES])
var/datum/D = locate(href_list[VV_HK_TARGET])
if(!D)
diff --git a/code/modules/antagonists/clockcult/clock_structures/ark_of_the_clockwork_justicar.dm b/code/modules/antagonists/clockcult/clock_structures/ark_of_the_clockwork_justicar.dm
index 973b615f4a..6204871a77 100644
--- a/code/modules/antagonists/clockcult/clock_structures/ark_of_the_clockwork_justicar.dm
+++ b/code/modules/antagonists/clockcult/clock_structures/ark_of_the_clockwork_justicar.dm
@@ -253,7 +253,7 @@
purpose_fulfilled = TRUE
make_glow()
animate(glow, transform = matrix() * 1.5, alpha = 255, time = 125)
- sound_to_playing_players(volume = 100, channel = CHANNEL_JUSTICAR_ARK, S = sound('sound/effects/ratvar_rises.ogg')) //End the sounds
+ sound_to_playing_players('sound/effects/ratvar_rises.ogg', 90, FALSE, channel = CHANNEL_JUSTICAR_ARK)
sleep(125)
make_glow()
animate(glow, transform = matrix() * 3, alpha = 0, time = 5)
@@ -318,19 +318,19 @@
if(-INFINITY to GATEWAY_REEBE_FOUND)
if(!second_sound_played)
sound_to_playing_players('sound/magic/clockwork/invoke_general.ogg', 30, FALSE)
- sound_to_playing_players(volume = 10, channel = CHANNEL_JUSTICAR_ARK, S = sound('sound/effects/clockcult_gateway_charging.ogg', TRUE))
+ sound_to_playing_players('sound/effects/clockcult_gateway_charging.ogg', 10, FALSE, channel = CHANNEL_JUSTICAR_ARK)
second_sound_played = TRUE
make_glow()
glow.icon_state = "clockwork_gateway_charging"
if(GATEWAY_REEBE_FOUND to GATEWAY_RATVAR_COMING)
if(!third_sound_played)
- sound_to_playing_players(volume = 30, channel = CHANNEL_JUSTICAR_ARK, S = sound('sound/effects/clockcult_gateway_active.ogg', TRUE))
+ sound_to_playing_players('sound/effects/clockcult_gateway_active.ogg', 30, FALSE, channel = CHANNEL_JUSTICAR_ARK)
third_sound_played = TRUE
make_glow()
glow.icon_state = "clockwork_gateway_active"
if(GATEWAY_RATVAR_COMING to GATEWAY_RATVAR_ARRIVAL)
if(!fourth_sound_played)
- sound_to_playing_players(volume = 70, channel = CHANNEL_JUSTICAR_ARK, S = sound('sound/effects/clockcult_gateway_closing.ogg', TRUE))
+ sound_to_playing_players('sound/effects/clockcult_gateway_closing.ogg', 70, FALSE, channel = CHANNEL_JUSTICAR_ARK)
fourth_sound_played = TRUE
make_glow()
glow.icon_state = "clockwork_gateway_closing"
diff --git a/code/modules/antagonists/eldritch_cult/eldritch_items.dm b/code/modules/antagonists/eldritch_cult/eldritch_items.dm
index 87a4ddf7eb..28228aedd6 100644
--- a/code/modules/antagonists/eldritch_cult/eldritch_items.dm
+++ b/code/modules/antagonists/eldritch_cult/eldritch_items.dm
@@ -260,7 +260,7 @@
if((IS_HERETIC(local_user) || IS_HERETIC_MONSTER(local_user)) && HAS_TRAIT(src,TRAIT_NODROP))
REMOVE_TRAIT(src, TRAIT_NODROP, CLOTHING_TRAIT)
- for(var/mob/living/carbon/human/human_in_range in spiral_range(9,local_user))
+ for(var/mob/living/carbon/human/human_in_range in viewers(9,local_user))
if(IS_HERETIC(human_in_range) || IS_HERETIC_MONSTER(human_in_range))
continue
diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm
index 6a49addc13..0af024985e 100644
--- a/code/modules/asset_cache/asset_list_items.dm
+++ b/code/modules/asset_cache/asset_list_items.dm
@@ -3,21 +3,21 @@
/datum/asset/simple/tgui_common
keep_local_name = TRUE
assets = list(
- "tgui-common.bundle.js" = 'tgui/public/tgui-common.bundle.js',
+ "tgui-common.bundle.js" = file("tgui/public/tgui-common.bundle.js"),
)
/datum/asset/simple/tgui
keep_local_name = TRUE
assets = list(
- "tgui.bundle.js" = 'tgui/public/tgui.bundle.js',
- "tgui.bundle.css" = 'tgui/public/tgui.bundle.css',
+ "tgui.bundle.js" = file("tgui/public/tgui.bundle.js"),
+ "tgui.bundle.css" = file("tgui/public/tgui.bundle.css"),
)
/datum/asset/simple/tgui_panel
keep_local_name = TRUE
assets = list(
- "tgui-panel.bundle.js" = 'tgui/public/tgui-panel.bundle.js',
- "tgui-panel.bundle.css" = 'tgui/public/tgui-panel.bundle.css',
+ "tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"),
+ "tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"),
)
/datum/asset/simple/headers
@@ -168,10 +168,12 @@
/datum/asset/simple/namespaced/tgfont
assets = list(
- "tgfont.eot" = 'tgui/packages/tgfont/dist/tgfont.eot',
- "tgfont.woff2" = 'tgui/packages/tgfont/dist/tgfont.woff2',
+ "tgfont.eot" = file("tgui/packages/tgfont/dist/tgfont.eot"),
+ "tgfont.woff2" = file("tgui/packages/tgfont/dist/tgfont.woff2"),
+ )
+ parents = list(
+ "tgfont.css" = file("tgui/packages/tgfont/dist/tgfont.css"),
)
- parents = list("tgfont.css" = 'tgui/packages/tgfont/dist/tgfont.css')
/datum/asset/spritesheet/chat
name = "chat"
diff --git a/code/modules/atmospherics/machinery/pipes/pipes.dm b/code/modules/atmospherics/machinery/pipes/pipes.dm
index e05502dbad..e286cbbe7f 100644
--- a/code/modules/atmospherics/machinery/pipes/pipes.dm
+++ b/code/modules/atmospherics/machinery/pipes/pipes.dm
@@ -75,6 +75,9 @@
/obj/machinery/atmospherics/pipe/setPipenet(datum/pipeline/P)
parent = P
+/obj/machinery/atmospherics/pipe/zap_act(power, zap_flags)
+ return 0 // they're not really machines in the normal sense, probably shouldn't explode
+
/obj/machinery/atmospherics/pipe/Destroy()
QDEL_NULL(parent)
diff --git a/code/modules/cargo/packs/goodies.dm b/code/modules/cargo/packs/goodies.dm
index 5d4598fd58..5151845221 100644
--- a/code/modules/cargo/packs/goodies.dm
+++ b/code/modules/cargo/packs/goodies.dm
@@ -70,6 +70,12 @@
cost = 1500
contains = list(/obj/item/toy/plush/beeplushie)
+/datum/supply_pack/goody/dyespray
+ name = "Hair Dye Spray"
+ desc = "A cool spray to dye your hair with awesome colors!"
+ cost = PAYCHECK_EASY * 2
+ contains = list(/obj/item/dyespray)
+
/datum/supply_pack/goody/beach_ball
name = "Beach Ball"
desc = "The simple beach ball is one of Nanotrasen's most popular products. 'Why do we make beach balls? Because we can! (TM)' - Nanotrasen"
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 8b214e85a4..cb262e57f3 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -119,6 +119,8 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/hair_color = "000000" //Hair color
var/facial_hair_style = "Shaved" //Face hair type
var/facial_hair_color = "000000" //Facial hair color
+ var/grad_style //Hair gradient style
+ var/grad_color = "FFFFFF" //Hair gradient color
var/skin_tone = "caucasian1" //Skin color
var/use_custom_skin_tone = FALSE
var/left_eye_color = "000000" //Eye color
@@ -505,6 +507,12 @@ GLOBAL_LIST_EMPTY(preferences_datums)
dat += "
< >"
dat += "
Change"
+ dat += "
Hair Gradient
"
+
+ dat += "
[grad_style]"
+ dat += "
< >"
+ dat += "
Change"
+
dat += ""
//Mutant stuff
var/mutant_category = 0
@@ -1711,6 +1719,23 @@ GLOBAL_LIST_EMPTY(preferences_datums)
if("previous_facehair_style")
facial_hair_style = previous_list_item(facial_hair_style, GLOB.facial_hair_styles_list)
+ if("grad_color")
+ var/new_grad_color = input(user, "Choose your character's gradient colour:", "Character Preference","#"+grad_color) as color|null
+ if(new_grad_color)
+ grad_color = sanitize_hexcolor(new_grad_color, 6)
+
+ if("grad_style")
+ var/new_grad_style
+ new_grad_style = input(user, "Choose your character's hair gradient style:", "Character Preference") as null|anything in GLOB.hair_gradients_list
+ if(new_grad_style)
+ grad_style = new_grad_style
+
+ if("next_grad_style")
+ grad_style = next_list_item(grad_style, GLOB.hair_gradients_list)
+
+ if("previous_grad_style")
+ grad_style = previous_list_item(grad_style, GLOB.hair_gradients_list)
+
if("cycle_bg")
bgstate = next_list_item(bgstate, bgstate_options)
@@ -3016,6 +3041,8 @@ GLOBAL_LIST_EMPTY(preferences_datums)
character.dna.skin_tone_override = use_custom_skin_tone ? skin_tone : null
character.hair_style = hair_style
character.facial_hair_style = facial_hair_style
+ character.grad_style = grad_style
+ character.grad_color = grad_color
character.underwear = underwear
character.saved_underwear = underwear
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 4d62bb149d..96ffd09af8 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -683,6 +683,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
S["skin_tone"] >> skin_tone
S["hair_style_name"] >> hair_style
S["facial_style_name"] >> facial_hair_style
+ S["grad_style"] >> grad_style
+ S["grad_color"] >> grad_color
S["underwear"] >> underwear
S["undie_color"] >> undie_color
S["undershirt"] >> undershirt
@@ -875,6 +877,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
age = sanitize_integer(age, AGE_MIN, AGE_MAX, initial(age))
hair_color = sanitize_hexcolor(hair_color, 6, FALSE)
facial_hair_color = sanitize_hexcolor(facial_hair_color, 6, FALSE)
+ grad_style = sanitize_inlist(grad_style, GLOB.hair_gradients_list, "None")
+ grad_color = sanitize_hexcolor(grad_color, 6, FALSE)
eye_type = sanitize_inlist(eye_type, GLOB.eye_types, DEFAULT_EYES_TYPE)
left_eye_color = sanitize_hexcolor(left_eye_color, 6, FALSE)
right_eye_color = sanitize_hexcolor(right_eye_color, 6, FALSE)
@@ -1044,6 +1048,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
WRITE_FILE(S["skin_tone"] , skin_tone)
WRITE_FILE(S["hair_style_name"] , hair_style)
WRITE_FILE(S["facial_style_name"] , facial_hair_style)
+ WRITE_FILE(S["grad_style"] , grad_style)
+ WRITE_FILE(S["grad_color"] , grad_color)
WRITE_FILE(S["underwear"] , underwear)
WRITE_FILE(S["undie_color"] , undie_color)
WRITE_FILE(S["undershirt"] , undershirt)
diff --git a/code/modules/clothing/masks/gasmask.dm b/code/modules/clothing/masks/gasmask.dm
index 525db577e0..2e3c544d08 100644
--- a/code/modules/clothing/masks/gasmask.dm
+++ b/code/modules/clothing/masks/gasmask.dm
@@ -83,6 +83,7 @@
icon_state = "clown"
item_state = "clown_hat"
dye_color = "clown"
+ w_class = WEIGHT_CLASS_SMALL
flags_cover = MASKCOVERSEYES
resistance_flags = FLAMMABLE
actions_types = list(/datum/action/item_action/adjust)
@@ -131,6 +132,7 @@
clothing_flags = ALLOWINTERNALS
icon_state = "mime"
item_state = "mime"
+ w_class = WEIGHT_CLASS_SMALL
flags_cover = MASKCOVERSEYES
resistance_flags = FLAMMABLE
actions_types = list(/datum/action/item_action/adjust)
diff --git a/code/modules/clothing/outfits/ert.dm b/code/modules/clothing/outfits/ert.dm
index 7cd2e9b394..86aa0748df 100644
--- a/code/modules/clothing/outfits/ert.dm
+++ b/code/modules/clothing/outfits/ert.dm
@@ -30,7 +30,7 @@
glasses = /obj/item/clothing/glasses/hud/security/sunglasses
back = /obj/item/storage/backpack/captain
belt = /obj/item/storage/belt/security/full
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer=1,\
/obj/item/gun/energy/e_gun=1)
@@ -50,7 +50,7 @@
suit = /obj/item/clothing/suit/space/hardsuit/ert/alert
glasses = /obj/item/clothing/glasses/thermal/eyepatch
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/gun/energy/e_gun=1)
@@ -58,7 +58,7 @@
/datum/outfit/ert/commander/alert/red
name = "ERT Commander - Red Alert"
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/gun/energy/pulse/pistol/loyalpin=1)
@@ -71,7 +71,7 @@
glasses = /obj/item/clothing/glasses/hud/security/sunglasses
belt = /obj/item/storage/belt/security/full
back = /obj/item/storage/backpack/security
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/storage/box/handcuffs=1,\
/obj/item/clothing/mask/gas/sechailer=1,\
/obj/item/gun/energy/e_gun/stun=1,\
@@ -91,7 +91,7 @@
name = "ERT Security - Amber Alert"
suit = /obj/item/clothing/suit/space/hardsuit/ert/alert/sec
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/storage/box/handcuffs=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/melee/baton/loaded=1,\
@@ -99,7 +99,7 @@
/datum/outfit/ert/security/alert/red
name = "ERT Security - Red Alert"
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/storage/box/handcuffs=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/melee/baton/loaded=1,\
@@ -114,7 +114,7 @@
back = /obj/item/storage/backpack/satchel/med
belt = /obj/item/storage/belt/medical
r_hand = /obj/item/storage/firstaid/regular
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer=1,\
/obj/item/gun/energy/e_gun=1,\
@@ -135,7 +135,7 @@
name = "ERT Medic - Amber Alert"
suit = /obj/item/clothing/suit/space/hardsuit/ert/alert/med
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/gun/energy/e_gun=1,\
@@ -144,7 +144,7 @@
/datum/outfit/ert/medic/alert/red
name = "ERT Medic - Red Alert"
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/gun/energy/pulse/pistol/loyalpin=1,\
@@ -161,7 +161,7 @@
belt = /obj/item/storage/belt/utility/full
l_pocket = /obj/item/rcd_ammo/large
r_hand = /obj/item/storage/firstaid/regular
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer=1,\
/obj/item/gun/energy/e_gun=1,\
@@ -181,7 +181,7 @@
name = "ERT Engineer - Amber Alert"
suit = /obj/item/clothing/suit/space/hardsuit/ert/alert/engi
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/gun/energy/e_gun=1,\
@@ -189,7 +189,7 @@
/datum/outfit/ert/engineer/alert/red
name = "ERT Engineer - Red Alert"
- backpack_contents = list(/obj/item/storage/box/engineer=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,\
/obj/item/melee/baton/loaded=1,\
/obj/item/clothing/mask/gas/sechailer/swat=1,\
/obj/item/gun/energy/pulse/pistol/loyalpin=1,\
@@ -260,7 +260,7 @@
name = "Inquisition Commander"
r_hand = /obj/item/nullrod/scythe/talking/chainsword
suit = /obj/item/clothing/suit/space/hardsuit/ert/paranormal
- backpack_contents = list(/obj/item/storage/box/engineer=1,
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,
/obj/item/clothing/mask/gas/sechailer=1,
/obj/item/gun/energy/e_gun=1)
@@ -269,7 +269,7 @@
suit = /obj/item/clothing/suit/space/hardsuit/ert/paranormal/inquisitor
- backpack_contents = list(/obj/item/storage/box/engineer=1,
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,
/obj/item/storage/box/handcuffs=1,
/obj/item/clothing/mask/gas/sechailer=1,
/obj/item/gun/energy/e_gun/stun=1,
@@ -281,7 +281,7 @@
suit = /obj/item/clothing/suit/space/hardsuit/ert/paranormal/inquisitor
- backpack_contents = list(/obj/item/storage/box/engineer=1,
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,
/obj/item/melee/baton/loaded=1,
/obj/item/clothing/mask/gas/sechailer=1,
/obj/item/gun/energy/e_gun=1,
@@ -307,7 +307,7 @@
glasses = /obj/item/clothing/glasses/hud/health
back = /obj/item/storage/backpack/cultpack
belt = /obj/item/storage/belt/soulstone
- backpack_contents = list(/obj/item/storage/box/engineer=1,
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,
/obj/item/nullrod=1,
/obj/item/clothing/mask/gas/sechailer=1,
/obj/item/gun/energy/e_gun=1,
@@ -319,7 +319,7 @@
suit = /obj/item/clothing/suit/space/hardsuit/ert/paranormal/inquisitor
belt = /obj/item/storage/belt/soulstone/full/chappy
- backpack_contents = list(/obj/item/storage/box/engineer=1,
+ backpack_contents = list(/obj/item/storage/box/survival/engineer=1,
/obj/item/grenade/chem_grenade/holy=1,
/obj/item/nullrod=1,
/obj/item/clothing/mask/gas/sechailer=1,
diff --git a/code/modules/clothing/outfits/vr.dm b/code/modules/clothing/outfits/vr.dm
index acf015c845..ac852a35a8 100644
--- a/code/modules/clothing/outfits/vr.dm
+++ b/code/modules/clothing/outfits/vr.dm
@@ -28,7 +28,7 @@
id = /obj/item/card/id/syndicate/locked_banking
belt = /obj/item/gun/ballistic/automatic/pistol
l_pocket = /obj/item/paper/fluff/vr/fluke_ops
- backpack_contents = list(/obj/item/storage/box/syndie=1,\
+ backpack_contents = list(/obj/item/storage/box/survival/syndie=1,\
/obj/item/kitchen/knife/combat/survival)
starting_funds = 0 //Should be operating, not shopping.
diff --git a/code/modules/clothing/spacesuits/hardsuit.dm b/code/modules/clothing/spacesuits/hardsuit.dm
index 4af9e7387d..cd48a81350 100644
--- a/code/modules/clothing/spacesuits/hardsuit.dm
+++ b/code/modules/clothing/spacesuits/hardsuit.dm
@@ -237,12 +237,28 @@
/obj/item/clothing/head/helmet/space/hardsuit/mining/Initialize()
. = ..()
AddComponent(/datum/component/armor_plate)
+ RegisterSignal(src, COMSIG_ARMOR_PLATED, .proc/upgrade_icon)
+
+/obj/item/clothing/head/helmet/space/hardsuit/mining/proc/upgrade_icon(datum/source, amount, maxamount)
+ SIGNAL_HANDLER
+
+ if(amount)
+ name = "reinforced [initial(name)]"
+ hardsuit_type = "mining_goliath"
+ if(amount == maxamount)
+ hardsuit_type = "mining_goliath_full"
+ icon_state = "hardsuit[on]-[hardsuit_type]"
+ if(ishuman(loc))
+ var/mob/living/carbon/human/wearer = loc
+ if(wearer.head == src)
+ wearer.update_inv_head()
/obj/item/clothing/suit/space/hardsuit/mining
icon_state = "hardsuit-mining"
name = "mining hardsuit"
desc = "A special suit that protects against hazardous, low pressure environments. Has reinforced plating for wildlife encounters."
item_state = "mining_hardsuit"
+ hardsuit_type = "mining"
max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
resistance_flags = FIRE_PROOF
armor = list("melee" = 30, "bullet" = 5, "laser" = 10, "energy" = 5, "bomb" = 50, "bio" = 100, "rad" = 50, "fire" = 50, "acid" = 75, "wound" = 15)
@@ -254,6 +270,21 @@
/obj/item/clothing/suit/space/hardsuit/mining/Initialize()
. = ..()
AddComponent(/datum/component/armor_plate)
+ RegisterSignal(src, COMSIG_ARMOR_PLATED, .proc/upgrade_icon)
+
+/obj/item/clothing/suit/space/hardsuit/mining/proc/upgrade_icon(datum/source, amount, maxamount)
+ SIGNAL_HANDLER
+
+ if(amount)
+ name = "reinforced [initial(name)]"
+ hardsuit_type = "mining_goliath"
+ if(amount == maxamount)
+ hardsuit_type = "mining_goliath_full"
+ icon_state = "hardsuit-[hardsuit_type]"
+ if(ishuman(loc))
+ var/mob/living/carbon/human/wearer = loc
+ if(wearer.wear_suit == src)
+ wearer.update_inv_wear_suit()
//Syndicate hardsuit
/obj/item/clothing/head/helmet/space/hardsuit/syndi
diff --git a/code/modules/clothing/suits/armor.dm b/code/modules/clothing/suits/armor.dm
index 254ed60c03..90fd0b2812 100644
--- a/code/modules/clothing/suits/armor.dm
+++ b/code/modules/clothing/suits/armor.dm
@@ -136,17 +136,24 @@
icon_state = "syndievest"
mutantrace_variation = STYLE_DIGITIGRADE
-/obj/item/clothing/suit/armor/vest/capcarapace/alt
+/obj/item/clothing/suit/toggle/captains_parade
name = "captain's parade jacket"
desc = "For when an armoured vest isn't fashionable enough."
icon_state = "capformal"
item_state = "capspacesuit"
+ body_parts_covered = CHEST|GROIN|ARMS
+ armor = list("melee" = 50, "bullet" = 40, "laser" = 50, "energy" = 50, "bomb" = 25, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 90, "wound" = 10)
+ togglename = "buttons"
+
+/obj/item/clothing/suit/toggle/captains_parade/Initialize()
+ . = ..()
+ allowed = GLOB.security_wintercoat_allowed
/obj/item/clothing/suit/armor/riot
name = "riot suit"
desc = "A suit of semi-flexible polycarbonate body armor with heavy padding to protect against melee attacks. Helps the wearer resist shoving in close quarters."
- icon_state = "swat"
- item_state = "swat_suit"
+ icon_state = "riot"
+ item_state = "riot"
body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
cold_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
heat_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
@@ -319,3 +326,29 @@
cold_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
armor = list("melee" = 25, "bullet" = 20, "laser" = 20, "energy" = 10, "bomb" = 20, "bio" = 50, "rad" = 20, "fire" = -10, "acid" = 50, "wound" = 10)
+
+/obj/item/clothing/suit/toggle/armor/vest/centcom_formal
+ name = "\improper CentCom formal coat"
+ desc = "A stylish coat given to CentCom Commanders. Perfect for sending ERTs to suicide missions with style!"
+ icon_state = "centcom_formal"
+ item_state = "centcom"
+ body_parts_covered = CHEST|GROIN|ARMS
+ armor = list("melee" = 35, "bullet" = 40, "laser" = 40, "energy" = 50, "bomb" = 35, "bio" = 10, "rad" = 10, "fire" = 10, "acid" = 60)
+ togglename = "buttons"
+
+/obj/item/clothing/suit/toggle/armor/vest/centcom_formal/Initialize()
+ . = ..()
+ allowed = GLOB.security_wintercoat_allowed
+
+/obj/item/clothing/suit/toggle/armor/hos/hos_formal
+ name = "\improper Head of Security's parade jacket"
+ desc = "For when an armoured vest isn't fashionable enough."
+ icon_state = "hosformal"
+ item_state = "hostrench"
+ body_parts_covered = CHEST|GROIN|ARMS
+ armor = list("melee" = 30, "bullet" = 30, "laser" = 30, "energy" = 40, "bomb" = 25, "bio" = 0, "rad" = 0, "fire" = 70, "acid" = 90, "wound" = 10)
+ togglename = "buttons"
+
+/obj/item/clothing/suit/toggle/armor/hos/hos_formal/Initialize()
+ . = ..()
+ allowed = GLOB.security_wintercoat_allowed
diff --git a/code/modules/clothing/suits/toggles.dm b/code/modules/clothing/suits/toggles.dm
index 4e29483846..78708f54bb 100644
--- a/code/modules/clothing/suits/toggles.dm
+++ b/code/modules/clothing/suits/toggles.dm
@@ -6,6 +6,7 @@
var/hoodtype = /obj/item/clothing/head/hooded/winterhood //so the chaplain hoodie or other hoodies can override this
///Alternative mode for hiding the hood, instead of storing the hood in the suit it qdels it, useful for when you deal with hooded suit with storage.
var/alternative_mode = FALSE
+ var/no_t //do not update sprites when pulling up hood so we can avoid oddities with certain mechanics
/obj/item/clothing/suit/hooded/Initialize()
. = ..()
@@ -51,6 +52,8 @@
update_icon()
/obj/item/clothing/suit/hooded/update_icon_state()
+ if(no_t)
+ return
icon_state = "[initial(icon_state)]"
if(ishuman(hood?.loc))
var/mob/living/carbon/human/H = hood.loc
diff --git a/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm b/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm
index 4fc55ba4c3..303d18b4a4 100644
--- a/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm
+++ b/code/modules/food_and_drinks/kitchen_machinery/icecream_vat.dm
@@ -8,8 +8,6 @@
#define CONE_WAFFLE 8
#define CONE_CHOC 9
-
-
/obj/machinery/icecream_vat
name = "ice cream vat"
desc = "Ding-aling ding dong. Get your Nanotrasen-approved ice cream!"
@@ -35,6 +33,8 @@
/datum/reagent/consumable/ethanol/singulo = 6,
/datum/reagent/consumable/peachjuice = 6,
/datum/reagent/consumable/grapejuice = 6)
+ var/custom_taste
+ var/custom_color
/obj/machinery/icecream_vat/proc/get_ingredient_list(type)
switch(type)
@@ -99,7 +99,10 @@
dat += "
Peach ice cream: Select Make x5 [product_types[ICECREAM_PEACH]] dollops left. (Ingredients: milk, ice, peach juice)
"
dat += "
Grape ice cream: Select Make x5 [product_types[ICECREAM_GRAPE]] dollops left. (Ingredients: milk, ice, grape juice)
"
dat += "
Blue ice cream: Select Make x5 [product_types[ICECREAM_BLUE]] dollops left. (Ingredients: milk, ice, singulo)
"
- dat += "
Custom ice cream: Select Make x5 [product_types[ICECREAM_CUSTOM]] dollops left. (Ingredients: milk, ice, optional flavoring)
[product_types[ICECREAM_CUSTOM]] dollops left. (Ingredients: milk, ice, optional flavoring)