diff --git a/aurorastation.dme b/aurorastation.dme
index 45d49c2b155..90dfd6cd067 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -22,6 +22,7 @@
#include "code\__DEFINES\_common.dm"
#include "code\__DEFINES\_compile_helpers.dm"
#include "code\__DEFINES\_flags.dm"
+#include "code\__DEFINES\_helpers.dm"
#include "code\__DEFINES\_layers.dm"
#include "code\__DEFINES\_macros.dm"
#include "code\__DEFINES\_protect.dm"
@@ -73,7 +74,6 @@
#include "code\__DEFINES\lists.dm"
#include "code\__DEFINES\logging.dm"
#include "code\__DEFINES\machinery.dm"
-#include "code\__DEFINES\manual_unit_testing.dm"
#include "code\__DEFINES\materials.dm"
#include "code\__DEFINES\math_physics.dm"
#include "code\__DEFINES\MC.dm"
@@ -133,6 +133,7 @@
#include "code\__DEFINES\dcs\flags.dm"
#include "code\__DEFINES\dcs\helpers.dm"
#include "code\__DEFINES\dcs\signals.dm"
+#include "code\__DEFINES\dcs\signals\signals_datum.dm"
#include "code\__DEFINES\dcs\signals\signals_global.dm"
#include "code\__DEFINES\dcs\signals\signals_record.dm"
#include "code\__DEFINES\dcs\signals\signals_subsystem.dm"
@@ -163,6 +164,7 @@
#include "code\__HELPERS\names.dm"
#include "code\__HELPERS\overlay.dm"
#include "code\__HELPERS\overmap.dm"
+#include "code\__HELPERS\qdel.dm"
#include "code\__HELPERS\sanitize_values.dm"
#include "code\__HELPERS\shell.dm"
#include "code\__HELPERS\smart_token_bucket.dm"
@@ -376,7 +378,7 @@
#include "code\datums\statistic.dm"
#include "code\datums\tgs_event_handler.dm"
#include "code\datums\tgui_module.dm"
-#include "code\datums\weakref.dm"
+#include "code\datums\weakrefs.dm"
#include "code\datums\components\_component.dm"
#include "code\datums\components\connect_mob_behalf.dm"
#include "code\datums\components\local_network.dm"
@@ -418,6 +420,7 @@
#include "code\datums\observation\shuttle_moved.dm"
#include "code\datums\observation\sight_set.dm"
#include "code\datums\observation\stat_set.dm"
+#include "code\datums\observation\~cleanup.dm"
#include "code\datums\outfits\outfit.dm"
#include "code\datums\outfits\outfit_admin.dm"
#include "code\datums\outfits\outfit_antag.dm"
@@ -1466,6 +1469,7 @@
#include "code\modules\admin\verbs\viewlist.dm"
#include "code\modules\admin\verbs\warning.dm"
#include "code\modules\admin\view_variables\helpers.dm"
+#include "code\modules\admin\view_variables\reference_tracking.dm"
#include "code\modules\admin\view_variables\topic.dm"
#include "code\modules\admin\view_variables\view_variables.dm"
#include "code\modules\alarm\alarm.dm"
diff --git a/code/__DEFINES/_helpers.dm b/code/__DEFINES/_helpers.dm
new file mode 100644
index 00000000000..998075b6a03
--- /dev/null
+++ b/code/__DEFINES/_helpers.dm
@@ -0,0 +1,15 @@
+// Stuff that is relatively "core" and is used in other defines/helpers
+
+//Returns the hex value of a decimal number
+//len == length of returned string
+#define num2hex(X, len) num2text(X, len, 16)
+
+//Returns an integer given a hex input, supports negative values "-ff"
+//skips preceding invalid characters
+#define hex2num(X) text2num(X, 16)
+
+/// subtypesof(), typesof() without the parent path
+#define subtypesof(typepath) ( typesof(typepath) - typepath )
+
+/// Takes a datum as input, returns its ref string
+#define text_ref(datum) ref(datum)
diff --git a/code/__DEFINES/_macros.dm b/code/__DEFINES/_macros.dm
index 37f78355a80..351ac9aed1e 100644
--- a/code/__DEFINES/_macros.dm
+++ b/code/__DEFINES/_macros.dm
@@ -2,9 +2,6 @@
#define CLAMP01(x) (Clamp(x, 0, 1))
#define JOINTEXT(X) jointext(X, null)
#define list_find(L, needle, LIMITS...) L.Find(needle, LIMITS)
-#define hex2num(hex) text2num(hex, 16)
-#define num2hex(num, pad) num2text(num, pad, 16)
-#define text_ref(datum) (isdatum(datum) ? (datum:cached_ref ||= "\ref[datum]") : ("\ref[datum]"))
#define span(class, text) ("" + text + "")
#define SPAN_NOTICE(X) ("" + X + "")
diff --git a/code/__DEFINES/auxtools.dm b/code/__DEFINES/auxtools.dm
index 885b57c8baa..1325c614072 100644
--- a/code/__DEFINES/auxtools.dm
+++ b/code/__DEFINES/auxtools.dm
@@ -8,8 +8,7 @@
CRASH("auxtools not loaded")
/world/Del()
- if(byond_version < 515)
- var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL")
- if (debug_server)
- call(debug_server, "auxtools_shutdown")()
+ var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL")
+ if (debug_server)
+ call_ext(debug_server, "auxtools_shutdown")()
. = ..()
diff --git a/code/__DEFINES/byond_compat.dm b/code/__DEFINES/byond_compat.dm
index 21fe5b62a2c..ccb88af21c5 100644
--- a/code/__DEFINES/byond_compat.dm
+++ b/code/__DEFINES/byond_compat.dm
@@ -1,57 +1,27 @@
-// 515 split call for external libraries into call_ext
-#if DM_VERSION < 515
-#define LIBCALL call
-#else
-#define LIBCALL call_ext
+// This file contains defines allowing targeting byond versions newer than the supported
+
+//Update this whenever you need to take advantage of more recent byond features
+#define MIN_COMPILER_VERSION 515
+#define MIN_COMPILER_BUILD 1609
+#if (DM_VERSION < MIN_COMPILER_VERSION || DM_BUILD < MIN_COMPILER_BUILD) && !defined(SPACEMAN_DMM) && !defined(OPENDREAM)
+//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 515.1609 or higher
#endif
-// So we want to have compile time guarantees these procs exist on local type, unfortunately 515 killed the .proc/procname syntax so we have to use nameof()
-#if DM_VERSION < 515
+// So we want to have compile time guarantees these methods exist on local type
+// We use wrappers for this in case some part of the api ever changes, and to make their function more clear
+// For the record: GLOBAL_VERB_REF would be useless as verbs can't be global.
- /**
- * Call by name proc reference, checks if the proc exists on this type or as a global proc
- *
- * * X - The proc name
- */
- #define PROC_REF(X) (.proc/##X)
+/// Call by name proc references, checks if the proc exists on either this type or as a global proc.
+#define PROC_REF(X) (nameof(.proc/##X))
+/// Call by name verb references, checks if the verb exists on either this type or as a global verb.
+#define VERB_REF(X) (nameof(.verb/##X))
- /**
- * Call by name proc reference, checks if the proc exists on given type or as a global proc
- *
- * * TYPE - The type (eg. `/datum/something` or `/atom`), without trailing slash
- * * X - The proc name
- */
- #define TYPE_PROC_REF(TYPE, X) (##TYPE.proc/##X)
+/// Call by name proc reference, checks if the proc exists on either the given type or as a global proc
+#define TYPE_PROC_REF(TYPE, X) (nameof(##TYPE.proc/##X))
+/// Call by name verb reference, checks if the verb exists on either the given type or as a global verb
+#define TYPE_VERB_REF(TYPE, X) (nameof(##TYPE.verb/##X))
- /**
- * Call by name proc reference, checks if the proc is existing global proc
- *
- * * X - The proc name
- */
- #define GLOBAL_PROC_REF(X) (/proc/##X)
-
-#else
-
- /**
- * Call by name proc reference, checks if the proc exists on this type or as a global proc
- *
- * * X - The proc name
- */
- #define PROC_REF(X) (nameof(.proc/##X))
-
- /**
- * Call by name proc reference, checks if the proc exists on given type or as a global proc
- *
- * * TYPE - The type (eg. `/datum/something` or `/atom`), without trailing slash
- * * X - The proc name
- */
- #define TYPE_PROC_REF(TYPE, X) (nameof(##TYPE.proc/##X))
-
- /**
- * Call by name proc reference, checks if the proc is existing global proc
- *
- * * X - The proc name
- */
- #define GLOBAL_PROC_REF(X) (/proc/##X)
-
-#endif
+/// Call by name proc reference, checks if the proc is an existing global proc
+#define GLOBAL_PROC_REF(X) (/proc/##X)
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 7b2e2dd1101..54c77295201 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -12,22 +12,6 @@
//////////////////////////////////////////////////////////////////
-// /datum signals
-/// when a component is added to a datum: (/datum/component)
-#define COMSIG_COMPONENT_ADDED "component_added"
-/// before a component is removed from a datum because of RemoveComponent: (/datum/component)
-#define COMSIG_COMPONENT_REMOVING "component_removing"
-/// before a datum's Destroy() is called: (force), returning a nonzero value will cancel the qdel operation
-#define COMSIG_PARENT_PREQDELETED "parent_preqdeleted"
-/// just before a datum's Destroy() is called: (force), at this point none of the other components chose to interrupt qdel and Destroy will be called
-#define COMSIG_QDELETING "parent_qdeleting"
-/// from datum ui_act (usr, action)
-#define COMSIG_UI_ACT "COMSIG_UI_ACT"
-/// fires on the target datum when an element is attached to it (/datum/element)
-#define COMSIG_ELEMENT_ATTACH "element_attach"
-/// fires on the target datum when an element is attached to it (/datum/element)
-#define COMSIG_ELEMENT_DETACH_ON_HOST_DESTROY "ELEMENT_DETACH_ON_HOST_DESTROY"
-
// /atom signals
diff --git a/code/__DEFINES/dcs/signals/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum.dm
new file mode 100644
index 00000000000..9ac01a252c0
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_datum.dm
@@ -0,0 +1,22 @@
+// Datum signals. Format:
+// When the signal is called: (signal arguments)
+// All signals send the source datum of the signal as the first argument
+
+// /datum signals
+/// when a component is added to a datum: (/datum/component)
+#define COMSIG_COMPONENT_ADDED "component_added"
+/// before a component is removed from a datum because of RemoveComponent: (/datum/component)
+#define COMSIG_COMPONENT_REMOVING "component_removing"
+
+/// before a datum's Destroy() is called: (force), returning a nonzero value will cancel the qdel operation
+/// you should only be using this if you want to block deletion
+/// that's the only functional difference between it and COMSIG_QDELETING, outside setting QDELETING to detect
+#define COMSIG_PREQDELETED "parent_preqdeleted"
+/// just before a datum's Destroy() is called: (force), at this point none of the other components chose to interrupt qdel and Destroy will be called
+#define COMSIG_QDELETING "parent_qdeleting"
+/// from datum ui_act (usr, action)
+#define COMSIG_UI_ACT "COMSIG_UI_ACT"
+/// fires on the target datum when an element is attached to it (/datum/element)
+#define COMSIG_ELEMENT_ATTACH "element_attach"
+/// fires on the target datum when an element is attached to it (/datum/element)
+#define COMSIG_ELEMENT_DETACH_ON_HOST_DESTROY "ELEMENT_DETACH_ON_HOST_DESTROY"
diff --git a/code/__DEFINES/global.dm b/code/__DEFINES/global.dm
index bb521a48320..4f3c913b040 100644
--- a/code/__DEFINES/global.dm
+++ b/code/__DEFINES/global.dm
@@ -1,6 +1,6 @@
//#define TESTING
-#if DM_VERSION < 513
-#error Your version of BYOND is too old to compile the code. At least BYOND 513 is required.
+#if DM_VERSION < 515 && !defined(OPENDREAM)
+#error Your version of BYOND is too old to compile the code. At least BYOND 515 is required.
#endif
diff --git a/code/__DEFINES/manual_unit_testing.dm b/code/__DEFINES/manual_unit_testing.dm
deleted file mode 100644
index f3dcdcb031e..00000000000
--- a/code/__DEFINES/manual_unit_testing.dm
+++ /dev/null
@@ -1,7 +0,0 @@
-// !!! For manual use only, remember to recomment before PRing !!!
-// #define UNIT_TEST
-// #define MANUAL_UNIT_TEST
-
-#if defined(MANUAL_UNIT_TEST) && !defined(SPACEMAN_DMM) && !defined(OPENDREAM)
- #warn Manual unit test is defined, remember to recomment it before PRing!
-#endif // MANUAL_UNIT_TEST
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index e001d1c02f9..650180ff72b 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -310,9 +310,6 @@
// Used for creating soft references to objects. A manner of storing an item reference
#define SOFTREF(A) ref(A)
-// This only works on 511 because it relies on 511's `var/something = foo = bar` syntax.
-#define WEAKREF(D) (istype(D, /datum) && !D:gcDestroyed ? (D:weakref || (D:weakref = new/datum/weakref(D))) : null)
-
#define ADD_VERB_IN(the_atom,time,verb) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(add_verb), the_atom, verb), time, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_NO_HASH_WAIT)
#define ADD_VERB_IN_IF(the_atom,time,verb,callback) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(add_verb), the_atom, verb, callback), time, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_NO_HASH_WAIT)
diff --git a/code/__DEFINES/profiler_tracy.dm b/code/__DEFINES/profiler_tracy.dm
index 33699d14ce5..8559b39833c 100644
--- a/code/__DEFINES/profiler_tracy.dm
+++ b/code/__DEFINES/profiler_tracy.dm
@@ -39,7 +39,7 @@ var/byond_tracy_running_v = null // Which version of byond-tracy is currently ru
if("tracy") lib = "tracy.dll"
if("tracy_disk") lib = "tracy-disk.dll"
- var/init = LIBCALL(lib, "init")()
+ var/init = call_ext(lib, "init")()
if("0" != init) CRASH("[lib] init error: [init]")
byond_tracy_running = 1
@@ -56,7 +56,7 @@ var/byond_tracy_running_v = null // Which version of byond-tracy is currently ru
if("tracy") lib = "tracy.dll"
if("tracy_disk") lib = "tracy-disk.dll"
- LIBCALL(lib, "destroy")()
+ call_ext(lib, "destroy")()
byond_tracy_running = 0
byond_tracy_running_v = null
diff --git a/code/__DEFINES/qdel.dm b/code/__DEFINES/qdel.dm
index 5cce66345db..a1ffd5c3699 100644
--- a/code/__DEFINES/qdel.dm
+++ b/code/__DEFINES/qdel.dm
@@ -1,36 +1,88 @@
+/**
+ * THIS IS A TEMPORARY WORKAROUND WHILE WE RESOLVE HARDDELS WITH THE NEW GC
+ *
+ * YOU ARE ONLY ALLOWED TO REMOVE THE AMOUNT OF THEM, NOT ADD MORE
+ *
+ * AND FOR WHAT MATTERS, THE SAME GOES FOR QDEL_HINT_HARDDEL,
+ * I DO NOT CARE HOW MANY REWRITES AND REFACTORS YOU HAVE TO DO, WHATEVER YOU ADD NEEDS TO BE GARGAGE COLLECTABLE, OR HAVE AN EXPLAINED, CLEAR, UNFIXABLE REASON WHY IT IS NOT
+ *
+ */
+#define GC_TEMPORARY_HARDDEL return QDEL_HINT_HARDDEL
+
//! Defines that give qdel hints.
//!
//! These can be given as a return in [/atom/proc/Destroy] or by calling [/proc/qdel].
/// `qdel` should queue the object for deletion.
-#define QDEL_HINT_QUEUE 0
-/// `qdel` should let the object live after calling [/datum/proc/Destroy].
-#define QDEL_HINT_LETMELIVE 1
-/// Functionally the same as above. `qdel` should assume the object will gc on its own, and not check it.
-#define QDEL_HINT_IWILLGC 2
-/// `qdel` should assume this object won't GC, and queue a hard delete using a hardref.
-#define QDEL_HINT_HARDDEL 3
-/// `qdel` should assume this object won't GC, and hard delete it immediately.
-#define QDEL_HINT_HARDDEL_NOW 4
+#define QDEL_HINT_QUEUE 0
+/// `qdel` should let the object live after calling [/atom/proc/Destroy].
+#define QDEL_HINT_LETMELIVE 1
+/// Functionally the same as the above. `qdel` should assume the object will gc on its own, and not check it.
+#define QDEL_HINT_IWILLGC 2
+/// Qdel should assume this object won't GC, and queue a hard delete using a hard reference.
+#define QDEL_HINT_HARDDEL 3
+// Qdel should assume this object won't gc, and hard delete it posthaste.
+#define QDEL_HINT_HARDDEL_NOW 4
-/*
-* If REFERENCE_TRACKING and [GC_FAILURE_HARD_LOOKUP] are not enabled, these are functionally identical to [QDEL_HINT_QUEUE].
-* If they are enabled, qdel will call this object's find_references() verb.
-*/
-#define QDEL_HINT_FINDREFERENCE 5
-/// Same behavior as [QDEL_HINT_FINDREFERENCE], but only if GC fails and a hard delete is forced.
+
+#ifdef REFERENCE_TRACKING
+
+ //A warning on compile is treated as an error in the CI, therefore unlike TG we must avoid the warn if it's running in the CI
+ #if defined(TESTING) && !defined(CIBUILDING) && !defined(OPENDREAM) && !defined(SPACEMAN_DMM)
+ /* If REFERENCE_TRACKING is enabled, qdel will call this object's find_references() verb.
+
+ Functionally identical to [QDEL_HINT_QUEUE] if [GC_FAILURE_HARD_LOOKUP] is not enabled in _compiler_options.dm.
+ */
+ #warn TG0001 qdel REFERENCE_TRACKING enabled
+ #endif
+
+#define QDEL_HINT_FINDREFERENCE 5
+/// Behavior as [QDEL_HINT_FINDREFERENCE], but only if the GC fails and a hard delete is forced.
#define QDEL_HINT_IFFAIL_FINDREFERENCE 6
-//defines for the gcDestroyed var
+#endif
-#define GC_QUEUED_FOR_QUEUING -1
-#define GC_QUEUED_FOR_HARD_DEL -2
-#define GC_CURRENTLY_BEING_QDELETED -3
+// Defines for the ssgarbage queues
+#define GC_QUEUE_FILTER 1 //! short queue to filter out quick gc successes so they don't hang around in the main queue for 5 minutes
+#define GC_QUEUE_CHECK 2 //! main queue that waits 5 minutes because thats the longest byond can hold a reference to our shit.
+#define GC_QUEUE_HARDDELETE 3 //! short queue for things that hard delete instead of going thru the gc subsystem, this is purely so if they *can* softdelete, they will soft delete rather then wasting time with a hard delete.
+#define GC_QUEUE_COUNT 3 //! Number of queues, used for allocating the nested lists. Don't forget to increase this if you add a new queue stage
-#define QDELING(X) (X.gcDestroyed)
-#define QDELETED(X) (!X || X.gcDestroyed)
-#define QDESTROYING(X) (!X || X.gcDestroyed == GC_CURRENTLY_BEING_QDELETED)
-#define QDEL_IN(item, time) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), item), time, TIMER_STOPPABLE)
-#define QDEL_NULL(item) qdel(item); item = null
-#define QDEL_NULL_LIST(x) if(x) { for(var/y in x) { qdel(y) }}; if(x) {x.Cut(); x = null } // Second x check to handle items that LAZYREMOVE on qdel.
-#define QDEL_NULL_ASSOC(x) if(x) { for(var/y in x) { qdel(x[y])}}; if(x) {x.Cut(); x = null } // As above, but qdels the value rather than the key
+// Defines for the ssgarbage queue items
+#define GC_QUEUE_ITEM_QUEUE_TIME 1 //! Time this item entered the queue
+#define GC_QUEUE_ITEM_REF 2 //! Ref to the item
+#define GC_QUEUE_ITEM_GCD_DESTROYED 3 //! Item's gc_destroyed var value. Used to detect ref reuse.
+#define GC_QUEUE_ITEM_INDEX_COUNT 3 //! Number of item indexes, used for allocating the nested lists. Don't forget to increase this if you add a new queue item index
+
+// Defines for the time an item has to get its reference cleaned before it fails the queue and moves to the next.
+#define GC_FILTER_QUEUE (1 SECONDS)
+#define GC_CHECK_QUEUE (5 MINUTES)
+#define GC_DEL_QUEUE (10 SECONDS)
+
+
+#define QDEL_ITEM_ADMINS_WARNED (1<<0) //! Set when admins are told about lag causing qdels in this type.
+#define QDEL_ITEM_SUSPENDED_FOR_LAG (1<<1) //! Set when a type can no longer be hard deleted on failure because of lag it causes while this happens.
+
+// Defines for the [gc_destroyed][/datum/var/gc_destroyed] var.
+#define GC_CURRENTLY_BEING_QDELETED -2
+
+#define QDELING(X) (X.gc_destroyed)
+#define QDELETED(X) (isnull(X) || QDELING(X))
+#define QDESTROYING(X) (!X || X.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
+
+// This is a bit hacky, we do it to avoid people relying on a return value for the macro
+// If you need that you should use QDEL_IN_STOPPABLE instead
+#define QDEL_IN(item, time) ; \
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), (time) > GC_FILTER_QUEUE ? WEAKREF(item) : item), time);
+#define QDEL_IN_STOPPABLE(item, time) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), (time) > GC_FILTER_QUEUE ? WEAKREF(item) : item), time, TIMER_STOPPABLE)
+#define QDEL_IN_CLIENT_TIME(item, time) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), item), time, TIMER_STOPPABLE | TIMER_CLIENT_TIME)
+
+//Bay uses the check before deleting something, to ensure it's not null, TG does not, and since I don't want to go hunt down 3 billion runtimes, we keep bay's version
+
+///Delete and null the reference, if the object isn't null
+#define QDEL_NULL(item) if(item) { qdel(item) ; item = null }
+
+#define QDEL_LIST(L) if(L) { for(var/I in L) qdel(I); L.Cut(); }
+#define QDEL_LIST_IN(L, time) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(______qdel_list_wrapper), L), time, TIMER_STOPPABLE)
+#define QDEL_LIST_ASSOC(L) if(L) { for(var/I in L) { qdel(L[I]); qdel(I); } L.Cut(); }
+#define QDEL_LIST_ASSOC_VAL(L) if(L) { for(var/I in L) qdel(L[I]); L.Cut(); }
diff --git a/code/__DEFINES/subsystem-priority.dm b/code/__DEFINES/subsystem-priority.dm
index 5dce9d45ce2..15de111c58a 100644
--- a/code/__DEFINES/subsystem-priority.dm
+++ b/code/__DEFINES/subsystem-priority.dm
@@ -1,4 +1,9 @@
-#define INIT_ORDER_PROFILER 101
+/*
+ THIS IS A LEGACY FILE, DO NOT ADD ONTOP OF THIS
+ USE code\__DEFINES\subsystems.dm AND THE APPROPRIATE DEF NAMES
+ THAT ARE PRESENT THERE
+*/
+
#define SS_INIT_PERSISTENT_CONFIG 26
#define SS_INIT_MISC_FIRST 25
#define SS_INIT_SEEDS 24 // Plant controller setup.
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 42d02f92aee..08180ee8bbe 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -191,10 +191,19 @@
/// The timer key used to know how long subsystem initialization takes
#define SS_INIT_TIMER_KEY "ss_init"
+//! ### SS initialization load orders
+// Subsystem init_order, from highest priority to lowest priority
+// Subsystems shutdown in the reverse of the order they initialize in
+// The numbers just define the ordering, they are meaningless otherwise.
+
+#define INIT_ORDER_PROFILER 101
+#define INIT_ORDER_GARBAGE 99
+
// Subsystem fire priority, from lowest to highest priority
// If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child)
#define FIRE_PRIORITY_DEFAULT 50
+#define FIRE_PRIORITY_GARBAGE 15
/* AURORA SHIT */
diff --git a/code/__DEFINES/time.dm b/code/__DEFINES/time.dm
index 73eb12b0f91..5a201855611 100644
--- a/code/__DEFINES/time.dm
+++ b/code/__DEFINES/time.dm
@@ -1,3 +1,7 @@
+#define MILLISECONDS *0.01
+
+#define DECISECONDS *1 //the base unit all of these defines are scaled by, because byond uses that as a unit of measurement for some fucking reason
+
#define SECOND *10
#define SECONDS *10
@@ -16,3 +20,7 @@
#define DS2TICKS(DS) ((DS)/world.tick_lag)
#define TICKS2DS(T) ((T) TICKS)
+
+#define MS2DS(T) ((T) MILLISECONDS)
+
+#define DS2MS(T) ((T) * 100)
diff --git a/code/__HELPERS/dll_call.dm b/code/__HELPERS/dll_call.dm
index aea6734bb61..2d174d216d8 100644
--- a/code/__HELPERS/dll_call.dm
+++ b/code/__HELPERS/dll_call.dm
@@ -8,7 +8,7 @@
var/list/calling_arguments = length(args) > 2 ? args.Copy(3) : null
- . = LIBCALL(dll, func)(arglist(calling_arguments))
+ . = call_ext(dll, func)(arglist(calling_arguments))
if (world.timeofday - start > 10 SECONDS)
crash_with("DLL call took longer than 10 seconds: [func]")
diff --git a/code/__HELPERS/lists.dm b/code/__HELPERS/lists.dm
index 4162c84bd7f..1bc0957e571 100644
--- a/code/__HELPERS/lists.dm
+++ b/code/__HELPERS/lists.dm
@@ -544,9 +544,6 @@
/proc/is_list_containing_type(var/list/L, type)
return count_by_type(L, type) == L.len
-/proc/subtypesof(prototype)
- return (typesof(prototype) - prototype)
-
//creates every subtype of prototype (excluding prototype) and adds it to list L.
//if no list/L is provided, one is created.
/proc/init_subtypes(prototype, list/L)
diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm
index 98f6272052c..110c94eb3a7 100644
--- a/code/__HELPERS/mobs.dm
+++ b/code/__HELPERS/mobs.dm
@@ -68,6 +68,8 @@
return current_species ? current_species.sanitize_name(name) : sanitizeName(name)
/proc/random_name(gender, species = SPECIES_HUMAN)
+ SHOULD_NOT_SLEEP(TRUE)
+
var/datum/species/current_species
if(species)
current_species = GLOB.all_species[species]
diff --git a/code/__HELPERS/nameof.dm b/code/__HELPERS/nameof.dm
index 7cd5777f465..5a2fd60e710 100644
--- a/code/__HELPERS/nameof.dm
+++ b/code/__HELPERS/nameof.dm
@@ -8,8 +8,4 @@
/**
* NAMEOF that actually works in static definitions because src::type requires src to be defined
*/
-#if DM_VERSION >= 515
#define NAMEOF_STATIC(datum, X) (nameof(type::##X))
-#else
-#define NAMEOF_STATIC(datum, X) (#X || ##datum.##X)
-#endif
diff --git a/code/__HELPERS/qdel.dm b/code/__HELPERS/qdel.dm
new file mode 100644
index 00000000000..bbbf1827db5
--- /dev/null
+++ b/code/__HELPERS/qdel.dm
@@ -0,0 +1,4 @@
+
+///the underscores are to encourage people not to use this directly.
+/proc/______qdel_list_wrapper(list/L)
+ QDEL_LIST(L)
diff --git a/code/__HELPERS/sorting/cmp.dm b/code/__HELPERS/sorting/cmp.dm
index 7baad507053..331863a31ce 100644
--- a/code/__HELPERS/sorting/cmp.dm
+++ b/code/__HELPERS/sorting/cmp.dm
@@ -40,6 +40,15 @@
/proc/cmp_timer(datum/timedevent/a, datum/timedevent/b)
return a.timeToRun - b.timeToRun
+/proc/cmp_qdel_item_time(datum/qdel_item/A, datum/qdel_item/B)
+ . = B.hard_delete_time - A.hard_delete_time
+ if (!.)
+ . = B.destroy_time - A.destroy_time
+ if (!.)
+ . = B.failures - A.failures
+ if (!.)
+ . = B.qdels - A.qdels
+
/proc/cmp_camera(obj/machinery/camera/a, obj/machinery/camera/b)
if (a.c_tag_order != b.c_tag_order)
return b.c_tag_order - a.c_tag_order
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index 382160b23bb..25fefdf36f9 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -30,10 +30,18 @@
Manual runs area, uncomment the appropriate defines depending on what you want to do
*/
-//#define TESTING // Creates debug feedback messages and enables many optional testing procs/checks
+// !!! For manual use only, remember to recomment before PRing !!!
+// #define TESTING // Creates debug feedback messages and enables many optional testing procs/checks
+// #define UNIT_TEST
+// #define MANUAL_UNIT_TEST
+
+#if defined(MANUAL_UNIT_TEST) && !defined(SPACEMAN_DMM) && !defined(OPENDREAM)
+ #warn Manual unit test is defined, remember to recomment it before PRing!
+#endif // MANUAL_UNIT_TEST
+
#ifdef TESTING
///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
+ // #define REFERENCE_TRACKING
#ifdef REFERENCE_TRACKING
//Run a lookup on things hard deleting by default.
@@ -77,7 +85,8 @@
//Test at full capacity, the extra cost doesn't matter
#define TIMER_DEBUG
- #define ZASDBG
+ //If you really are lost with atmos issues
+ // #define ZASDBG
#define SQL_PREF_DEBUG
#endif //UNIT_TEST
diff --git a/code/_onclick/hud/ability_screen_objects.dm b/code/_onclick/hud/ability_screen_objects.dm
index 5b97cf46c0c..1e9d6c5e4b5 100644
--- a/code/_onclick/hud/ability_screen_objects.dm
+++ b/code/_onclick/hud/ability_screen_objects.dm
@@ -19,7 +19,6 @@
update_abilities(0, owner)
/obj/screen/movable/ability_master/Destroy()
- . = ..()
//Get rid of the ability objects.
remove_all_abilities()
ability_objects.Cut()
@@ -31,6 +30,8 @@
my_mob.client.screen -= src
my_mob = null
+ . = ..()
+
/obj/screen/movable/ability_master/MouseDrop()
if(showing)
return
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index e6b00d9b296..64a20ad5629 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -149,7 +149,6 @@ var/list/global_huds
..()
/datum/hud/Destroy()
- . = ..()
grab_intent = null
hurt_intent = null
disarm_intent = null
@@ -167,6 +166,8 @@ var/list/global_huds
// item_action_list = null // ?
mymob = null
+ . = ..()
+
/datum/hud/proc/hidden_inventory_update()
if(!mymob) return
if(ishuman(mymob))
diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm
index b051a645f8c..a9ac5b43900 100644
--- a/code/_onclick/hud/radial.dm
+++ b/code/_onclick/hud/radial.dm
@@ -9,7 +9,8 @@ GLOBAL_LIST_EMPTY(radial_menus)
var/datum/radial_menu/parent
/obj/screen/radial/Destroy()
- qdel(parent)
+ QDEL_NULL(parent)
+
return ..()
/obj/screen/radial/slice
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 52229d0a001..f92a2f2bb09 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -86,8 +86,8 @@
var/obj/item/owner
/obj/screen/item_action/Destroy()
- . = ..()
owner = null
+ . = ..()
/obj/screen/item_action/Click()
if(!usr || !owner)
diff --git a/code/_onclick/hud/spell_screen_objects.dm b/code/_onclick/hud/spell_screen_objects.dm
index 886d55853d5..87d626a90e7 100644
--- a/code/_onclick/hud/spell_screen_objects.dm
+++ b/code/_onclick/hud/spell_screen_objects.dm
@@ -13,7 +13,6 @@
var/mob/spell_holder
/obj/screen/movable/spell_master/Destroy()
- . = ..()
for(var/obj/screen/spell/spells in spell_objects)
spells.spellmaster = null
spell_objects.Cut()
@@ -23,6 +22,8 @@
spell_holder.client.screen -= src
spell_holder = null
+ . = ..()
+
/obj/screen/movable/spell_master/MouseDrop()
if(showing)
return
@@ -157,7 +158,6 @@
var/icon/last_charged_icon
/obj/screen/spell/Destroy()
- . = ..()
spell = null
last_charged_icon = null
if(spellmaster)
@@ -168,6 +168,8 @@
qdel(spellmaster)
spellmaster = null
+ . = ..()
+
/obj/screen/spell/proc/update_charge(var/forced_update = 0)
if(!spell)
qdel(src)
diff --git a/code/controllers/globals.dm b/code/controllers/globals.dm
index 0fc64b901fd..e8411701644 100644
--- a/code/controllers/globals.dm
+++ b/code/controllers/globals.dm
@@ -13,7 +13,14 @@ GLOBAL_REAL(GLOB, /datum/controller/global_vars)
GLOB = src
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))
+ // I know this is dumb but the nested vars list hangs a ref to the datum. This fixes that
+ var/list/controller_vars = exclude_these.vars.Copy()
+ controller_vars["vars"] = null
+ gvars_datum_in_built_vars = controller_vars + list(NAMEOF(src, gvars_datum_protected_varlist), NAMEOF(src, gvars_datum_in_built_vars), NAMEOF(src, gvars_datum_init_order))
+
+#if MIN_COMPILER_VERSION >= 515 && MIN_COMPILER_BUILD > 1620
+ #warn datum.vars hanging a ref should now be fixed, there should be no reason to remove the vars list from our controller's vars list anymore
+#endif
QDEL_IN(exclude_these, 0) //signal logging isn't ready
Initialize()
diff --git a/code/controllers/subsystems/garbage.dm b/code/controllers/subsystems/garbage.dm
index 451a0e16671..17eee1f305c 100644
--- a/code/controllers/subsystems/garbage.dm
+++ b/code/controllers/subsystems/garbage.dm
@@ -1,39 +1,53 @@
+/*!
+## Debugging GC issues
+
+In order to debug `qdel()` failures, there are several tools available.
+To enable these tools, define `TESTING` in [_compile_options.dm](https://github.com/tgstation/-tg-station/blob/master/code/_compile_options.dm).
+
+First is a verb called "Find References", which lists **every** refererence to an object in the world. This allows you to track down any indirect or obfuscated references that you might have missed.
+
+Complementing this is another verb, "qdel() then Find References".
+This does exactly what you'd expect; it calls `qdel()` on the object and then it finds all references remaining.
+This is great, because it means that `Destroy()` will have been called before it starts to find references,
+so the only references you'll find will be the ones preventing the object from `qdel()`ing gracefully.
+
+If you have a datum or something you are not destroying directly (say via the singulo),
+the next tool is `QDEL_HINT_FINDREFERENCE`. You can return this in `Destroy()` (where you would normally `return ..()`),
+to print a list of references once it enters the GC queue.
+
+Finally is a verb, "Show qdel() Log", which shows the deletion log that the garbage subsystem keeps. This is helpful if you are having race conditions or need to review the order of deletions.
+
+Note that for any of these tools to work `TESTING` must be defined.
+By using these methods of finding references, you can make your life far, far easier when dealing with `qdel()` failures.
+*/
+
SUBSYSTEM_DEF(garbage)
name = "Garbage"
- priority = SS_PRIORITY_GARBAGE
+ priority = FIRE_PRIORITY_GARBAGE
wait = 2 SECONDS
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND|SS_NO_INIT
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
+ init_order = INIT_ORDER_GARBAGE
init_stage = INITSTAGE_EARLY
- var/collection_timeout = 3000// deciseconds to wait to let running procs finish before we just say fuck it and force del() the object
- var/delslasttick = 0 // number of del()'s we've done this tick
- var/gcedlasttick = 0 // number of things that gc'ed last tick
+ var/list/collection_timeout = list(GC_FILTER_QUEUE, GC_CHECK_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/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/queue = list() // list of refID's of things that should be garbage collected
- // refID's are associated with the time at which they time out and need to be manually del()
- // we do this so we aren't constantly locating them and preventing them from being gc'd
+ var/list/pass_counts
+ var/list/fail_counts
- var/list/tobequeued = list() //We store the references of things to be added to the queue seperately so we can spread out GC overhead over a few ticks
-
- var/list/didntgc = list() // list of all types that have failed to GC associated with the number of times that's happened.
- // the types are stored as strings
- var/list/sleptDestroy = list() //Same as above but these are paths that slept during their Destroy call
-
- var/list/noqdelhint = list()// list of all types that do not return a QDEL_HINT
- // all types that did not respect qdel(A, force=TRUE) and returned one
- // of the immortality qdel hints
- var/list/noforcerespect = list()
-
- #ifdef REFERENCE_TRACKING
- var/list/qdel_list = list() // list of all types that have been qdel()eted
- #endif
+ var/list/items = list() // Holds our qdel_item statistics datums
+ //Queue
+ var/list/queues
#ifdef REFERENCE_TRACKING
var/list/reference_find_on_fail = list()
#ifdef REFERENCE_TRACKING_DEBUG
@@ -42,8 +56,15 @@ SUBSYSTEM_DEF(garbage)
#endif
#endif
+
+/datum/controller/subsystem/garbage/PreInit()
+ InitQueues()
+
/datum/controller/subsystem/garbage/stat_entry(msg)
- msg = "W:[tobequeued.len]|Q:[queue.len]|D:[delslasttick]|G:[gcedlasttick]|"
+ var/list/counts = list()
+ for (var/list/L in queues)
+ counts += length(L)
+ msg += "Q:[counts.Join(",")]|D:[delslasttick]|G:[gcedlasttick]|"
msg += "GR:"
if (!(delslasttick+gcedlasttick))
msg += "n/a|"
@@ -55,405 +76,339 @@ SUBSYSTEM_DEF(garbage)
msg += "n/a|"
else
msg += "TGR:[round((totalgcs/(totaldels+totalgcs))*100, 0.01)]%"
+ msg += " P:[pass_counts.Join(",")]"
+ msg += "|F:[fail_counts.Join(",")]"
return ..()
+/datum/controller/subsystem/garbage/Shutdown()
+ //Adds the del() log to the qdel log file
+ var/list/del_log = list()
+
+ //sort by how long it's wasted hard deleting
+ sortTim(items, cmp=/proc/cmp_qdel_item_time, associative = TRUE)
+ for(var/path in items)
+ var/datum/qdel_item/I = items[path]
+ var/list/entry = list()
+ del_log[path] = entry
+
+ if (I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG)
+ entry["SUSPENDED FOR LAG"] = TRUE
+ if (I.failures)
+ entry["Failures"] = I.failures
+ entry["qdel() Count"] = I.qdels
+ entry["Destroy() Cost (ms)"] = I.destroy_time
+
+ if (I.hard_deletes)
+ entry["Total Hard Deletes"] = I.hard_deletes
+ entry["Time Spend Hard Deleting (ms)"] = I.hard_delete_time
+ entry["Highest Time Spend Hard Deleting (ms)"] = I.hard_delete_max
+ if (I.hard_deletes_over_threshold)
+ entry["Hard Deletes Over Threshold"] = I.hard_deletes_over_threshold
+ if (I.slept_destroy)
+ entry["Total Sleeps"] = I.slept_destroy
+ if (I.no_respect_force)
+ entry["Total Ignored Force"] = I.no_respect_force
+ if (I.no_hint)
+ entry["Total No Hint"] = I.no_hint
+ if(LAZYLEN(I.extra_details))
+ entry["Deleted Metadata"] = I.extra_details
+
+ log_subsystem_garbage(json_encode(del_log))
+
/datum/controller/subsystem/garbage/fire()
- HandleToBeQueued()
- if(state == SS_RUNNING)
- HandleQueue()
+ //the fact that this resets its processing each fire (rather then resume where it left off) is intentional.
+ var/queue = GC_QUEUE_FILTER
- if (state == SS_PAUSED)
- state = SS_RUNNING
+ while (state == SS_RUNNING)
+ switch (queue)
+ if (GC_QUEUE_FILTER)
+ HandleQueue(GC_QUEUE_FILTER)
+ queue = GC_QUEUE_FILTER+1
+ if (GC_QUEUE_CHECK)
+ HandleQueue(GC_QUEUE_CHECK)
+ queue = GC_QUEUE_CHECK+1
+ if (GC_QUEUE_HARDDELETE)
+ HandleQueue(GC_QUEUE_HARDDELETE)
+ if (state == SS_PAUSED) //make us wait again before the next run.
+ state = SS_RUNNING
+ break
-//If you see this proc high on the profile, what you are really seeing is the garbage collection/soft delete overhead in byond.
-//Don't attempt to optimize, not worth the effort.
-/datum/controller/subsystem/garbage/proc/HandleToBeQueued()
- var/list/tobequeued = src.tobequeued
- var/starttime = world.time
- var/starttimeofday = world.timeofday
- var/idex = 1
- while((tobequeued.len - (idex - 1)) && starttime == world.time && starttimeofday == world.timeofday)
- if (MC_TICK_CHECK)
- break
- var/ref = tobequeued[idex]
- tobequeued[idex++] = null // Clear this ref to assist hard deletes in Queue().
- Queue(ref)
- if (idex > 1)
- tobequeued.Cut(1, idex)
-/datum/controller/subsystem/garbage/proc/HandleQueue()
- delslasttick = 0
- gcedlasttick = 0
- var/time_to_kill = world.time - collection_timeout // Anything qdel() but not GC'd BEFORE this time needs to be manually del()
- var/list/queue = src.queue
- var/starttime = world.time
- var/starttimeofday = world.timeofday
- var/idex = 1
- #ifdef REFERENCE_TRACKING
- var/ref_searching = FALSE
- #endif
- while((queue.len - (idex - 1)) && starttime == world.time && starttimeofday == world.timeofday)
- if (MC_TICK_CHECK)
- break
- #ifdef REFERENCE_TRACKING
- if (ref_searching)
- break
- #endif
- var/refID = queue[idex]
- if (!refID)
- idex++
+/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_FILTER)
+ if (level == GC_QUEUE_FILTER)
+ delslasttick = 0
+ gcedlasttick = 0
+ var/cut_off_time = world.time - collection_timeout[level] //ignore entries newer then this
+ var/list/queue = queues[level]
+ var/static/lastlevel
+ var/static/count = 0
+ if (count) //runtime last run before we could do this.
+ var/c = count
+ count = 0 //so if we runtime on the Cut, we don't try again.
+ var/list/lastqueue = queues[lastlevel]
+ lastqueue.Cut(1, c+1)
+
+ lastlevel = level
+
+ //We do this rather then for(var/list/ref_info in queue) because that sort of for loop copies the whole list.
+ //Normally this isn't expensive, but the gc queue can grow to 40k items, and that gets costly/causes overrun.
+ for (var/i in 1 to length(queue))
+ var/list/L = queue[i]
+ if (length(L) < GC_QUEUE_ITEM_INDEX_COUNT)
+ count++
+ if (MC_TICK_CHECK)
+ return
continue
- var/GCd_at_time = queue[refID]
- if(GCd_at_time > time_to_kill)
+ var/queued_at_time = L[GC_QUEUE_ITEM_QUEUE_TIME]
+ if(queued_at_time > cut_off_time)
break // Everything else is newer, skip them
- idex++
- var/datum/A
- A = locate(refID)
- if (A && A.gcDestroyed == GCd_at_time) // So if something else coincidently gets the same ref, it's not deleted by mistake
- #ifdef REFERENCE_TRACKING
- if(reference_find_on_fail[text_ref(A)])
- INVOKE_ASYNC(A, TYPE_PROC_REF(/datum, find_references))
- ref_searching = TRUE
- #ifdef GC_FAILURE_HARD_LOOKUP
- else
- INVOKE_ASYNC(A, TYPE_PROC_REF(/datum, find_references))
- ref_searching = TRUE
- #endif
- reference_find_on_fail -= text_ref(A)
- #endif
+ count++
- // Something's still referring to the qdel'd object. Kill it.
- var/type = A.type
- log_subsystem_garbage_harddel("-- \ref[A] | [type] was unable to be GC'd and was deleted --", type)
- didntgc["[type]"]++
+ var/datum/D = L[GC_QUEUE_ITEM_REF]
- #ifdef REFERENCE_TRACKING
- if(ref_searching)
- continue //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
-
- HardDelete(A)
-
- ++delslasttick
- ++totaldels
- else
+ // 1 from the hard reference in the queue, and 1 from the variable used before this
+ // If that's all we've got, send er off
+ if (refcount(D) == 2)
++gcedlasttick
++totalgcs
+ pass_counts[level]++
#ifdef REFERENCE_TRACKING
- reference_find_on_fail -= text_ref(A)
+ reference_find_on_fail -= text_ref(D) //It's deleted we don't care anymore.
#endif
+ if (MC_TICK_CHECK)
+ return
+ continue
- if (idex > 1)
- queue.Cut(1, idex)
+ // Something's still referring to the qdel'd object.
+ fail_counts[level]++
-/datum/controller/subsystem/garbage/proc/QueueForQueuing(datum/A)
- if (istype(A) && A.gcDestroyed == GC_CURRENTLY_BEING_QDELETED)
- tobequeued += A
- A.gcDestroyed = GC_QUEUED_FOR_QUEUING
+ #ifdef REFERENCE_TRACKING
+ var/ref_searching = FALSE
+ #endif
-/datum/controller/subsystem/garbage/proc/Queue(datum/A)
- if (!istype(A) || (!isnull(A.gcDestroyed) && A.gcDestroyed >= 0))
+ switch (level)
+ if (GC_QUEUE_CHECK)
+ #ifdef REFERENCE_TRACKING
+ if(reference_find_on_fail[text_ref(D)])
+ INVOKE_ASYNC(D, TYPE_PROC_REF(/datum,find_references))
+ ref_searching = TRUE
+ #ifdef GC_FAILURE_HARD_LOOKUP
+ else
+ INVOKE_ASYNC(D, TYPE_PROC_REF(/datum,find_references))
+ ref_searching = TRUE
+ #endif
+ reference_find_on_fail -= text_ref(D)
+ #endif
+ var/type = D.type
+ var/datum/qdel_item/I = items[type]
+
+ var/message = "## TESTING: GC: -- [text_ref(D)] | [type] was unable to be GC'd --"
+ message = "[message] (ref count of [refcount(D)])"
+ log_world(message)
+
+ var/detail = D.dump_harddel_info()
+ if(detail)
+ LAZYADD(I.extra_details, detail)
+
+ #ifdef TESTING
+ for(var/c in GLOB.staff) //Using testing() here would fill the logs with ADMIN_VV garbage
+ var/client/admin = c
+ if(!check_rights(R_ADMIN, user = admin?.mob))
+ continue
+ // Used to be to_chat(admin, "## TESTING: GC: -- [ADMIN_VV(D)] | [type] was unable to be GC'd --") but we do not have that macro
+ to_chat(admin, "## TESTING: GC: -- VV | [type] was unable to be GC'd --")
+ #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)
+ return
+ continue
+
+ 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
+
+/datum/controller/subsystem/garbage/proc/Queue(datum/D, level = GC_QUEUE_FILTER)
+ if (isnull(D))
return
- if (A.gcDestroyed == GC_QUEUED_FOR_HARD_DEL)
- HardDelete(A)
+ if (level > GC_QUEUE_COUNT)
+ HardDelete(D)
return
- var/gctime = world.time
- var/refid = "\ref[A]"
+ var/queue_time = world.time
- A.gcDestroyed = gctime
+ if (D.gc_destroyed <= 0)
+ D.gc_destroyed = queue_time
- if (queue[refid])
- queue -= refid // Removing any previous references that were GC'd so that the current object will be at the end of the list.
+ var/list/queue = queues[level]
+ queue[++queue.len] = list(queue_time, D, D.gc_destroyed) // not += for byond reasons
- queue[refid] = gctime
+//this is mainly to separate things profile wise.
+/datum/controller/subsystem/garbage/proc/HardDelete(datum/D)
+ ++delslasttick
+ ++totaldels
+ var/type = D.type
+ var/refID = text_ref(D)
+ var/datum/qdel_item/type_info = items[type]
+ var/detail = D.dump_harddel_info()
+ if(detail)
+ LAZYADD(type_info.extra_details, detail)
-// For profiling.
-/datum/controller/subsystem/garbage/proc/HardDelete(datum/A)
- var/time = world.timeofday
- var/tick = world.tick_usage
- var/ticktime = world.time
+ var/tick_usage = TICK_USAGE
+ del(D)
+ tick_usage = TICK_USAGE_TO_MS(tick_usage)
- var/type = A.type
- var/refID = "\ref[A]"
+ type_info.hard_deletes++
+ type_info.hard_delete_time += tick_usage
+ if (tick_usage > type_info.hard_delete_max)
+ type_info.hard_delete_max = tick_usage
+ if (tick_usage > highest_del_ms)
+ highest_del_ms = tick_usage
+ highest_del_type_string = "[type]"
- del(A)
+ var/time = MS2DS(tick_usage)
- tick = (world.tick_usage-tick+((world.time-ticktime)/world.tick_lag*100))
- 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_subsystem_garbage_error("Error: [type]([refID]) took longer then 1 second to delete (took [time/10] seconds to delete)", type, TRUE)
- message_admins("Error: [type]([refID]) took longer then 1 second to delete (took [time/10] seconds to delete).")
- postpone(time/5)
-
-/datum/controller/subsystem/garbage/proc/HardQueue(datum/A)
- if (istype(A) && A.gcDestroyed == GC_CURRENTLY_BEING_QDELETED)
- tobequeued += A
- A.gcDestroyed = GC_QUEUED_FOR_HARD_DEL
+ if (time > 0.1 SECONDS)
+ postpone(time)
+ var/threshold = 0.5 // Used to be CONFIG_GET(number/hard_deletes_overrun_threshold)
+ if (threshold && (time > threshold SECONDS))
+ if (!(type_info.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).")
+ type_info.qdel_flags |= QDEL_ITEM_ADMINS_WARNED
+ type_info.hard_deletes_over_threshold++
+ var/overrun_limit = 0 // Used to be CONFIG_GET(number/hard_deletes_overrun_limit)
+ if (overrun_limit && type_info.hard_deletes_over_threshold >= overrun_limit)
+ type_info.qdel_flags |= QDEL_ITEM_SUSPENDED_FOR_LAG
/datum/controller/subsystem/garbage/Recover()
- if (istype(SSgarbage.queue))
- queue |= SSgarbage.queue
- if (istype(SSgarbage.tobequeued))
- tobequeued |= SSgarbage.tobequeued
+ 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]
-// Should be treated as a replacement for the 'del' keyword.
-// Datums passed to this will be given a chance to clean up references to allow the GC to collect them.
-/proc/qdel(datum/D, force=FALSE)
- if(!D)
+/// Qdel Item: Holds statistics on each type that passes thru qdel
+/datum/qdel_item
+ 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.
+ var/list/extra_details //!Lazylist of string metadata about the deleted objects
+
+/datum/qdel_item/New(mytype)
+ name = "[mytype]"
+
+/// Should be treated as a replacement for the 'del' keyword.
+///
+/// Datums passed to this will be given a chance to clean up references to allow the GC to collect them.
+/proc/qdel(datum/to_delete, force = FALSE)
+ if(!istype(to_delete))
+ del(to_delete)
return
-#ifdef REFERENCE_TRACKING
- SSgarbage.qdel_list += "[D.type]"
-#endif
- if(!istype(D))
- del(D)
- else if(isnull(D.gcDestroyed))
- if (SEND_SIGNAL(D, COMSIG_PARENT_PREQDELETED, force)) // Give the components a chance to prevent their parent from being deleted
+
+ var/datum/qdel_item/trash = SSgarbage.items[to_delete.type]
+ if (isnull(trash))
+ trash = SSgarbage.items[to_delete.type] = new /datum/qdel_item(to_delete.type)
+ trash.qdels++
+
+ if(!isnull(to_delete.gc_destroyed))
+ if(to_delete.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
+ CRASH("[to_delete.type] destroy proc was called multiple times, likely due to a qdel loop in the Destroy logic")
+ return
+
+ if (SEND_SIGNAL(to_delete, COMSIG_PREQDELETED, force)) // Give the components a chance to prevent their parent from being deleted
+ return
+
+ to_delete.gc_destroyed = GC_CURRENTLY_BEING_QDELETED
+ var/start_time = world.time
+ var/start_tick = world.tick_usage
+ SEND_SIGNAL(to_delete, COMSIG_QDELETING, force) // Let the (remaining) components know about the result of Destroy
+ var/hint = to_delete.Destroy(force) // Let our friend know they're about to get fucked up.
+
+ if(world.time != start_time)
+ trash.slept_destroy++
+ else
+ trash.destroy_time += TICK_USAGE_TO_MS(start_tick)
+
+ if(isnull(to_delete))
+ return
+
+ switch(hint)
+ if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
+ SSgarbage.Queue(to_delete)
+ if (QDEL_HINT_IWILLGC)
+ to_delete.gc_destroyed = world.time
return
- // this SEND_SIGNAL should be above Destroy because Destroy sets signal_enabled to FALSE
- SEND_SIGNAL(D, COMSIG_QDELETING, force) // Let the (remaining) components know about the result of Destroy
- D.gcDestroyed = GC_CURRENTLY_BEING_QDELETED
- var/start_time = world.time
- var/hint = D.Destroy(force) // Let our friend know they're about to get fucked up.
- if(world.time != start_time)
- SSgarbage.sleptDestroy["[D.type]"]++
- if(!D)
- return
- switch(hint)
- if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
- SSgarbage.QueueForQueuing(D)
- if (QDEL_HINT_IWILLGC)
- D.gcDestroyed = world.time
+ if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
+ if(!force)
+ to_delete.gc_destroyed = null //clear the gc variable (important!)
return
- if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
- if(!force)
- D.gcDestroyed = null //clear the gc variable (important!)
- return
- // Returning LETMELIVE after being told to force destroy
- // indicates the objects Destroy() does not respect force
- if(!SSgarbage.noforcerespect["[D.type]"])
- SSgarbage.noforcerespect["[D.type]"] = "[D.type]"
- log_subsystem_garbage_warning("WARNING: [D.type] has been force deleted, but is \
- returning an immortal QDEL_HINT, indicating it does \
- not respect the force flag for qdel(). It has been \
- placed in the queue, further instances of this type \
- will also be queued.")
- SSgarbage.QueueForQueuing(D)
- if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete using a hard reference to save time from the locate()
- SSgarbage.HardQueue(D)
- if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
- SSgarbage.HardDelete(D)
- if (QDEL_HINT_FINDREFERENCE)//qdel will, if REFERENCE_TRACKING is enabled, display all references to this object, then queue the object for deletion.
- SSgarbage.QueueForQueuing(D)
- #ifdef REFERENCE_TRACKING
- INVOKE_ASYNC(D, TYPE_PROC_REF(/datum, find_references), TRUE)
- #endif
- 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.QueueForQueuing(D)
- #ifdef REFERENCE_TRACKING
- SSgarbage.reference_find_on_fail[text_ref(D)] = TRUE
- #endif
- else
- if(!SSgarbage.noqdelhint["[D.type]"])
- SSgarbage.noqdelhint["[D.type]"] = "[D.type]"
- log_subsystem_garbage_warning("WARNING: [D.type] is not returning a qdel hint. It is being placed in the queue. Further instances of this type will also be queued.")
- SSgarbage.QueueForQueuing(D)
- else if(D.gcDestroyed == GC_CURRENTLY_BEING_QDELETED)
- CRASH("[D.type] destroy proc was called multiple times, likely due to a qdel loop in the Destroy logic")
-
-/client/Destroy()
- ..()
- return QDEL_HINT_HARDDEL_NOW
-
-
-// REFERENCE TRACKING (the old garbage-debug file) //
-// Only present if the appropriate define is set //
-#ifdef REFERENCE_TRACKING
-
-/datum/verb/find_refs()
- set category = "Debug"
- set name = "Find References"
- set background = 1
- set src in world
-
- find_references(FALSE)
-
-/client/verb/show_qdeleted()
- set category = "Debug"
- set name = "Show qdel() Log"
- set desc = "Render the qdel() log and display it"
-
- var/dat = "List of things that have been qdel()eted this round
"
-
- var/tmplist = list()
- for(var/elem in SSgarbage.qdel_list)
- if(!(elem in tmplist))
- tmplist[elem] = 0
- tmplist[elem]++
-
- sortTim(tmplist, GLOBAL_PROC_REF(cmp_numeric_dsc), TRUE)
-
- for(var/path in tmplist)
- dat += "[path] - [tmplist[path]] times
"
-
- usr << browse(dat, "window=qdeletedlog")
-
-/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].")
- usr.client.running_find_references = null
- running_find_references = null
- SSgarbage.can_fire = TRUE
- SSgarbage.update_nextfire(reset_time = TRUE)
- return
-
- if(!skip_alert && alert(usr, "Running this will lock everything up for 5+ minutes. Would you like to begin the search?", "Find References", "Yes", "No") != "Yes")
- running_find_references = null
- return
-
- SSgarbage.can_fire = FALSE // Keeps the GC from failing to collect objects being searched for here
-
- if(usr?.client)
- usr.client.running_find_references = type
-
- //Time to search the whole game for our ref
- testing("Beginning search for references to a [type].")
- var/starting_time = world.time
-
- //Yes we do actually need to do this. The searcher refuses to read weird lists
- //And global.vars is a really weird list
- var/global_vars = list()
- for(var/key in global.vars)
- global_vars[key] = global.vars[key]
-
- search_var(global_vars, "Native Global", search_time = starting_time)
- testing("Finished searching native globals")
-
- for(var/datum/thing in world) // atoms (don't believe its lies)
- search_var(thing, "World -> [thing.type]", search_time = starting_time)
- testing("Finished searching atoms")
-
- for(var/datum/thing) // datums
- search_var(thing, "Datums -> [thing.type]", search_time = starting_time)
- testing("Finished searching datums")
-
- //Warning, attempting to search clients like this will cause crashes if done on live. Watch yourself
- for(var/client/thing) // clients
- search_var(thing, "Clients -> [thing.type]", search_time = starting_time)
- testing("Finished searching clients")
-
- testing("Completed all searches for references to a [type].")
-
- if(usr?.client)
- usr.client.running_find_references = null
- running_find_references = null
-
- SSgarbage.can_fire = TRUE //restart the garbage collector
- SSgarbage.update_nextfire(reset_time = TRUE) //restart the garbage collector
-
-/datum/proc/search_var(potential_container, container_name, recursive_limit = 64, search_time = world.time)
- //If we are performing a search without a check tick, we should avoid sleeping
- #if defined(FIND_REF_NO_CHECK_TICK)
- SHOULD_NOT_SLEEP(TRUE)
- #endif
-
- #ifdef REFERENCE_TRACKING_DEBUG
- if(SSgarbage.should_save_refs && !found_refs)
- found_refs = list()
- #endif
-
- if(usr?.client && !usr.client.running_find_references)
- return
-
- if(!recursive_limit)
- testing("Recursion limit reached. [container_name]")
- return
-
- //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(isdatum(potential_container))
- 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
+ // Returning LETMELIVE after being told to force destroy
+ // indicates the objects Destroy() does not respect force
+ #ifdef TESTING
+ if(!trash.no_respect_force)
+ testing("WARNING: [to_delete.type] has been force deleted, but is \
+ returning an immortal QDEL_HINT, indicating it does \
+ not respect the force flag for qdel(). It has been \
+ placed in the queue, further instances of this type \
+ will also be queued.")
#endif
- if (varname == "vars" || varname == "vis_locs") //Fun fact, vis_locs don't count for references
- continue
- var/variable = vars_list[varname]
+ trash.no_respect_force++
- if(variable == src)
- #ifdef REFERENCE_TRACKING_DEBUG
- if(SSgarbage.should_save_refs)
- found_refs[varname] = TRUE
- continue //End early, don't want these logging
- #endif
- testing("Found [type] [text_ref(src)] in [datum_container.type]'s [text_ref(datum_container)] [varname] var. [container_name]")
- continue
-
- if(islist(variable))
- search_var(variable, "[container_name] [text_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
+ SSgarbage.Queue(to_delete)
+ if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete
+ SSgarbage.Queue(to_delete, GC_QUEUE_HARDDELETE)
+ if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
+ SSgarbage.HardDelete(to_delete)
+ #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(to_delete)
+ INVOKE_ASYNC(to_delete, TYPE_PROC_REF(/datum, 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(to_delete)
+ SSgarbage.reference_find_on_fail[text_ref(to_delete)] = TRUE
+ #endif
+ else
+ #ifdef TESTING
+ if(!trash.no_hint)
+ testing("WARNING: [to_delete.type] is not returning a qdel hint. It is being placed in the queue. Further instances of this type will also be queued.")
#endif
- // Check normal entries
- if(element_in_list == src)
- #ifdef REFERENCE_TRACKING_DEBUG
- if(SSgarbage.should_save_refs)
- found_refs[potential_cache] = TRUE
- continue //End early, don't want these logging
- #endif
- testing("Found [type] [text_ref(src)] in list [container_name]\[[element_in_list]\]")
- continue
-
- var/assoc_val = null
- if(!isnum(element_in_list) && normal)
- assoc_val = potential_cache[element_in_list]
- // Check assoc entries
- if(assoc_val == src)
- #ifdef REFERENCE_TRACKING_DEBUG
- if(SSgarbage.should_save_refs)
- found_refs[potential_cache] = TRUE
- continue //End early, don't want these logging
- #endif
- testing("Found [type] [text_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))
- search_var(element_in_list, "[container_name] -> [element_in_list] (list)", recursive_limit - 1, search_time)
- // Check assoc sublists
- if(islist(assoc_val))
- search_var(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_qdel, force = FALSE)
- thing_to_qdel.qdel_and_find_ref_if_fail(force)
-
-/datum/proc/qdel_and_find_ref_if_fail(force = FALSE)
- SSgarbage.reference_find_on_fail[text_ref(src)] = TRUE
- qdel(src, force)
-
-#endif
+ trash.no_hint++
+ SSgarbage.Queue(to_delete)
diff --git a/code/controllers/subsystems/radio.dm b/code/controllers/subsystems/radio.dm
index b5d664472d7..5940e18a1ee 100644
--- a/code/controllers/subsystems/radio.dm
+++ b/code/controllers/subsystems/radio.dm
@@ -108,6 +108,8 @@ SUBSYSTEM_DEF(radio)
return ..()
/datum/controller/subsystem/radio/proc/add_object(obj/device, new_frequency, filter = null)
+ SHOULD_NOT_SLEEP(TRUE)
+
var/f_text = num2text(new_frequency)
var/datum/radio_frequency/frequency = frequencies[f_text]
@@ -120,6 +122,8 @@ SUBSYSTEM_DEF(radio)
return frequency
/datum/controller/subsystem/radio/proc/remove_object(obj/device, old_frequency)
+ SHOULD_NOT_SLEEP(TRUE)
+
var/f_text = num2text(old_frequency)
var/datum/radio_frequency/frequency = frequencies[f_text]
@@ -127,10 +131,14 @@ SUBSYSTEM_DEF(radio)
frequency.remove_listener(device)
/datum/controller/subsystem/radio/proc/remove_object_all(obj/device)
+ SHOULD_NOT_SLEEP(TRUE)
+
for(var/freq in frequencies)
SSradio.remove_object(device, text2num(freq))
/datum/controller/subsystem/radio/proc/get_devices(freq, filter = RADIO_DEFAULT)
+ SHOULD_NOT_SLEEP(TRUE)
+
var/datum/radio_frequency/frequency = frequencies[num2text(freq)]
if(!frequency)
return
@@ -150,6 +158,8 @@ SUBSYSTEM_DEF(radio)
// Used to test connectivity to the telecomms network.
/datum/controller/subsystem/radio/proc/telecomms_ping(obj/O, test_freq = PUB_FREQ)
+ SHOULD_NOT_SLEEP(TRUE)
+
var/datum/signal/subspace/testsig = new(O, test_freq)
for (var/obj/machinery/telecomms/R in SSmachinery.all_receivers)
if(R.receive_range(testsig) >= 0)
@@ -159,6 +169,8 @@ SUBSYSTEM_DEF(radio)
//callback used by objects to react to incoming radio signals
/obj/proc/receive_signal(datum/signal/signal, receive_method, receive_param)
+ SHOULD_NOT_SLEEP(TRUE)
+
return null
/proc/frequency_span_class(var/frequency)
diff --git a/code/controllers/subsystems/spatial_gridmap.dm b/code/controllers/subsystems/spatial_gridmap.dm
index 15b3bcf012e..00a101c62ff 100644
--- a/code/controllers/subsystems/spatial_gridmap.dm
+++ b/code/controllers/subsystems/spatial_gridmap.dm
@@ -131,7 +131,7 @@ SUBSYSTEM_DEF(spatial_grid)
if(movable_turf)
enter_cell(movable, movable_turf)
- UnregisterSignal(movable, COMSIG_PARENT_PREQDELETED)
+ UnregisterSignal(movable, COMSIG_PREQDELETED)
waiting_to_add_by_type[channel_type] -= movable
pregenerate_more_oranges_ears(NUMBER_OF_PREGENERATED_ORANGES_EARS)
@@ -143,7 +143,7 @@ SUBSYSTEM_DEF(spatial_grid)
///add a movable to the pre init queue for whichever type is specified so that when the subsystem initializes they get added to the grid
/datum/controller/subsystem/spatial_grid/proc/enter_pre_init_queue(atom/movable/waiting_movable, type)
- RegisterSignal(waiting_movable, COMSIG_PARENT_PREQDELETED, PROC_REF(queued_item_deleted), override = TRUE)
+ RegisterSignal(waiting_movable, COMSIG_PREQDELETED, PROC_REF(queued_item_deleted), override = TRUE)
//override because something can enter the queue for two different types but that is done through unrelated procs that shouldnt know about eachother
waiting_to_add_by_type[type] += waiting_movable
@@ -158,11 +158,11 @@ SUBSYSTEM_DEF(spatial_grid)
waiting_movable_is_in_other_queues = TRUE
if(!waiting_movable_is_in_other_queues)
- UnregisterSignal(movable_to_remove, COMSIG_PARENT_PREQDELETED)
+ UnregisterSignal(movable_to_remove, COMSIG_PREQDELETED)
return
- UnregisterSignal(movable_to_remove, COMSIG_PARENT_PREQDELETED)
+ UnregisterSignal(movable_to_remove, COMSIG_PREQDELETED)
for(var/type in waiting_to_add_by_type)
waiting_to_add_by_type[type] -= movable_to_remove
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 35ecdd5e24c..eb0e9e9a28e 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -1,15 +1,14 @@
/datum
- var/tmp/list/active_timers
- var/tmp/datum/weakref/weakref
- var/tmp/isprocessing = 0
-
/**
* Tick count time when this object was destroyed.
*
* If this is non zero then the object has been garbage collected and is awaiting either
* a hard del by the GC subsystme, or to be autocollected (if it has no references)
*/
- var/tmp/gcDestroyed
+ var/gc_destroyed
+
+ var/tmp/list/active_timers
+ var/tmp/isprocessing = 0
/// Status traits attached to this datum. associative list of the form: list(trait name (string) = list(source1, source2, source3,...))
var/list/status_traits
@@ -24,10 +23,9 @@
/// Is this datum capable of sending signals?
/// Set to true when a signal has been registered
var/signal_enabled = FALSE
- /// A cached version of our \ref
- /// The brunt of \ref costs are in creating entries in the string tree (a tree of immutable strings)
- /// This avoids doing that more then once per datum by ensuring ref strings always have a reference to them after they're first pulled
- var/cached_ref
+
+ /// A weak reference to another datum
+ var/datum/weakref/weak_reference
#ifdef REFERENCE_TRACKING
var/running_find_references
@@ -38,29 +36,29 @@
#endif
#endif
+ // If we have called dump_harddel_info already. Used to avoid duped calls (since we call it immediately in some cases on failure to process)
+ // Create and destroy is weird and I wanna cover my bases
+ var/harddel_deets_dumped = FALSE
+
// Default implementation of clean-up code.
// This should be overridden to remove all references pointing to the object being destroyed.
// Return the appropriate QDEL_HINT; in most cases this is QDEL_HINT_QUEUE.
/datum/proc/Destroy(force=FALSE)
SHOULD_CALL_PARENT(TRUE)
+ //SHOULD_NOT_SLEEP(TRUE) //Soon my friend, soon...
- weakref = null
- GLOB.destroyed_event.raise_event(src)
- var/ui_key = SOFTREF(src)
- if(LAZYISIN(SSnanoui.open_uis, ui_key))
- SSnanoui.close_uis(src)
tag = null
- var/list/timers = active_timers
- active_timers = null
- if (timers)
- for (var/thing in timers)
- var/datum/timedevent/timer = thing
- if (timer.spent)
- continue
- qdel(timer)
+ weak_reference = null //ensure prompt GCing of weakref.
- // Handle components & signals
- signal_enabled = FALSE
+ if(active_timers)
+ var/list/timers = active_timers
+ active_timers = null
+ if (timers)
+ for (var/thing in timers)
+ var/datum/timedevent/timer = thing
+ if (timer.spent)
+ continue
+ qdel(timer)
#ifdef REFERENCE_TRACKING
#ifdef REFERENCE_TRACKING_DEBUG
@@ -68,6 +66,19 @@
#endif
#endif
+ GLOB.destroyed_event.raise_event(src)
+ if (!isturf(src))
+ cleanup_events(src)
+
+ var/ui_key = SOFTREF(src)
+ if(LAZYISIN(SSnanoui.open_uis, ui_key))
+ SSnanoui.close_uis(src)
+
+
+ // Handle components & signals
+ signal_enabled = FALSE
+
+ //BEGIN: ECS SHIT
var/list/dc = datum_components
if(dc)
var/all_components = dc[/datum/component]
@@ -95,6 +106,7 @@
for(var/target in signal_procs)
UnregisterSignal(target, signal_procs[target])
+ //END: ECS SHIT
return QDEL_HINT_QUEUE
@@ -125,3 +137,17 @@
return FALSE
vars[var_name] = var_value
return TRUE
+
+
+/// Return text from this proc to provide extra context to hard deletes that happen to it
+/// Optional, you should use this for cases where replication is difficult and extra context is required
+/// Can be called more then once per object, use harddel_deets_dumped to avoid duplicate calls (I am so sorry)
+/datum/proc/dump_harddel_info()
+ return
+
+///images are pretty generic, this should help a bit with tracking harddels related to them
+/image/dump_harddel_info()
+ if(harddel_deets_dumped)
+ return
+ harddel_deets_dumped = TRUE
+ return "Image icon: [icon] - icon_state: [icon_state] [loc ? "loc: [loc] ([loc.x],[loc.y],[loc.z])" : ""]"
diff --git a/code/datums/helper_datums/construction_datum.dm b/code/datums/helper_datums/construction_datum.dm
index 08e5e55efab..25204fad7de 100644
--- a/code/datums/helper_datums/construction_datum.dm
+++ b/code/datums/helper_datums/construction_datum.dm
@@ -2,17 +2,22 @@
var/list/steps
var/atom/holder
var/result
- var/list/steps_desc
var/current_desc = null
/datum/construction/New(atom)
..()
holder = atom
if(!holder) //don't want this without a holder
- spawn
- qdel(src)
+ qdel(src)
set_desc(steps.len)
+/datum/construction/Destroy(force)
+ holder = null
+
+ steps = null
+
+ . = ..()
+
/datum/construction/proc/next_step()
steps.len--
if(!steps.len)
@@ -60,8 +65,7 @@
/datum/construction/proc/spawn_result()
if(result)
new result(get_turf(holder))
- spawn()
- qdel(holder)
+ QDEL_NULL(holder)
/datum/construction/proc/set_desc(index as num)
var/list/step = steps[index]
diff --git a/code/datums/observation/_debug.dm b/code/datums/observation/_debug.dm
index 02b4a31e66f..b45a4fbcfb2 100644
--- a/code/datums/observation/_debug.dm
+++ b/code/datums/observation/_debug.dm
@@ -1,4 +1 @@
-/****************
-* Debug Support *
-****************/
GLOBAL_LIST_EMPTY(all_observable_events)
diff --git a/code/datums/observation/~cleanup.dm b/code/datums/observation/~cleanup.dm
new file mode 100644
index 00000000000..01fc7192d3b
--- /dev/null
+++ b/code/datums/observation/~cleanup.dm
@@ -0,0 +1,71 @@
+GLOBAL_LIST_EMPTY(global_listen_count)
+GLOBAL_LIST_EMPTY(event_sources_count)
+GLOBAL_LIST_EMPTY(event_listen_count)
+
+/proc/cleanup_events(source)
+ if(GLOB.global_listen_count && GLOB.global_listen_count[source])
+ cleanup_global_listener(source, GLOB.global_listen_count[source])
+ if(GLOB.event_sources_count && GLOB.event_sources_count[source])
+ cleanup_source_listeners(source, GLOB.event_sources_count[source])
+ if(GLOB.event_listen_count && GLOB.event_listen_count[source])
+ cleanup_event_listener(source, GLOB.event_listen_count[source])
+
+/singleton/observ/register(datum/event_source, datum/listener, proc_call)
+ . = ..()
+ if(.)
+ GLOB.event_sources_count[event_source] += 1
+ GLOB.event_listen_count[listener] += 1
+
+/singleton/observ/unregister(datum/event_source, datum/listener, proc_call)
+ . = ..()
+ if(.)
+ GLOB.event_sources_count[event_source] -= 1
+ GLOB.event_listen_count[listener] -= 1
+
+ if(GLOB.event_sources_count[event_source] <= 0)
+ GLOB.event_sources_count -= event_source
+ if(GLOB.event_listen_count[listener] <= 0)
+ GLOB.event_listen_count -= listener
+
+/singleton/observ/register_global(datum/listener, proc_call)
+ . = ..()
+ if(.)
+ GLOB.global_listen_count[listener] += 1
+
+/singleton/observ/unregister_global(datum/listener, proc_call)
+ . = ..()
+ if(.)
+ GLOB.global_listen_count[listener] -= 1
+ if(GLOB.global_listen_count[listener] <= 0)
+ GLOB.global_listen_count -= listener
+
+/proc/cleanup_global_listener(listener, listen_count)
+ GLOB.global_listen_count -= listener
+ for(var/entry in GLOB.all_observable_events)
+ var/singleton/observ/event = entry
+ if(event.unregister_global(listener))
+ log_debug("[event] - [listener] was deleted while still registered to global events.")
+ if(!(--listen_count))
+ return
+
+/proc/cleanup_source_listeners(event_source, source_listener_count)
+ GLOB.event_sources_count -= event_source
+ for(var/entry in GLOB.all_observable_events)
+ var/singleton/observ/event = entry
+ var/proc_owners = event.event_sources[event_source]
+ if(proc_owners)
+ for(var/proc_owner in proc_owners)
+ if(event.unregister(event_source, proc_owner))
+ log_debug("[event] - [event_source] was deleted while still being listened to by [proc_owner].")
+ if(!(--source_listener_count))
+ return
+
+/proc/cleanup_event_listener(listener, listener_count)
+ GLOB.event_listen_count -= listener
+ for(var/entry in GLOB.all_observable_events)
+ var/singleton/observ/event = entry
+ for(var/event_source in event.event_sources)
+ if(event.unregister(event_source, listener))
+ log_debug("[event] - [listener] was deleted while still listening to [event_source].")
+ if(!(--listener_count))
+ return
diff --git a/code/datums/weakref.dm b/code/datums/weakref.dm
deleted file mode 100644
index a60ee9f1d07..00000000000
--- a/code/datums/weakref.dm
+++ /dev/null
@@ -1,17 +0,0 @@
-/datum/weakref
- var/ref
-
-/datum/weakref/New(datum/D)
- ref = SOFTREF(D)
-
-/datum/weakref/Destroy(force = 0)
- crash_with("Some fuck is trying to [force ? "force-" : ""]delete a weakref!")
- if (!force)
- return QDEL_HINT_LETMELIVE // feck off
-
- return ..()
-
-/datum/weakref/proc/resolve()
- var/datum/D = locate(ref)
- if (!QDELETED(D) && D.weakref == src)
- . = D
diff --git a/code/datums/weakrefs.dm b/code/datums/weakrefs.dm
new file mode 100644
index 00000000000..911623ff4c2
--- /dev/null
+++ b/code/datums/weakrefs.dm
@@ -0,0 +1,95 @@
+/// Creates a weakref to the given input.
+/// See /datum/weakref's documentation for more information.
+/proc/WEAKREF(datum/input)
+ if(istype(input) && !QDELETED(input))
+ if(isweakref(input))
+ return input
+
+ if(!input.weak_reference)
+ input.weak_reference = new /datum/weakref(input)
+ return input.weak_reference
+
+/datum/proc/create_weakref() //Forced creation for admin proccalls
+ return WEAKREF(src)
+
+/**
+ * A weakref holds a non-owning reference to a datum.
+ * The datum can be referenced again using `resolve()`.
+ *
+ * To figure out why this is important, you must understand how deletion in
+ * BYOND works.
+ *
+ * Imagine a datum as a TV in a living room. When one person enters to watch
+ * TV, they turn it on. Others can come into the room and watch the TV.
+ * When the last person leaves the room, they turn off the TV because it's
+ * no longer being used.
+ *
+ * A datum being deleted tells everyone who's watching the TV to stop.
+ * If everyone leaves properly (AKA cleaning up their references), then the
+ * last person will turn off the TV, and everything is well.
+ * However, if someone is resistant (holds a hard reference after deletion),
+ * then someone has to walk in, drag them away, and turn off the TV forecefully.
+ * This process is very slow, and it's known as hard deletion.
+ *
+ * This is where weak references come in. Weak references don't count as someone
+ * watching the TV. Thus, when what it's referencing is destroyed, it will
+ * hopefully clean up properly, and limit hard deletions.
+ *
+ * A common use case for weak references is holding onto what created itself.
+ * For example, if a machine wanted to know what its last user was, it might
+ * create a `var/mob/living/last_user`. However, this is a strong reference to
+ * the mob, and thus will force a hard deletion when that mob is deleted.
+ * It is often better in this case to instead create a weakref to the user,
+ * meaning this type definition becomes `var/datum/weakref/last_user`.
+ *
+ * A good rule of thumb is that you should hold strong references to things
+ * that you *own*. For example, a dog holding a chew toy would be the owner
+ * of that chew toy, and thus a `var/obj/item/chew_toy` reference is fine
+ * (as long as it is cleaned up properly).
+ * However, a chew toy does not own its dog, so a `var/mob/living/dog/owner`
+ * might be inferior to a weakref.
+ * This is also a good rule of thumb to avoid circular references, such as the
+ * chew toy example. A circular reference that doesn't clean itself up properly
+ * will always hard delete.
+ */
+/datum/weakref
+ var/reference
+
+/datum/weakref/New(datum/thing)
+ reference = text_ref(thing)
+
+/datum/weakref/Destroy(force)
+ var/datum/target = resolve()
+ qdel(target)
+
+ if(!force)
+ return QDEL_HINT_LETMELIVE //Let BYOND autoGC thiswhen nothing is using it anymore.
+ target?.weak_reference = null
+ return ..()
+
+/**
+ * Retrieves the datum that this weakref is referencing.
+ *
+ * This will return `null` if the datum was deleted. This MUST be respected.
+ */
+/datum/weakref/proc/resolve()
+ var/datum/D = locate(reference)
+ return (!QDELETED(D) && D.weak_reference == src) ? D : null
+
+/**
+ * SERIOUSLY READ THE AUTODOC COMMENT FOR THIS PROC BEFORE EVEN THINKING ABOUT USING IT
+ *
+ * Like resolve, but doesn't care if the datum is being qdeleted but hasn't been deleted yet.
+ *
+ * The return value of this proc leaves hanging references if the datum is being qdeleted but hasn't been deleted yet.
+ *
+ * Do not do anything that would create a lasting reference to the return value, such as giving it a tag, putting it on the map,
+ * adding it to an atom's contents or vis_contents, giving it a key (if it's a mob), attaching it to an atom (if it's an image),
+ * or assigning it to a datum or list referenced somewhere other than a temporary value.
+ *
+ * Unless you're resolving a weakref to a datum in a COMSIG_QDELETING signal handler registered on that very same datum,
+ * just use resolve instead.
+ */
+/datum/weakref/proc/hard_resolve()
+ var/datum/D = locate(reference)
+ return (D?.weak_reference == src) ? D : null
diff --git a/code/game/atom/atoms_initializing_EXPENSIVE.dm b/code/game/atom/atoms_initializing_EXPENSIVE.dm
index 7189c7c670e..1e960128759 100644
--- a/code/game/atom/atoms_initializing_EXPENSIVE.dm
+++ b/code/game/atom/atoms_initializing_EXPENSIVE.dm
@@ -4,7 +4,7 @@
if(QDELING(A))
// Check init_start_time to not worry about atoms created before the atoms SS that are cleaned up before this
- if (A.gcDestroyed > init_start_time)
+ if (A.gc_destroyed > init_start_time)
BadInitializeCalls[the_type] |= BAD_INIT_QDEL_BEFORE
return TRUE
diff --git a/code/game/dna/dna_modifier.dm b/code/game/dna/dna_modifier.dm
index 79b6b90f839..6f959a9375c 100644
--- a/code/game/dna/dna_modifier.dm
+++ b/code/game/dna/dna_modifier.dm
@@ -222,6 +222,29 @@
active_power_usage = 400
var/waiting_for_user_input=0 // Fix for #274 (Mash create block injector without answering dialog to make unlimited injectors) - N3X
+/obj/machinery/computer/scan_consolenew/Initialize()
+ ..()
+ for(var/i=0;i<3;i++)
+ buffers[i+1]=new /datum/dna2/record
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/computer/scan_consolenew/LateInitialize()
+ . = ..()
+ for(dir in list(NORTH,EAST,SOUTH,WEST))
+ connected = locate(/obj/machinery/dna_scannernew, get_step(src, dir))
+ if(!isnull(connected))
+ break
+
+ src.injector_ready = 1
+
+/obj/machinery/computer/scan_consolenew/Destroy()
+ connected = null
+ disk = null
+
+ QDEL_LIST_ASSOC(buffers)
+
+ . = ..()
+
/obj/machinery/computer/scan_consolenew/attackby(obj/item/I as obj, mob/user as mob)
if (istype(I, /obj/item/disk/data)) //INSERT SOME diskS
if (!src.disk)
@@ -234,31 +257,14 @@
return ..()
/obj/machinery/computer/scan_consolenew/ex_act(severity)
-
switch(severity)
if(1.0)
//SN src = null
qdel(src)
- return
if(2.0)
if (prob(50))
//SN src = null
qdel(src)
- return
- return
-
-/obj/machinery/computer/scan_consolenew/New()
- ..()
- for(var/i=0;i<3;i++)
- buffers[i+1]=new /datum/dna2/record
- spawn(5)
- for(dir in list(NORTH,EAST,SOUTH,WEST))
- connected = locate(/obj/machinery/dna_scannernew, get_step(src, dir))
- if(!isnull(connected))
- break
- spawn(250)
- src.injector_ready = 1
- return
return
/obj/machinery/computer/scan_consolenew/proc/all_dna_blocks(var/list/buffer)
diff --git a/code/game/gamemodes/technomancer/spells/spawner/destablize.dm b/code/game/gamemodes/technomancer/spells/spawner/destablize.dm
index a270897a56e..c44aa5e25aa 100644
--- a/code/game/gamemodes/technomancer/spells/spawner/destablize.dm
+++ b/code/game/gamemodes/technomancer/spells/spawner/destablize.dm
@@ -35,20 +35,31 @@
var/pulses_remaining = 40 // Lasts 20 seconds.
var/instability_power = 5
var/instability_range = 6
+ var/timer_id = null
/obj/effect/temporary_effect/destabilize/Initialize()
+ ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/effect/temporary_effect/destabilize/LateInitialize()
+ . = ..()
+ timer_id = addtimer(CALLBACK(src, PROC_REF(radiate_loop)), 5 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
+
+/obj/effect/temporary_effect/destabilize/Destroy()
+ deltimer(timer_id)
. = ..()
- radiate_loop()
/obj/effect/temporary_effect/destabilize/proc/radiate_loop()
- set waitfor = FALSE
- while(pulses_remaining)
- sleep(5)
+ if(pulses_remaining && !QDELETED(src))
for(var/mob/living/L in range(src, instability_range) )
var/radius = max(get_dist(L, src), 1)
// Being farther away lessens the amount of instability received.
var/outgoing_instability = instability_power * ( 1 / (radius**2) )
L.receive_radiated_instability(outgoing_instability)
pulses_remaining--
- qdel(src)
+ timer_id = addtimer(CALLBACK(src, PROC_REF(radiate_loop)), 5 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
+
+ else
+ qdel(src)
+
diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm
index 30710ea7dd1..c4ce6189d9d 100644
--- a/code/game/jobs/job/job.dm
+++ b/code/game/jobs/job/job.dm
@@ -145,6 +145,8 @@
. = equip(H, TRUE, FALSE, alt_title=alt_title)
/datum/job/proc/get_access(selected_title)
+ SHOULD_NOT_SLEEP(TRUE)
+
if(!GLOB.config || GLOB.config.jobs_have_minimal_access)
. = minimal_access.Copy()
else
diff --git a/code/game/machinery/autolathe/autolathe.dm b/code/game/machinery/autolathe/autolathe.dm
index 711ef592535..b7edb605c12 100644
--- a/code/game/machinery/autolathe/autolathe.dm
+++ b/code/game/machinery/autolathe/autolathe.dm
@@ -14,7 +14,7 @@
clicksound = /singleton/sound_category/keyboard_sound
clickvol = 30
- var/print_loc
+ var/atom/print_loc
var/list/stored_material = list(DEFAULT_WALL_MATERIAL = 0, MATERIAL_GLASS = 0)
var/list/storage_capacity = list(DEFAULT_WALL_MATERIAL = 0, MATERIAL_GLASS = 0)
@@ -62,8 +62,12 @@
populate_lathe_recipes()
/obj/machinery/autolathe/Destroy()
+ print_loc = null
+ QDEL_NULL(currently_printing)
QDEL_NULL(wires)
- print_queue.Cut()
+
+ QDEL_LIST(print_queue)
+
return ..()
/obj/machinery/autolathe/proc/populate_lathe_recipes()
diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm
index d84a16a0f0c..f6faeceb9d1 100644
--- a/code/game/machinery/camera/camera.dm
+++ b/code/game/machinery/camera/camera.dm
@@ -74,7 +74,8 @@
GLOB.cameranet.remove_source(src)
GLOB.cameranet.cameras -= src
- return ..()
+ . = ..()
+ GC_TEMPORARY_HARDDEL
/obj/machinery/camera/set_pixel_offsets()
pixel_x = dir & (NORTH|SOUTH) ? 0 : (dir == EAST ? -13 : 13)
diff --git a/code/game/machinery/computer/slotmachine.dm b/code/game/machinery/computer/slotmachine.dm
index a3928e5371e..761950d9027 100644
--- a/code/game/machinery/computer/slotmachine.dm
+++ b/code/game/machinery/computer/slotmachine.dm
@@ -50,8 +50,6 @@
coinvalues["[cointype]"] = get_value(cointype)
/obj/machinery/computer/slot_machine/Destroy()
- if(balance)
- give_payout(balance)
return ..()
/obj/machinery/computer/slot_machine/process(delta_time)
diff --git a/code/game/machinery/doors/blast_door.dm b/code/game/machinery/doors/blast_door.dm
index 289a33ece3d..cecd3adf95a 100644
--- a/code/game/machinery/doors/blast_door.dm
+++ b/code/game/machinery/doors/blast_door.dm
@@ -48,9 +48,8 @@
else
layer = open_layer
-/obj/machinery/door/airlock/Destroy()
- qdel(wifi_receiver)
- wifi_receiver = null
+/obj/machinery/door/blast/Destroy()
+ QDEL_NULL(wifi_receiver)
return ..()
// Proc: Bumped()
diff --git a/code/game/machinery/doors/multi_tile.dm b/code/game/machinery/doors/multi_tile.dm
index 3e3ead762af..d998cfd40ab 100644
--- a/code/game/machinery/doors/multi_tile.dm
+++ b/code/game/machinery/doors/multi_tile.dm
@@ -35,7 +35,7 @@
LAZYADD(vision_blockers, vision_blocker)
/obj/machinery/door/airlock/multi_tile/Destroy()
- QDEL_NULL_LIST(vision_blockers)
+ QDEL_LIST(vision_blockers)
return ..()
/obj/machinery/door/airlock/multi_tile/set_opacity(var/new_opacity)
@@ -100,7 +100,7 @@
LAZYADD(vision_blockers, vision_blocker)
/obj/machinery/door/firedoor/multi_tile/Destroy()
- QDEL_NULL_LIST(vision_blockers)
+ QDEL_LIST(vision_blockers)
return ..()
/obj/machinery/door/firedoor/multi_tile/set_opacity(var/new_opacity)
diff --git a/code/game/machinery/embedded_controller/airlock_controllers.dm b/code/game/machinery/embedded_controller/airlock_controllers.dm
index b9fbab51797..d1179a14579 100644
--- a/code/game/machinery/embedded_controller/airlock_controllers.dm
+++ b/code/game/machinery/embedded_controller/airlock_controllers.dm
@@ -31,6 +31,10 @@
tag_chamber_sensor = given_tag_chamber_sensor
program = new /datum/computer/file/embedded_program/airlock(src)
+/obj/machinery/embedded_controller/radio/airlock/Destroy()
+ . = ..()
+ GC_TEMPORARY_HARDDEL
+
//Advanced airlock controller for when you want a more versatile airlock controller - useful for turning simple access control rooms into airlocks
/obj/machinery/embedded_controller/radio/airlock/advanced_airlock_controller
name = "Advanced Airlock Controller"
diff --git a/code/game/machinery/embedded_controller/embedded_controller_base.dm b/code/game/machinery/embedded_controller/embedded_controller_base.dm
index 5947e964dd4..7dcea6efd5b 100644
--- a/code/game/machinery/embedded_controller/embedded_controller_base.dm
+++ b/code/game/machinery/embedded_controller/embedded_controller_base.dm
@@ -13,7 +13,8 @@
/obj/machinery/embedded_controller/radio/Destroy()
if(SSradio)
SSradio.remove_object(src,frequency)
- return ..()
+ . = ..()
+ GC_TEMPORARY_HARDDEL
/obj/machinery/embedded_controller/proc/post_signal(datum/signal/signal, comm_line)
return 0
diff --git a/code/game/machinery/machinery.dm b/code/game/machinery/machinery.dm
index c7e7d2b1338..30613523ce5 100644
--- a/code/game/machinery/machinery.dm
+++ b/code/game/machinery/machinery.dm
@@ -155,12 +155,15 @@ Class Procs:
/obj/machinery/Destroy()
STOP_PROCESSING_MACHINE(src, MACHINERY_PROCESS_ALL)
SSmachinery.machinery -= src
+
+ //Clear the component parts
+ //If the components are inside the machine, delete them, otherwise we assume they were dropped to the ground during deconstruction,
+ //and were not removed from the component_parts list by deconstruction code
if(component_parts)
for(var/atom/A in component_parts)
- if(A.loc == src) // If the components are inside the machine, delete them.
+ if(A.loc == src)
qdel(A)
- else // Otherwise we assume they were dropped to the ground during deconstruction, and were not removed from the component_parts list by deconstruction code.
- component_parts -= A
+ component_parts = null
return ..()
@@ -451,9 +454,13 @@ Class Procs:
M.set_dir(src.dir)
M.state = 3
M.icon_state = "blueprint_1"
+
for(var/obj/I in component_parts)
I.forceMove(loc)
+ component_parts -= I
+
qdel(src)
+
return TRUE
/obj/machinery/proc/print(var/obj/paper, var/play_sound = 1, var/print_sfx = /singleton/sound_category/print_sound, var/print_delay = 10, var/message, var/mob/user)
diff --git a/code/game/machinery/telecomms/machines/message_server.dm b/code/game/machinery/telecomms/machines/message_server.dm
index 0b67ef80b49..7c07eb95385 100644
--- a/code/game/machinery/telecomms/machines/message_server.dm
+++ b/code/game/machinery/telecomms/machines/message_server.dm
@@ -215,7 +215,7 @@
+ "