diff --git a/code/__defines/_compile_options.dm b/code/__defines/_compile_options.dm index d2f61e13dca..0c60ba0a5dd 100644 --- a/code/__defines/_compile_options.dm +++ b/code/__defines/_compile_options.dm @@ -1,3 +1,16 @@ // The default value for all uses of set background. Set background can cause gradual lag and is recommended you only turn this on if necessary. // 1 will enable set background. 0 will disable set background. #define BACKGROUND_ENABLED 0 + +// If REFTRACK_IN_CI is defined, the reftracker will run in CI. +#define REFTRACK_IN_CI +#if defined(REFTRACK_IN_CI) && defined(UNIT_TEST) && !defined(SPACEMAN_DMM) +#define REFTRACKING_ENABLED +#define GC_FAILURE_HARD_LOOKUP +#define FIND_REF_NO_CHECK_TICK +#endif + +// parity with previous behavior where TESTING enabled reftracking +#ifdef TESTING +#define REFTRACKING_ENABLED +#endif \ No newline at end of file diff --git a/code/__defines/qdel.dm b/code/__defines/qdel.dm index 48a9f0e0973..fa9cf0a8deb 100644 --- a/code/__defines/qdel.dm +++ b/code/__defines/qdel.dm @@ -5,8 +5,8 @@ #define QDEL_HINT_IWILLGC 2 //functionally the same as the above. qdel should assume the object will gc on its own, and not check it. #define QDEL_HINT_HARDDEL 3 //qdel should assume this object won't gc, and queue a hard delete using a hard reference. #define QDEL_HINT_HARDDEL_NOW 4 //qdel should assume this object won't gc, and hard del it post haste. -#define QDEL_HINT_FINDREFERENCE 5 //functionally identical to QDEL_HINT_QUEUE if TESTING is not enabled in _compiler_options.dm. - //if TESTING is enabled, qdel will call this object's find_references() verb. +#define QDEL_HINT_FINDREFERENCE 5 //functionally identical to QDEL_HINT_QUEUE if REFTRACKING_ENABLED is not enabled in _compiler_options.dm. + //if REFTRACKING_ENABLED is enabled, qdel will call this object's find_references() verb. #define QDEL_HINT_IFFAIL_FINDREFERENCE 6 //Above but only if gc fails. //defines for the gc_destroyed var @@ -15,6 +15,12 @@ #define GC_QUEUE_HARDDELETE 3 #define GC_QUEUE_COUNT 3 //increase this when adding more steps. +// 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 + #define GC_QUEUED_FOR_HARD_DEL -1 #define GC_CURRENTLY_BEING_QDELETED -2 diff --git a/code/controllers/subsystems/garbage.dm b/code/controllers/subsystems/garbage.dm index 47c1be9e245..18f92570395 100644 --- a/code/controllers/subsystems/garbage.dm +++ b/code/controllers/subsystems/garbage.dm @@ -37,7 +37,7 @@ SUBSYSTEM_DEF(garbage) //Queue var/list/queues - #ifdef TESTING + #ifdef REFTRACKING_ENABLED var/list/reference_find_on_fail = list() #endif @@ -130,30 +130,33 @@ SUBSYSTEM_DEF(garbage) lastlevel = level +// 1 from the hard reference in the queue, and 1 from `D` in the code below +#define REFS_WE_EXPECT 2 + //We do this rather then for(var/refID 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/refidx in 1 to length(queue)) - var/refID = queue[refidx] - if (isnull(refID)) + 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 > cut_off_time) + var/queued_at_time = L[GC_QUEUE_ITEM_QUEUE_TIME] + if(queued_at_time > cut_off_time) break // Everything else is newer, skip them count++ - var/datum/D - D = locate(refID) + var/datum/D = L[GC_QUEUE_ITEM_REF] - if (isnull(D) || D.gc_destroyed != GCd_at_time) // So if something else coincidently gets the same ref, it's not deleted by mistake + // If that's all we've got, send er off + if (refcount(D) == REFS_WE_EXPECT) ++gcedlasttick ++totalgcs pass_counts[level]++ - #ifdef TESTING - reference_find_on_fail -= refID //It's deleted we don't care anymore. + #ifdef REFTRACKING_ENABLED + reference_find_on_fail -= ref(D) //It's deleted we don't care anymore. #endif if (MC_TICK_CHECK) return @@ -161,14 +164,19 @@ SUBSYSTEM_DEF(garbage) // Something's still referring to the qdel'd object. fail_counts[level]++ + switch (level) if (GC_QUEUE_CHECK) - #ifdef TESTING + #ifdef REFTRACKING_ENABLED + // Decides how many refs to look for (potentially) + // Based off the remaining and the ones we can account for + var/remaining_refs = refcount(D) - REFS_WE_EXPECT + var/refID = ref(D) if(reference_find_on_fail[refID]) - D.find_references() + D.find_references(remaining_refs) #ifdef GC_FAILURE_HARD_LOOKUP else - D.find_references() + D.find_references(remaining_refs) #endif reference_find_on_fail -= refID #endif @@ -201,12 +209,14 @@ SUBSYSTEM_DEF(garbage) if (level > GC_QUEUE_COUNT) HardDelete(D) return - var/gctime = world.time - var/refid = "\ref[D]" + var/queue_time = world.time - D.gc_destroyed = gctime + if(D.gc_destroyed <= 0) // hasn't been queued yet, or is queued for harddel/actively being qdeleted + D.gc_destroyed = queue_time var/list/queue = queues[level] - queue[refid] = gctime + // not += for byond reasons + // we include D.gc_destroyed to skip things under the cutoff + queue[++queue.len] = list(queue_time, D, D.gc_destroyed) //this is mainly to separate things profile wise. /datum/controller/subsystem/garbage/proc/HardDelete(datum/D) @@ -240,11 +250,6 @@ SUBSYSTEM_DEF(garbage) message_admins("Error: [type]([refID]) took longer than 1 second to delete (took [time/10] seconds to delete).") postpone(time) -/datum/controller/subsystem/garbage/proc/HardQueue(datum/D) - if (D.gc_destroyed == GC_CURRENTLY_BEING_QDELETED) - queues[GC_QUEUE_FILTER] += D - D.gc_destroyed = GC_QUEUED_FOR_HARD_DEL - /datum/controller/subsystem/garbage/Recover() if (istype(SSgarbage.queues)) for (var/i in 1 to SSgarbage.queues.len) @@ -270,7 +275,7 @@ SUBSYSTEM_DEF(garbage) /datum/qdel_item/New(mytype) name = "[mytype]" -#ifdef TESTING +#ifdef REFTRACKING_ENABLED /proc/qdel_and_find_ref_if_fail(datum/D, force = FALSE) SSgarbage.reference_find_on_fail["\ref[D]"] = TRUE qdel(D, force) @@ -328,7 +333,7 @@ SUBSYSTEM_DEF(garbage) return // Returning LETMELIVE after being told to force destroy // indicates the objects Destroy() does not respect force - #ifdef TESTING + #ifdef REFTRACKING_ENABLED if(!I.no_respect_force) PRINT_STACK_TRACE("WARNING: [D.type] has been force deleted, but is \ returning an immortal QDEL_HINT, indicating it does \ @@ -341,21 +346,22 @@ SUBSYSTEM_DEF(garbage) SSgarbage.Queue(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() GC_CHECK_AM_NULLSPACE(D, "QDEL_HINT_HARDDEL") - SSgarbage.HardQueue(D) + 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. SSgarbage.HardDelete(D) - if (QDEL_HINT_FINDREFERENCE)//qdel will, if TESTING is enabled, display all references to this object, then queue the object for deletion. + if (QDEL_HINT_FINDREFERENCE)//qdel will, if REFTRACKING_ENABLED is enabled, display all references to this object, then queue the object for deletion. SSgarbage.Queue(D) - #ifdef TESTING - D.find_references() + #ifdef REFTRACKING_ENABLED + var/remaining_refs = refcount(D) - REFS_WE_EXPECT + D.find_references(remaining_refs) #endif if (QDEL_HINT_IFFAIL_FINDREFERENCE) SSgarbage.Queue(D) - #ifdef TESTING + #ifdef REFTRACKING_ENABLED SSgarbage.reference_find_on_fail["\ref[D]"] = TRUE #endif else - #ifdef TESTING + #ifdef REFTRACKING_ENABLED if(!I.no_hint) PRINT_STACK_TRACE("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.") #endif @@ -364,7 +370,7 @@ SUBSYSTEM_DEF(garbage) 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 +#ifdef REFTRACKING_ENABLED /datum/verb/find_refs() set category = "Debug" @@ -378,8 +384,9 @@ SUBSYSTEM_DEF(garbage) return find_references() -/datum/proc/find_references() +/datum/proc/find_references(references_to_clear = INFINITY) running_find_references = type + src.references_to_clear = references_to_clear if(usr && usr.client) if(usr.client.running_find_references) testing("CANCELLED search for references to a [usr.client.running_find_references].") @@ -406,13 +413,19 @@ SUBSYSTEM_DEF(garbage) normal_globals[global_var] = global.vars[global_var] DoSearchVar(normal_globals, "(global) -> ") //globals testing("Finished searching globals") + if(src.references_to_clear == 0) // Found all expected references! + return for(var/atom/atom_thing) //atoms DoSearchVar(atom_thing, "World -> [atom_thing]") + if(src.references_to_clear == 0) // Found all expected references! + return testing("Finished searching atoms") for (var/datum/datum_thing) //datums DoSearchVar(datum_thing, "World -> [datum_thing]") + if(src.references_to_clear == 0) // Found all expected references! + return testing("Finished searching datums") #ifndef FIND_REF_SKIP_CLIENTS @@ -420,6 +433,8 @@ SUBSYSTEM_DEF(garbage) // IT WILL CRASH!!! for (var/client/client_thing) //clients DoSearchVar(client_thing, "World -> [client_thing]") + if(src.references_to_clear == 0) // Found all expected references! + return testing("Finished searching clients") #endif @@ -455,37 +470,47 @@ SUBSYSTEM_DEF(garbage) #define GET_TYPEID(ref) ( ( (length(ref) <= 10) ? "TYPEID_NULL" : copytext(ref, 4, length(ref)-6) ) ) #define IS_NORMAL_LIST(L) (GET_TYPEID("\ref[L]") == TYPEID_NORMAL_LIST) -/datum/proc/DoSearchVar(X, Xname, recursive_limit = 128) +/datum/proc/DoSearchVar(X, container_name, recursive_limit = 128) if(usr && usr.client && !usr.client.running_find_references) return if (!recursive_limit) return + if(references_to_clear == 0) + return #ifndef FIND_REF_NO_CHECK_TICK CHECK_TICK #endif if(istype(X, /datum)) - var/datum/D = X - if(D.last_find_references == last_find_references) + var/datum/datum_container = X + if(datum_container.last_find_references == last_find_references) return - D.last_find_references = last_find_references - var/list/L = D.vars + datum_container.last_find_references = last_find_references + var/list/vars_list = datum_container.vars - for(var/varname in L) + var/is_atom = FALSE + var/is_area = FALSE + if(isatom(datum_container)) + is_atom = TRUE + if(isarea(datum_container)) + is_area = TRUE + for(var/varname in vars_list) #ifndef FIND_REF_NO_CHECK_TICK CHECK_TICK #endif - if (varname == "vars") + //Fun fact, vis_locs don't count for references + if(varname == "vars" || (is_atom && (varname == "vis_locs" || varname == "overlays" || varname == "underlays" || varname == "filters" || varname == "verbs" || (is_area && varname == "contents")))) continue - var/variable = L[varname] + var/variable = vars_list[varname] if(variable == src) - testing("Found [src.type] \ref[src] in [D.type]'s [varname] var. [Xname]") + testing("Found [src.type] \ref[src] in [datum_container.type]'s [varname] var. [container_name]") + references_to_clear -= 1 else if(islist(variable)) - DoSearchVar(variable, "[Xname] -> [varname] (list)", recursive_limit-1) + DoSearchVar(variable, "[container_name] -> [varname] (list)", recursive_limit-1) else if(islist(X)) var/normal = IS_NORMAL_LIST(X) @@ -494,16 +519,33 @@ SUBSYSTEM_DEF(garbage) CHECK_TICK #endif if (I == src) - testing("Found [src.type] \ref[src] in list [Xname].") + testing("Found [src.type] \ref[src] in list [container_name].") + + // This is dumb as hell I'm sorry + // I don't want the garbage subsystem to count as a ref for the purposes of this number + // If we find all other refs before it I want to early exit, and if we don't I want to keep searching past it + var/ignore_ref = FALSE + var/list/queues = SSgarbage.queues + for(var/list/queue in queues) + if(X in queue) + ignore_ref = TRUE + break + if(ignore_ref) + testing("[container_name] does not count as a ref for our count") + else + references_to_clear -= 1 + if(references_to_clear == 0) + testing("All references to [type] \ref[src] found, exiting.") + return else if (I && !isnum(I) && normal) if(X[I] == src) - testing("Found [src.type] \ref[src] in list [Xname]\[[I]\]") + testing("Found [src.type] \ref[src] in list [container_name]\[[I]\]") else if(islist(X[I])) - DoSearchVar(X[I], "[Xname]\[[I]\]", recursive_limit-1) + DoSearchVar(X[I], "[container_name]\[[I]\]", recursive_limit-1) else if (islist(I)) var/list/Xlist = X - DoSearchVar(I, "[Xname]\[[Xlist.Find(I)]\] -> list", recursive_limit-1) + DoSearchVar(I, "[container_name]\[[Xlist.Find(I)]\] -> list", recursive_limit-1) #endif diff --git a/code/controllers/subsystems/processing/nano.dm b/code/controllers/subsystems/processing/nano.dm index dfaae9e7999..dd358012c92 100644 --- a/code/controllers/subsystems/processing/nano.dm +++ b/code/controllers/subsystems/processing/nano.dm @@ -37,11 +37,10 @@ PROCESSING_SUBSYSTEM_DEF(nano) * @return /nanoui Returns the found ui, or null if none exists */ /datum/controller/subsystem/processing/nano/proc/get_open_ui(mob/user, src_object, ui_key) - var/src_object_key = "\ref[src_object]" - if (!open_uis[src_object_key] || !open_uis[src_object_key][ui_key]) + if (!open_uis[src_object] || !open_uis[src_object][ui_key]) return - for (var/datum/nanoui/ui in open_uis[src_object_key][ui_key]) + for (var/datum/nanoui/ui in open_uis[src_object][ui_key]) if (ui.user == user) return ui @@ -54,12 +53,11 @@ PROCESSING_SUBSYSTEM_DEF(nano) */ /datum/controller/subsystem/processing/nano/proc/update_uis(src_object) . = 0 - var/src_object_key = "\ref[src_object]" - if (!open_uis[src_object_key]) + if (!open_uis[src_object]) return - for (var/ui_key in open_uis[src_object_key]) - for (var/datum/nanoui/ui in open_uis[src_object_key][ui_key]) + for (var/ui_key in open_uis[src_object]) + for (var/datum/nanoui/ui in open_uis[src_object][ui_key]) if(ui.src_object && ui.user && ui.src_object.nano_host()) ui.try_update(1) .++ @@ -78,12 +76,11 @@ PROCESSING_SUBSYSTEM_DEF(nano) if (!length(open_uis)) return - var/src_object_key = "\ref[src_object]" - if (!open_uis[src_object_key]) + if (!open_uis[src_object]) return - for (var/ui_key in open_uis[src_object_key]) - for (var/datum/nanoui/ui in open_uis[src_object_key][ui_key]) + for (var/ui_key in open_uis[src_object]) + for (var/datum/nanoui/ui in open_uis[src_object][ui_key]) ui.close() // If it's missing src_object or user, we want to close it even more. .++ @@ -134,9 +131,9 @@ PROCESSING_SUBSYSTEM_DEF(nano) * @return nothing */ /datum/controller/subsystem/processing/nano/proc/ui_opened(datum/nanoui/ui) - var/src_object_key = "\ref[ui.src_object]" - LAZYINITLIST(open_uis[src_object_key]) - LAZYDISTINCTADD(open_uis[src_object_key][ui.ui_key], ui) + var/src_object = ui.src_object + LAZYINITLIST(open_uis[src_object]) + LAZYDISTINCTADD(open_uis[src_object][ui.ui_key], ui) LAZYDISTINCTADD(ui.user.open_uis, ui) START_PROCESSING(SSnano, ui) @@ -149,18 +146,18 @@ PROCESSING_SUBSYSTEM_DEF(nano) * @return int 0 if no ui was removed, 1 if removed successfully */ /datum/controller/subsystem/processing/nano/proc/ui_closed(var/datum/nanoui/ui) - var/src_object_key = "\ref[ui.src_object]" - if (!open_uis[src_object_key] || !open_uis[src_object_key][ui.ui_key]) + var/src_object = ui.src_object + if (!open_uis[src_object] || !open_uis[src_object][ui.ui_key]) return 0 // wasn't open STOP_PROCESSING(SSnano, ui) if(ui.user) // Sanity check in case a user has been deleted (say a blown up borg watching the alarm interface) LAZYREMOVE(ui.user.open_uis, ui) - open_uis[src_object_key][ui.ui_key] -= ui - if(!length(open_uis[src_object_key][ui.ui_key])) - open_uis[src_object_key] -= ui.ui_key - if(!length(open_uis[src_object_key])) - open_uis -= src_object_key + open_uis[src_object][ui.ui_key] -= ui + if(!length(open_uis[src_object][ui.ui_key])) + open_uis[src_object] -= ui.ui_key + if(!length(open_uis[src_object])) + open_uis -= src_object return 1 /** diff --git a/code/datums/datum.dm b/code/datums/datum.dm index a0aeed07808..aa855c0b1e3 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -10,9 +10,14 @@ /// Used to avoid unnecessary refstring creation in Destroy(). var/tmp/has_state_machine = FALSE -#ifdef TESTING +#ifdef REFTRACKING_ENABLED var/tmp/running_find_references + /// When was this datum last touched by a reftracker? + /// If this value doesn't match with the start of the search + /// We know this datum has never been seen before, and we should check it var/tmp/last_find_references = 0 + /// How many references we're trying to find when searching + var/tmp/references_to_clear = 0 #endif // Default implementation of clean-up code. @@ -54,11 +59,11 @@ cleanup_events(src) if(has_state_machine) - var/list/machines = global.state_machines["\ref[src]"] + var/list/machines = global.state_machines[src] if(length(machines)) for(var/base_type in machines) qdel(machines[base_type]) - global.state_machines -= "\ref[src]" + global.state_machines -= src return QDEL_HINT_QUEUE diff --git a/code/datums/extensions/state_machine.dm b/code/datums/extensions/state_machine.dm index 63e58f0dd67..edba84c7965 100644 --- a/code/datums/extensions/state_machine.dm +++ b/code/datums/extensions/state_machine.dm @@ -3,16 +3,15 @@ var/global/list/state_machines = list() /proc/get_state_machine(var/datum/holder, var/base_type) if(istype(holder) && base_type && holder.has_state_machine) - var/list/machines = global.state_machines["\ref[holder]"] + var/list/machines = global.state_machines[holder] return islist(machines) && machines[base_type] /proc/add_state_machine(var/datum/holder, var/datum/state_machine/fsm_type) if(istype(holder) && fsm_type) - var/holder_ref = "\ref[holder]" - var/list/machines = global.state_machines[holder_ref] + var/list/machines = global.state_machines[holder] if(!islist(machines)) machines = list() - global.state_machines[holder_ref] = machines + global.state_machines[holder] = machines var/base_type = fsm_type::base_type if(!machines[base_type]) var/datum/state_machine/machine = new fsm_type(holder) @@ -22,12 +21,11 @@ var/global/list/state_machines = list() /proc/remove_state_machine(var/datum/holder, var/base_type) if(istype(holder) && base_type && holder.has_state_machine) - var/holder_ref = "\ref[holder]" - var/list/machines = global.state_machines[holder_ref] + var/list/machines = global.state_machines[holder] if(length(machines)) machines -= base_type if(!length(machines)) - global.state_machines -= holder_ref + global.state_machines -= holder holder.has_state_machine = FALSE return TRUE return FALSE diff --git a/code/datums/extensions/storage/_storage.dm b/code/datums/extensions/storage/_storage.dm index c79e91a7357..4ff70967e1c 100644 --- a/code/datums/extensions/storage/_storage.dm +++ b/code/datums/extensions/storage/_storage.dm @@ -252,6 +252,8 @@ var/global/list/_test_storage_items = list() storage_ui?.on_insertion() /datum/storage/proc/update_ui_after_item_removal(obj/item/removed) + if(QDELETED(holder)) + return prepare_ui() storage_ui?.on_post_remove() diff --git a/code/datums/extensions/storage/_storage_ui.dm b/code/datums/extensions/storage/_storage_ui.dm index 6f4db924f32..a3e1f23bfcb 100644 --- a/code/datums/extensions/storage/_storage_ui.dm +++ b/code/datums/extensions/storage/_storage_ui.dm @@ -216,7 +216,8 @@ closer.screen_loc = "LEFT+[SCREEN_LOC_MOD_FIRST + cols + 1]:[SCREEN_LOC_MOD_DIVIDED],BOTTOM+[SCREEN_LOC_MOD_SECOND]:[SCREEN_LOC_MOD_DIVIDED]" /datum/storage_ui/default/proc/space_orient_objs() - + if(QDELETED(_storage?.holder)) // don't bother if we've been deleted + return var/baseline_max_storage_space = DEFAULT_BOX_STORAGE //storage size corresponding to 224 pixels var/storage_cap_width = 2 //length of sprite for start and end of the box representing total storage space var/stored_cap_width = 4 //length of sprite for start and end of the box representing the stored item diff --git a/code/datums/graph/graph.dm b/code/datums/graph/graph.dm index 249f513e419..1610a6b4397 100644 --- a/code/datums/graph/graph.dm +++ b/code/datums/graph/graph.dm @@ -35,6 +35,8 @@ /datum/graph/proc/Connect(var/datum/node/node, var/list/neighbours, var/queue = TRUE) SHOULD_NOT_SLEEP(TRUE) SHOULD_NOT_OVERRIDE(TRUE) + if(QDELETED(src)) + CRASH("Attempted to connect node [node] to a qdeleted graph!") if(!istype(neighbours)) neighbours = list(neighbours) if(!length(neighbours)) @@ -90,8 +92,7 @@ if(neighbours_to_disconnect) neighbours |= neighbours_to_disconnect - if(neighbours.len) - LAZYSET(pending_disconnections, node, neighbours) + LAZYSET(pending_disconnections, node, neighbours) if(queue) SSgraphs_update.Queue(src) @@ -193,8 +194,6 @@ neighbour_edges |= N LAZYCLEARLIST(pending_connections) - if(!LAZYLEN(pending_disconnections)) - return for(var/pending_node_disconnect in pending_disconnections) var/pending_edge_disconnects = pending_disconnections[pending_node_disconnect] @@ -211,7 +210,8 @@ if(!length(other_pending_edge_disconnects)) pending_disconnections -= connected_node - edges[pending_node_disconnect] -= pending_edge_disconnects + if(edges[pending_node_disconnect]) + edges[pending_node_disconnect] -= pending_edge_disconnects if(!length(edges[pending_node_disconnect])) edges -= pending_node_disconnect @@ -225,12 +225,15 @@ var/checked_nodes = list() var/list/nodes_to_traverse = list(root_node) while(length(nodes_to_traverse)) - var/node_to_check = nodes_to_traverse[nodes_to_traverse.len] + var/datum/node/node_to_check = nodes_to_traverse[nodes_to_traverse.len] nodes_to_traverse.len-- + if(QDELETED(node_to_check)) + continue checked_nodes += node_to_check nodes_to_traverse |= ((edges[node_to_check] || list()) - checked_nodes) - all_nodes -= checked_nodes - subgraphs[++subgraphs.len] = checked_nodes + if(length(checked_nodes)) + all_nodes -= checked_nodes + subgraphs[++subgraphs.len] = checked_nodes if(length(subgraphs) == 1) if(!length(nodes)) diff --git a/code/game/alpha_masks.dm b/code/game/alpha_masks.dm index 05b23f45eaa..9c8a341a482 100644 --- a/code/game/alpha_masks.dm +++ b/code/game/alpha_masks.dm @@ -57,7 +57,7 @@ var/global/list/_alpha_masks = list() // Proc called by /turf/Entered() to update a mob's mask overlay. /atom/movable/proc/update_turf_alpha_mask() set waitfor = FALSE - if(!simulated || updating_turf_alpha_mask) + if(!simulated || QDELETED(src) || updating_turf_alpha_mask) return updating_turf_alpha_mask = TRUE sleep(0) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 5dd985c6064..d13760956e3 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -145,10 +145,12 @@ /atom/proc/try_on_reagent_change() SHOULD_NOT_OVERRIDE(TRUE) set waitfor = FALSE - if(_reagent_update_started >= world.time) + if(QDELETED(src) ||_reagent_update_started >= world.time) return FALSE _reagent_update_started = world.time sleep(0) // Defer to end of tick so we don't drop subsequent reagent updates. + if(QDELETED(src)) + return return on_reagent_change() /atom/proc/on_reagent_change() diff --git a/code/game/atoms_init.dm b/code/game/atoms_init.dm index 3a2f3b619c1..d1cd533c796 100644 --- a/code/game/atoms_init.dm +++ b/code/game/atoms_init.dm @@ -80,7 +80,7 @@ /atom/Destroy() // must be done before deletion // TODO: ADD PRE_DELETION OBSERVATION - if(isatom(loc) && loc.storage) + if(isatom(loc) && loc.storage && !QDELETED(loc.storage)) loc.storage.on_item_pre_deletion(src) UNQUEUE_TEMPERATURE_ATOM(src) QDEL_NULL(reagents) @@ -94,7 +94,7 @@ QDEL_NULL(atom_codex_ref) var/atom/oldloc = loc . = ..() - if(isatom(oldloc) && oldloc.storage) + if(isatom(oldloc) && oldloc.storage && !QDELETED(loc.storage)) oldloc.storage.on_item_post_deletion(src) // must be done after deletion // This might need to be moved onto a Del() override at some point. QDEL_NULL(storage) diff --git a/code/game/machinery/vending/_vending.dm b/code/game/machinery/vending/_vending.dm index 866062f2a54..b3036e17317 100644 --- a/code/game/machinery/vending/_vending.dm +++ b/code/game/machinery/vending/_vending.dm @@ -309,10 +309,8 @@ if (href_list["vend"] && !currently_vending) var/key = text2num(href_list["vend"]) - if(!is_valid_index(key, product_records)) - return TOPIC_REFRESH - var/datum/stored_items/vending_products/R = product_records[key] - if(!istype(R)) + var/datum/stored_items/vending_products/R = LAZYACCESS(product_records, key) + if(!R) return TOPIC_REFRESH // This should not happen unless the request from NanoUI was bad diff --git a/code/game/objects/auras/aura.dm b/code/game/objects/auras/aura.dm index 8d1d35a0680..5de18f3adf6 100644 --- a/code/game/objects/auras/aura.dm +++ b/code/game/objects/auras/aura.dm @@ -15,6 +15,7 @@ They should also be used for when you want to effect the ENTIRE mob, like having /obj/aura/Destroy() if(user) user.remove_aura(src) + removed() return ..() /obj/aura/proc/added_to(var/mob/living/target) diff --git a/code/game/objects/item_mob_overlay.dm b/code/game/objects/item_mob_overlay.dm index d6881129c23..fe253ea6f0b 100644 --- a/code/game/objects/item_mob_overlay.dm +++ b/code/game/objects/item_mob_overlay.dm @@ -8,13 +8,14 @@ var/global/list/bodypart_to_slot_lookup_table = list( ) /obj/item/proc/reconsider_single_icon(var/update_icon) - use_single_icon = check_state_in_icon(ICON_STATE_INV, icon) || check_state_in_icon(ICON_STATE_WORLD, icon) + var/list/icon_states = get_states_in_icon_cached(icon) // pre-cache to make our up-to-three checks faster + // except now we only do two because i rewrote it + has_inventory_icon = use_single_icon = icon_states[ICON_STATE_INV] + if(!has_inventory_icon) + use_single_icon = icon_states[ICON_STATE_WORLD] if(use_single_icon) - has_inventory_icon = check_state_in_icon(ICON_STATE_INV, icon) icon_state = get_world_inventory_state() . = TRUE - else - has_inventory_icon = FALSE if(. || update_icon) update_icon() @@ -27,6 +28,23 @@ var/global/list/icon_state_cache = list() // isicon() is apparently quite expensive so short-circuit out early if we can. if(!istext(checkstate) || isnull(checkicon) || !(isfile(checkicon) || isicon(checkicon))) return FALSE + var/list/check = _fetch_icon_state_cache_entry(checkicon) // should never return null once we reach this point + return check[checkstate] + +/// A proc for getting an associative list of icon states in an icon. +/// Uses the same cache as check_state_in_icon. +/// Does not copy, MUST NOT BE MUTATED. +/proc/get_states_in_icon_cached(checkicon) /* as OD_MAP(text, OD_BOOL) */ + return _fetch_icon_state_cache_entry(checkicon) || list() + +/// get_states_in_icon_cached but it does a copy, so the return value can be mutated. +/proc/get_states_in_icon(checkicon) /* as OD_MAP(text, OD_BOOL) */ + var/list/out = get_states_in_icon_cached(checkicon) + return out.Copy() + +/proc/_fetch_icon_state_cache_entry(checkicon) + if(isnull(checkicon) || !(isfile(checkicon) || isicon(checkicon))) + return null var/checkkey = "\ref[checkicon]" var/list/check = global.icon_state_cache[checkkey] if(!check) @@ -34,7 +52,7 @@ var/global/list/icon_state_cache = list() for(var/istate in icon_states(checkicon)) check[istate] = TRUE global.icon_state_cache[checkkey] = check - . = check[checkstate] + return check /obj/item/proc/update_world_inventory_state() if(use_single_icon && has_inventory_icon) diff --git a/code/game/objects/items/blueprints.dm b/code/game/objects/items/blueprints.dm index c9c67e53fc7..5a082bb9180 100644 --- a/code/game/objects/items/blueprints.dm +++ b/code/game/objects/items/blueprints.dm @@ -59,7 +59,7 @@ return FALSE name += " - [S.name]" - desc = "Blueprints of \the [S.name]. There is a \"Classified\" stamp and several coffee stains on it." + desc = "Blueprints of \the [S]. There is a \"Classified\" stamp and several coffee stains on it." valid_z_levels += S.map_z area_prefix = S.name return TRUE diff --git a/code/game/objects/items/devices/lightreplacer.dm b/code/game/objects/items/devices/lightreplacer.dm index 3565259a08e..34115f49b63 100644 --- a/code/game/objects/items/devices/lightreplacer.dm +++ b/code/game/objects/items/devices/lightreplacer.dm @@ -110,7 +110,7 @@ if(!user.try_unequip(L)) return TRUE AddUses(1) - to_chat(user, "You insert \the [L.name] into \the [src]. You have [uses] light\s remaining.") + to_chat(user, "You insert \the [L] into \the [src]. You have [uses] light\s remaining.") qdel(L) return TRUE else diff --git a/code/game/objects/items/devices/paint_sprayer.dm b/code/game/objects/items/devices/paint_sprayer.dm index a9dbd8a0878..88c1e1195d7 100644 --- a/code/game/objects/items/devices/paint_sprayer.dm +++ b/code/game/objects/items/devices/paint_sprayer.dm @@ -223,7 +223,7 @@ return FALSE if(!flooring.can_paint || F.is_floor_damaged()) - to_chat(user, SPAN_WARNING("\The [src] cannot paint \the [F.name].")) + to_chat(user, SPAN_WARNING("\The [src] cannot paint \the [F].")) return FALSE var/list/decal_data = decals[decal] diff --git a/code/game/objects/items/devices/transfer_valve.dm b/code/game/objects/items/devices/transfer_valve.dm index b83f35bca48..2f2506b6aa0 100644 --- a/code/game/objects/items/devices/transfer_valve.dm +++ b/code/game/objects/items/devices/transfer_valve.dm @@ -7,11 +7,27 @@ var/obj/item/tank/tank_one var/obj/item/tank/tank_two var/obj/item/assembly/attached_device - var/mob/attacher = null + var/weakref/attacher_ref = null var/valve_open = 0 var/toggle = 1 movable_flags = MOVABLE_FLAG_PROXMOVE +/obj/item/transfer_valve/Destroy() + if(!QDELETED(tank_one)) + QDEL_NULL(tank_one) + else + tank_one = null + if(!QDELETED(tank_two)) + QDEL_NULL(tank_two) + else + tank_two = null + if(!QDELETED(attached_device)) + QDEL_NULL(attached_device) + else + attached_device = null + attacher_ref = null + return ..() + /obj/item/transfer_valve/attackby(obj/item/item, mob/user) var/turf/location = get_turf(src) // For admin logs if(istype(item, /obj/item/tank)) @@ -56,7 +72,7 @@ global.bombers += "[key_name(user)] attached a [item] to a transfer valve." message_admins("[key_name_admin(user)] attached a [item] to a transfer valve. (JMP)") log_game("[key_name_admin(user)] attached a [item] to a transfer valve.") - attacher = user + attacher_ref = weakref(user) . = TRUE if(.) update_icon() @@ -189,6 +205,7 @@ var/area/A = get_area(bombturf) var/attacher_name = "" + var/mob/attacher = attacher_ref.resolve() if(!attacher) attacher_name = "Unknown" else diff --git a/code/game/objects/items/weapons/ecigs.dm b/code/game/objects/items/weapons/ecigs.dm index dc05320c090..0a55a70a950 100644 --- a/code/game/objects/items/weapons/ecigs.dm +++ b/code/game/objects/items/weapons/ecigs.dm @@ -22,6 +22,10 @@ ec_cartridge = new cartridge_type(src) . = ..() +/obj/item/clothing/mask/smokable/ecig/Destroy() + QDEL_NULL(ec_cartridge) + return ..() + /obj/item/clothing/mask/smokable/ecig/setup_power_supply(loaded_cell_type, accepted_cell_type, power_supply_extension_type, charge_value) loaded_cell_type = loaded_cell_type || /obj/item/cell/device/standard accepted_cell_type = accepted_cell_type || /obj/item/cell/device diff --git a/code/game/objects/items/weapons/soap.dm b/code/game/objects/items/weapons/soap.dm index c14047c71f6..7667753288a 100644 --- a/code/game/objects/items/weapons/soap.dm +++ b/code/game/objects/items/weapons/soap.dm @@ -68,13 +68,13 @@ //So this is a workaround. This also makes more sense from an IC standpoint. ~Carn var/cleaned = FALSE if(user.client && (target in user.client.screen)) - to_chat(user, SPAN_NOTICE("You need to take that [target.name] off before cleaning it.")) + to_chat(user, SPAN_NOTICE("You need to take \the [target] off before cleaning it.")) else if(istype(target,/obj/effect/decal/cleanable/blood)) - to_chat(user, SPAN_NOTICE("You scrub \the [target.name] out.")) + to_chat(user, SPAN_NOTICE("You scrub \the [target] out.")) target.clean() //Blood is a cleanable decal, therefore needs to be accounted for before all cleanable decals. cleaned = TRUE else if(istype(target,/obj/effect/decal/cleanable)) - to_chat(user, SPAN_NOTICE("You scrub \the [target.name] out.")) + to_chat(user, SPAN_NOTICE("You scrub \the [target] out.")) qdel(target) cleaned = TRUE else if(isturf(target) || istype(target, /obj/structure/catwalk)) @@ -90,7 +90,7 @@ to_chat(user, SPAN_NOTICE("You wet \the [src] in the sink.")) wet() else - to_chat(user, SPAN_NOTICE("You clean \the [target.name].")) + to_chat(user, SPAN_NOTICE("You clean \the [target].")) target.clean() //Clean bloodied atoms. Blood decals themselves need to be handled above. cleaned = TRUE diff --git a/code/game/objects/structures/bookcase.dm b/code/game/objects/structures/bookcase.dm index 06d58e357aa..944bcd3fe0f 100644 --- a/code/game/objects/structures/bookcase.dm +++ b/code/game/objects/structures/bookcase.dm @@ -168,7 +168,7 @@ var/global/list/station_bookcases = list() add_overlay(book_overlay) var/page_state = "[book_overlay.icon_state]-pages" - if(check_state_in_icon(book_overlay.icon, page_state)) + if(check_state_in_icon(page_state, book_overlay.icon)) var/image/page_overlay = overlay_image(book_overlay.icon, page_state, COLOR_WHITE, RESET_COLOR) page_overlay.pixel_x = book_overlay.pixel_x page_overlay.pixel_y = book_overlay.pixel_y diff --git a/code/game/objects/structures/stool_bed_chair_nest_sofa/wheelchair.dm b/code/game/objects/structures/stool_bed_chair_nest_sofa/wheelchair.dm index caa3e14df93..67640a583fd 100644 --- a/code/game/objects/structures/stool_bed_chair_nest_sofa/wheelchair.dm +++ b/code/game/objects/structures/stool_bed_chair_nest_sofa/wheelchair.dm @@ -141,7 +141,7 @@ user.visible_message("[user] starts to lay out \the [src].") if(do_after(user, 4 SECONDS, src)) var/obj/structure/bed/chair/wheelchair/W = new structure_form_type(get_turf(user)) - user.visible_message(SPAN_NOTICE("[user] lays out \the [W.name].")) + user.visible_message(SPAN_NOTICE("[user] lays out \the [W].")) W.add_fingerprint(user) qdel(src) diff --git a/code/modules/assembly/assembly.dm b/code/modules/assembly/assembly.dm index a4cf742c410..47dcc913a45 100644 --- a/code/modules/assembly/assembly.dm +++ b/code/modules/assembly/assembly.dm @@ -12,7 +12,7 @@ var/secured = 1 var/list/attached_overlays = null - var/obj/item/assembly_holder/holder = null + var/obj/item/assembly_holder/holder = null // currently can be a TTV or assemblyholder, todo make ttv use assemblyholder var/cooldown = 0//To prevent spam var/wires = WIRE_RECEIVE | WIRE_PULSE @@ -24,10 +24,7 @@ /obj/item/assembly/Destroy() if(!QDELETED(holder)) - if(holder.a_left == src) - holder.a_left = null - if(holder.a_right == src) - holder.a_right = null + // the holder has the responsibility to clear its associated vars on destroy QDEL_NULL(holder) else holder = null diff --git a/code/modules/client/ui_styles/_ui_style.dm b/code/modules/client/ui_styles/_ui_style.dm index 5e0a5ba34c4..1a5885cfac6 100644 --- a/code/modules/client/ui_styles/_ui_style.dm +++ b/code/modules/client/ui_styles/_ui_style.dm @@ -46,7 +46,7 @@ var/check_icon = icons[ui_key] var/list/missing_states = list() var/list/checking_states = states_to_check[ui_key] - var/list/remaining_states = icon_states(check_icon) + var/list/remaining_states = get_states_in_icon(check_icon) for(var/check_state in checking_states) remaining_states -= check_state if(!check_state_in_icon(check_state, check_icon)) diff --git a/code/modules/clothing/masks/voice.dm b/code/modules/clothing/masks/voice.dm index edee4acd8eb..8ecc47d727c 100644 --- a/code/modules/clothing/masks/voice.dm +++ b/code/modules/clothing/masks/voice.dm @@ -30,3 +30,7 @@ /obj/item/clothing/mask/chameleon/voice/Initialize() . = ..() changer = new(src) + +/obj/item/clothing/mask/chameleon/voice/Destroy() + QDEL_NULL(changer) + return ..() diff --git a/code/modules/economy/worth_currency.dm b/code/modules/economy/worth_currency.dm index ed12b938e92..9e5de940b12 100644 --- a/code/modules/economy/worth_currency.dm +++ b/code/modules/economy/worth_currency.dm @@ -70,13 +70,13 @@ if(!name_singular) . += "No singular name set." - var/list/coinage_states = icon_states(icon) + var/list/coinage_states = get_states_in_icon_cached(icon) // cache this to avoid excessive ref() usage for(var/datum/denomination/denomination in denominations) if(!istext(denomination.name)) . += "Non-text name found for '[denomination.type]'." else if(!(denomination.state in coinage_states)) . += "State '[denomination.state]' not found in icon file for '[denomination.type]'." - else if(denomination.mark && !(denomination.mark in coinage_states)) + else if(denomination.mark && !coinage_states[denomination.mark]) . += "Mark state '[denomination.mark]' not found in icon file for '[denomination.type]'." else if(!isnum(denomination.marked_value)) . += "Non-numerical denomination marked value found for '[denomination]'." diff --git a/code/modules/hydroponics/seed_storage.dm b/code/modules/hydroponics/seed_storage.dm index 2d609cf629e..840d453ce50 100644 --- a/code/modules/hydroponics/seed_storage.dm +++ b/code/modules/hydroponics/seed_storage.dm @@ -3,14 +3,12 @@ var/amount var/datum/seed/seed_type // Keeps track of what our seed is var/list/obj/item/seeds/seeds = list() // Tracks actual objects contained in the pile - var/ID -/datum/seed_pile/New(var/obj/item/seeds/O, var/ID) +/datum/seed_pile/New(var/obj/item/seeds/O) name = O.name amount = 1 seed_type = O.seed seeds += O - src.ID = ID /datum/seed_pile/proc/matches(var/obj/item/seeds/O) if (O.seed == seed_type) @@ -195,7 +193,8 @@ if ("soil" in scanner) dat += "