Ports TG's Garbage Subsystem

This commit is contained in:
Fox McCloud
2018-03-28 20:02:39 -04:00
parent 067f5cf8e8
commit 22dc6860b3
74 changed files with 646 additions and 486 deletions
+2 -2
View File
@@ -158,7 +158,7 @@ Pipelines + Other Objects -> Pipe network
P.other_atmosmch -= src
//(De)construction
/obj/machinery/atmospherics/attackby(var/obj/item/weapon/W as obj, var/mob/user as mob)
/obj/machinery/atmospherics/attackby(obj/item/weapon/W, mob/user)
if(can_unwrench && istype(W, /obj/item/weapon/wrench))
var/turf/T = get_turf(src)
if(level == 1 && isturf(T) && T.intact)
@@ -179,7 +179,7 @@ Pipelines + Other Objects -> Pipe network
to_chat(user, "<span class='warning'>As you begin unwrenching \the [src] a gush of air blows in your face... maybe you should reconsider?</span>")
unsafe_wrenching = TRUE //Oh dear oh dear
if(do_after(user, 40 * W.toolspeed, target = src) && isnull(gcDestroyed))
if(do_after(user, 40 * W.toolspeed, target = src) && !QDELETED(src))
user.visible_message( \
"[user] unfastens \the [src].", \
"<span class='notice'>You have unfastened \the [src].</span>", \
+1 -1
View File
@@ -32,7 +32,7 @@
. = ..()
// if we're somehow by ourself
if(parent && isnull(parent.gcDestroyed) && parent.members.len == 1 && parent.members[1] == src)
if(parent && !QDELETED(parent) && parent.members.len == 1 && parent.members[1] == src)
qdel(parent)
parent = null
+22 -2
View File
@@ -1,11 +1,31 @@
//defines that give qdel hints. these can be given as a return in destory() or by calling
#define QDEL_HINT_QUEUE 0 //qdel should queue the object for deletion.
#define QDEL_HINT_LETMELIVE 1 //qdel should let the object live after calling destory.
#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.
//defines for the gc_destroyed var
// A convenient wrapper around checking for qdeletion/non-existence
#define exists(A) (A && !qdeleted(A))
#define GC_QUEUE_PREQUEUE 1
#define GC_QUEUE_CHECK 2
#define GC_QUEUE_HARDDELETE 3
#define GC_QUEUE_COUNT 3 //increase this when adding more steps.
#define GC_QUEUED_FOR_QUEUING -1
#define GC_QUEUED_FOR_HARD_DEL -2
#define GC_CURRENTLY_BEING_QDELETED -3
#define QDELING(X) (X.gc_destroyed)
#define QDELETED(X) (!X || QDELING(X))
#define QDESTROYING(X) (!X || X.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
/proc/check_datum_qdeleted(datum/D) //for checking if something is a datum, first, then checking gc_destroyed; get rid of this eventually. TO-DO
if(!istype(D))
return FALSE
if(D.gc_destroyed)
return TRUE
return FALSE
+9
View File
@@ -31,6 +31,15 @@
/proc/cmp_subsystem_priority(datum/controller/subsystem/a, datum/controller/subsystem/b)
return a.priority - b.priority
/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_atom_layer_asc(atom/A,atom/B)
if(A.plane != B.plane)
return A.plane - B.plane
+1 -1
View File
@@ -109,7 +109,7 @@
return adjacencies
/proc/smooth_icon(atom/A)
if(qdeleted(A))
if(QDELETED(A))
return
if(A && (A.smooth & SMOOTH_TRUE) || (A.smooth & SMOOTH_MORE))
var/adjacencies = calculate_adjacencies(A)
+7 -4
View File
@@ -15,9 +15,12 @@
/proc/warning(msg)
log_to_dd("## WARNING: [msg]")
//print a testing-mode debug message to world.log
/proc/testing(msg)
log_to_dd("## TESTING: [msg]")
//print a testing-mode debug message to world.log and world
#ifdef TESTING
#define testing(msg) log_to_dd("## TESTING: [msg]"); to_chat(world, "## TESTING: [msg]")
#else
#define testing(msg)
#endif
/proc/log_admin(text)
admin_log.Add(text)
@@ -49,7 +52,7 @@
/proc/log_say(text)
if(config.log_say)
diary << "\[[time_stamp()]]SAY: [text]"
/proc/log_robot(text)
if(config.log_say)
diary << "\[[time_stamp()]]ROBOT: [text]"
+10 -2
View File
@@ -1,7 +1,15 @@
// So you can be all 10 SECONDS
#define SECONDS *10
#define MINUTES *600
#define HOURS *36000
#define MINUTES SECONDS*60
#define HOURS MINUTES*60
#define TICKS *world.tick_lag
#define DS2TICKS(DS) ((DS)/world.tick_lag)
#define TICKS2DS(T) ((T) TICKS)
#define TimeOfGame (get_game_time())
#define TimeOfTick (world.tick_usage*0.01*world.tick_lag)
+3 -6
View File
@@ -1482,12 +1482,9 @@ var/mob/dview/dview_mob = new
/mob/dview/New() //For whatever reason, if this isn't called, then BYOND will throw a type mismatch runtime when attempting to add this to the mobs list. -Fox
/proc/IsValidSrc(var/A)
if(istype(A, /datum))
var/datum/B = A
return isnull(B.gcDestroyed)
if(istype(A, /client))
return 1
/proc/IsValidSrc(datum/D)
if(istype(D))
return !QDELETED(D)
return 0
+8
View File
@@ -1,6 +1,14 @@
#define DEBUG
//#define TESTING
#ifdef TESTING
//#define GC_FAILURE_HARD_LOOKUP //makes paths that fail to GC call find_references before del'ing.
//implies FIND_REF_NO_CHECK_TICK
//#define FIND_REF_NO_CHECK_TICK //Sets world.loop_checks to false and prevents find references from sleeping
#endif
#define IS_MODE_COMPILED(MODE) (ispath(text2path("/datum/game_mode/"+(MODE))))
#define BACKGROUND_ENABLED 0 // 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 -1
View File
@@ -2,7 +2,7 @@
if(pre_attackby(target, user, params))
// Return 1 in attackby() to prevent afterattack() effects (when safely moving items for example)
var/resolved = target.attackby(src, user, params)
if(!resolved && target && !qdeleted(src))
if(!resolved && target && !QDELETED(src))
afterattack(target, user, 1, params) // 1: clicking something Adjacent
// Called when the item is in the active hand, and clicked; alternately, there is an 'activate held object' verb or you can hit pagedown.
@@ -393,7 +393,7 @@
exceptions[eid] = 0
/datum/controller/process/proc/catchBadType(var/datum/caught)
if(isnull(caught) || !istype(caught) || !isnull(caught.gcDestroyed))
if(isnull(caught) || !istype(caught) || QDELETED(caught))
return // Only bother with types we can identify and that don't belong
catchException("Type [caught.type] does not belong in process' queue")
-17
View File
@@ -1,17 +0,0 @@
var/global/datum/controller/process/garbage_collector/garbageCollector
/datum/controller/process/garbage_collector/setup()
name = "garbage"
schedule_interval = 10
start_delay = 3
/datum/controller/process/garbage_collector/doWork()
// Garbage collection code can be found in code\modules\garbage collection\garbage_collector.dm
processGarbage()
/datum/controller/process/garbage_collector/statProcess()
..()
stat(null, "[del_everything ? "Off" : "On"], [queue.len] queued")
stat(null, "Dels: [dels_count], [soft_dels] soft, [hard_dels] hard")
DECLARE_GLOBAL_CONTROLLER(garbage_collector, garbageCollector)
+3 -3
View File
@@ -24,7 +24,7 @@
/datum/controller/process/machinery/proc/process_machines()
for(last_object in machine_processing)
var/obj/machinery/M = last_object
if(istype(M) && isnull(M.gcDestroyed))
if(istype(M) && !QDELETED(M))
try
if(M.process() == PROCESS_KILL)
machine_processing.Remove(M)
@@ -43,7 +43,7 @@
/datum/controller/process/machinery/proc/process_power()
for(last_object in deferred_powernet_rebuilds)
var/obj/O = last_object
if(istype(O) && isnull(O.gcDestroyed))
if(istype(O) && !QDELETED(O))
try
var/datum/powernet/newPN = new()// creates a new powernet...
propagate_network(O, newPN)//... and propagates it to the other side of the cable
@@ -56,7 +56,7 @@
for(last_object in powernets)
var/datum/powernet/powerNetwork = last_object
if(istype(powerNetwork) && isnull(powerNetwork.gcDestroyed))
if(istype(powerNetwork) && !QDELETED(powerNetwork))
try
powerNetwork.reset()
catch(var/exception/e)
+1 -1
View File
@@ -21,7 +21,7 @@ var/global/datum/controller/process/mob/mob_master
/datum/controller/process/mob/doWork()
for(last_object in mob_list)
var/mob/M = last_object
if(istype(M) && isnull(M.gcDestroyed))
if(istype(M) && !QDELETED(M))
try
M.Life()
catch(var/exception/e)
+2 -2
View File
@@ -25,7 +25,7 @@ var/global/datum/controller/process/npcai/npcai_master
/datum/controller/process/npcai/doWork()
for(last_object in simple_animal_list)
var/mob/living/simple_animal/M = last_object
if(istype(M) && isnull(M.gcDestroyed))
if(istype(M) && !QDELETED(M))
if(!M.client && M.stat == CONSCIOUS)
try
M.process_ai()
@@ -44,7 +44,7 @@ var/global/datum/controller/process/npcai/npcai_master
for(last_object in snpc_list)
var/mob/living/carbon/human/interactive/M = last_object
if(istype(M) && isnull(M.gcDestroyed))
if(istype(M) && !QDELETED(M))
try
if(!M.alternateProcessing || M.forceProcess)
M.doProcess()
+1 -1
View File
@@ -15,7 +15,7 @@
/datum/controller/process/obj/doWork()
for(last_object in processing_objects)
var/datum/O = last_object
if(istype(O) && isnull(O.gcDestroyed))
if(istype(O) && !QDELETED(O))
try
// Reagent datums get shoved in here, but the process proc isn't on the
// base datum type, so we just call it blindly.
+1 -1
View File
@@ -19,7 +19,7 @@ var/global/datum/controller/process/timer/timer_master
return
for(last_object in processing_timers)
var/datum/timedevent/event = last_object
if(!event.thingToCall || qdeleted(event.thingToCall))
if(!event.thingToCall || check_datum_qdeleted(event.thingToCall))
qdel(event)
if(event.timeToRun <= world.time)
runevent(event)
+1 -1
View File
@@ -28,7 +28,7 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
/datum/controller/failsafe/Initialize()
set waitfor = 0
Failsafe.Loop()
if(!qdeleted(src))
if(!QDELETED(src))
qdel(src) //when Loop() returns, we delete ourselves and let the mc recreate us
/datum/controller/failsafe/Destroy()
+1 -1
View File
@@ -21,7 +21,7 @@ SUBSYSTEM_DEF(fires)
while(currentrun.len)
var/obj/O = currentrun[currentrun.len]
currentrun.len--
if(!O || qdeleted(O))
if(!O || QDELETED(O))
processing -= O
if(MC_TICK_CHECK)
return
+436
View File
@@ -0,0 +1,436 @@
SUBSYSTEM_DEF(garbage)
name = "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
var/list/collection_timeout = list(0, 2 MINUTES, 10 SECONDS) // 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/list/pass_counts
var/list/fail_counts
var/list/items = list() // Holds our qdel_item statistics datums
//Queue
var/list/queues
/datum/controller/subsystem/garbage/PreInit()
queues = new(GC_QUEUE_COUNT)
pass_counts = new(GC_QUEUE_COUNT)
fail_counts = new(GC_QUEUE_COUNT)
for(var/i in 1 to GC_QUEUE_COUNT)
queues[i] = list()
pass_counts[i] = 0
fail_counts[i] = 0
/datum/controller/subsystem/garbage/stat_entry(msg)
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|"
else
msg += "[round((gcedlasttick/(delslasttick+gcedlasttick))*100, 0.01)]%|"
msg += "TD:[totaldels]|TG:[totalgcs]|"
if(!(totaldels+totalgcs))
msg += "n/a|"
else
msg += "TGR:[round((totalgcs/(totaldels+totalgcs))*100, 0.01)]%"
msg += " P:[pass_counts.Join(",")]"
msg += "|F:[fail_counts.Join(",")]"
..(msg)
/* TO-DO
/datum/controller/subsystem/garbage/Shutdown()
//Adds the del() log to the qdel log file
var/list/dellog = 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]
dellog += "Path: [path]"
if(I.failures)
dellog += "\tFailures: [I.failures]"
dellog += "\tqdel() Count: [I.qdels]"
dellog += "\tDestroy() Cost: [I.destroy_time]ms"
if(I.hard_deletes)
dellog += "\tTotal Hard Deletes [I.hard_deletes]"
dellog += "\tTime Spent Hard Deleting: [I.hard_delete_time]ms"
if(I.slept_destroy)
dellog += "\tSleeps: [I.slept_destroy]"
if(I.no_respect_force)
dellog += "\tIgnored force: [I.no_respect_force] times"
if(I.no_hint)
dellog += "\tNo hint: [I.no_hint] times"
log_qdel(dellog.Join("\n"))
*/
/datum/controller/subsystem/garbage/fire()
//the fact that this resets its processing each fire (rather then resume where it left off) is intentional.
var/queue = GC_QUEUE_PREQUEUE
while(state == SS_RUNNING)
switch (queue)
if(GC_QUEUE_PREQUEUE)
HandlePreQueue()
queue = GC_QUEUE_PREQUEUE+1
if(GC_QUEUE_CHECK)
HandleQueue(GC_QUEUE_CHECK)
queue = GC_QUEUE_CHECK+1
if(GC_QUEUE_HARDDELETE)
HandleQueue(GC_QUEUE_HARDDELETE)
break
if(state == SS_PAUSED) //make us wait again before the next run.
state = SS_RUNNING
//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/HandlePreQueue()
var/list/tobequeued = queues[GC_QUEUE_PREQUEUE]
var/static/count = 0
if(count)
var/c = count
count = 0 //so if we runtime on the Cut, we don't try again.
tobequeued.Cut(1,c+1)
for(var/ref in tobequeued)
count++
Queue(ref, GC_QUEUE_PREQUEUE+1)
if (MC_TICK_CHECK)
break
if(count)
tobequeued.Cut(1,count+1)
count = 0
/datum/controller/subsystem/garbage/proc/HandleQueue(level = GC_QUEUE_CHECK)
if(level == GC_QUEUE_CHECK)
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
for(var/refID in queue)
if(!refID)
count++
if(MC_TICK_CHECK)
break
continue
var/GCd_at_time = queue[refID]
if(GCd_at_time > cut_off_time)
break // Everything else is newer, skip them
count++
var/datum/D
D = locate(refID)
if(!D || D.gc_destroyed != GCd_at_time) // So if something else coincidently gets the same ref, it's not deleted by mistake
++gcedlasttick
++totalgcs
pass_counts[level]++
if(MC_TICK_CHECK)
break
continue
// Something's still referring to the qdel'd object.
fail_counts[level]++
switch(level)
if(GC_QUEUE_CHECK)
#ifdef GC_FAILURE_HARD_LOOKUP
D.find_references()
#endif
var/type = D.type
var/datum/qdel_item/I = items[type]
testing("GC: -- \ref[src] | [type] was unable to be GC'd --")
I.failures++
if(GC_QUEUE_HARDDELETE)
HardDelete(D)
if(MC_TICK_CHECK)
break
continue
Queue(D, level+1)
if(MC_TICK_CHECK)
break
if(count)
queue.Cut(1,count+1)
count = 0
/datum/controller/subsystem/garbage/proc/PreQueue(datum/D)
if(D.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
queues[GC_QUEUE_PREQUEUE] += D
D.gc_destroyed = GC_QUEUED_FOR_QUEUING
/datum/controller/subsystem/garbage/proc/Queue(datum/D, level = GC_QUEUE_CHECK)
if(isnull(D))
return
if(D.gc_destroyed == GC_QUEUED_FOR_HARD_DEL)
level = GC_QUEUE_HARDDELETE
if(level > GC_QUEUE_COUNT)
HardDelete(D)
return
var/gctime = world.time
var/refid = "\ref[D]"
D.gc_destroyed = gctime
var/list/queue = queues[level]
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.
queue[refid] = gctime
//this is mainly to separate things profile wise.
/datum/controller/subsystem/garbage/proc/HardDelete(datum/D)
var/time = world.timeofday
var/tick = TICK_USAGE
var/ticktime = world.time
++delslasttick
++totaldels
var/type = D.type
var/refID = "\ref[D]"
del(D)
tick = (TICK_USAGE-tick+((world.time-ticktime)/world.tick_lag*100))
var/datum/qdel_item/I = items[type]
I.hard_deletes++
I.hard_delete_time += TICK_DELTA_TO_MS(tick)
if(tick > highest_del_tickusage)
highest_del_tickusage = tick
time = world.timeofday - time
if(!time && TICK_DELTA_TO_MS(tick) > 1)
time = TICK_DELTA_TO_MS(tick)/100
if(time > highest_del_time)
highest_del_time = time
if(time > 10)
log_game("Error: [type]([refID]) took longer than 1 second to delete (took [time/10] seconds to delete)")
message_admins("Error: [type]([refID]) took longer than 1 second to delete (took [time/10] seconds to delete).")
postpone(time)
/datum/controller/subsystem/garbage/proc/HardQueue(datum/D)
if(D.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
queues[GC_QUEUE_PREQUEUE] += 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)
queues[i] |= SSgarbage.queues[i]
/datum/qdel_item
var/name = ""
var/qdels = 0 //Total number of times it's passed thru qdel.
var/destroy_time = 0 //Total amount of milliseconds spent processing this type's Destroy()
var/failures = 0 //Times it was queued for soft deletion but failed to soft delete.
var/hard_deletes = 0 //Different from failures because it also includes QDEL_HINT_HARDDEL deletions
var/hard_delete_time = 0//Total amount of milliseconds spent hard deleting this type.
var/no_respect_force = 0//Number of times it's not respected force=TRUE
var/no_hint = 0 //Number of times it's not even bother to give a qdel hint
var/slept_destroy = 0 //Number of times it's slept in its destroy
/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/D, force=FALSE, ...)
if(!istype(D))
del(D)
return
var/datum/qdel_item/I = SSgarbage.items[D.type]
if(!I)
I = SSgarbage.items[D.type] = new /datum/qdel_item(D.type)
I.qdels++
if(isnull(D.gc_destroyed))
D.gc_destroyed = GC_CURRENTLY_BEING_QDELETED
var/start_time = world.time
var/start_tick = world.tick_usage
var/hint = D.Destroy(arglist(args.Copy(2))) // Let our friend know they're about to get fucked up.
if(world.time != start_time)
I.slept_destroy++
else
I.destroy_time += TICK_USAGE_TO_MS(start_tick)
if(!D)
return
switch(hint)
if(QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
SSgarbage.PreQueue(D)
if(QDEL_HINT_IWILLGC)
D.gc_destroyed = world.time
return
if(QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
if(!force)
D.gc_destroyed = null //clear the gc variable (important!)
return
// Returning LETMELIVE after being told to force destroy
// indicates the objects Destroy() does not respect force
#ifdef TESTING
if(!I.no_respect_force)
testing("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.")
#endif
I.no_respect_force++
SSgarbage.PreQueue(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 TESTING is enabled, display all references to this object, then queue the object for deletion.
SSgarbage.PreQueue(D)
#ifdef TESTING
D.find_references()
#endif
else
#ifdef TESTING
if(!I.no_hint)
testing("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
I.no_hint++
SSgarbage.PreQueue(D)
else if(D.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
CRASH("[D.type] destroy proc was called multiple times, likely due to a qdel loop in the Destroy logic")
#ifdef TESTING
/datum/verb/find_refs()
set category = "Debug"
set name = "Find References"
set src in world
find_references(FALSE)
/datum/proc/find_references(skip_alert)
running_find_references = type
if(usr && 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
//restart the garbage collector
SSgarbage.can_fire = 1
SSgarbage.next_fire = world.time + world.tick_lag
return
if(!skip_alert)
if(alert("Running this will lock everything up for about 5 minutes. Would you like to begin the search?", "Find References", "Yes", "No") == "No")
running_find_references = null
return
//this keeps the garbage collector from failing to collect objects being searched for in here
SSgarbage.can_fire = 0
if(usr && usr.client)
usr.client.running_find_references = type
testing("Beginning search for references to a [type].")
last_find_references = world.time
DoSearchVar(GLOB) //globals
for(var/datum/thing in world) //atoms (don't beleive it's lies)
DoSearchVar(thing, "World -> [thing]")
for(var/datum/thing) //datums
DoSearchVar(thing, "World -> [thing]")
for(var/client/thing) //clients
DoSearchVar(thing, "World -> [thing]")
testing("Completed search for references to a [type].")
if(usr && usr.client)
usr.client.running_find_references = null
running_find_references = null
//restart the garbage collector
SSgarbage.can_fire = 1
SSgarbage.next_fire = world.time + world.tick_lag
/datum/verb/qdel_then_find_references()
set category = "Debug"
set name = "qdel() then Find References"
set src in world
qdel(src)
if(!running_find_references)
find_references(TRUE)
/datum/proc/DoSearchVar(X, Xname, recursive_limit = 64)
if(usr && usr.client && !usr.client.running_find_references)
return
if(!recursive_limit)
return
if(istype(X, /datum))
var/datum/D = X
if(D.last_find_references == last_find_references)
return
D.last_find_references = last_find_references
var/list/L = D.vars
for(var/varname in L)
if (varname == "vars")
continue
var/variable = L[varname]
if(variable == src)
testing("Found [src.type] \ref[src] in [D.type]'s [varname] var. [Xname]")
else if(islist(variable))
DoSearchVar(variable, "[Xname] -> list", recursive_limit-1)
else if(islist(X))
var/normal = IS_NORMAL_LIST(X)
for(var/I in X)
if (I == src)
testing("Found [src.type] \ref[src] in list [Xname].")
else if (I && !isnum(I) && normal && X[I] == src)
testing("Found [src.type] \ref[src] in list [Xname]\[[I]\]")
else if (islist(I))
DoSearchVar(I, "[Xname] -> list", recursive_limit-1)
#ifndef FIND_REF_NO_CHECK_TICK
CHECK_TICK
#endif
#endif
+1 -1
View File
@@ -70,7 +70,7 @@
debug_variables(alarm_manager)
feedback_add_details("admin_verb", "DAlarm")
if("Garbage")
debug_variables(garbageCollector)
debug_variables(SSgarbage)
feedback_add_details("admin_verb","DGarbage")
if("Nano")
debug_variables(SSnanoui)
+2 -2
View File
@@ -20,9 +20,9 @@ var/global/list/round_voters = list() //Keeps track of the individuals voting fo
qdel(vote)
vote = src
spawn(0)
while(!gcDestroyed)
while(!QDELETED(src))
try
while(!gcDestroyed)
while(!QDELETED(src))
sleep(10)
process()
catch(var/exception/e)
+14
View File
@@ -0,0 +1,14 @@
/datum
var/gc_destroyed //Time when this object was destroyed.
#ifdef TESTING
var/running_find_references
var/last_find_references = 0
#endif
// 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, ...)
tag = null
return QDEL_HINT_QUEUE
+1 -1
View File
@@ -54,7 +54,7 @@
target.setDir(holder.dir)
sleep(jaunt_in_time)
qdel(holder)
if(!qdeleted(target))
if(!QDELETED(target))
if(mobloc.density)
for(var/direction in alldirs)
var/turf/T = get_step(mobloc, direction)
+2 -2
View File
@@ -28,7 +28,7 @@
config.continuous_rounds = 0
return ..()
/obj/effect/proc_holder/spell/targeted/lichdom/cast(list/targets,mob/user = usr)
/obj/effect/proc_holder/spell/targeted/lichdom/cast(list/targets,mob/user = usr)
if(!config.continuous_rounds)
existence_stops_round_end = 1
config.continuous_rounds = 1
@@ -49,7 +49,7 @@
charge_counter = charge_max
return
if(!marked_item || qdeleted(marked_item)) //Wait nevermind
if(!marked_item || QDELETED(marked_item)) //Wait nevermind
to_chat(M, "<span class='warning'>Your phylactery is gone!</span>")
return
+2 -2
View File
@@ -144,7 +144,7 @@
if(throwing)
throwing.hit_atom(A)
. = 1
if(!A || qdeleted(A))
if(!A || QDELETED(A))
return
A.Bumped(src)
@@ -224,7 +224,7 @@
//called when src is thrown into hit_atom
/atom/movable/proc/throw_impact(atom/hit_atom, throwingdatum)
set waitfor = 0
if(exists(hit_atom))
if(!QDELETED(hit_atom))
return hit_atom.hitby(src)
/atom/movable/hitby(atom/movable/AM, skipcatch, hitpush = 1, blocked)
+1 -1
View File
@@ -98,7 +98,7 @@
return
var/choice = input(user, selection_prompt, selection_title) as null|anything in choosable_items
var/pickedtype = choosable_items[choice]
if(pickedtype && Adjacent(user) && src && !qdeleted(src) && !user.incapacitated() && cooldowntime <= world.time)
if(pickedtype && Adjacent(user) && src && !QDELETED(src) && !user.incapacitated() && cooldowntime <= world.time)
cooldowntime = world.time + creation_delay
var/obj/item/N = new pickedtype(get_turf(src))
to_chat(user, replacetext("[creation_message]", "%ITEM%", "[N]"))
+4 -4
View File
@@ -252,7 +252,7 @@
if(!possible_runes.len)
return
entered_rune_name = input(user, "Choose a rite to scribe.", "Sigils of Power") as null|anything in possible_runes
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated())
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated())
return
for(var/T in typesof(/obj/effect/rune))
var/obj/effect/rune/R = T
@@ -271,7 +271,7 @@
if(locate(/obj/effect/rune) in runeturf)
to_chat(user, "<span class='cult'>There is already a rune here.</span>")
return
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated())
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated())
return
if(ispath(rune_to_scribe, /obj/effect/rune/narsie) || ispath(rune_to_scribe, /obj/effect/rune/slaughter))//may need to change this - Fethas
if(finale_runes_ok(user,rune_to_scribe))
@@ -299,7 +299,7 @@
if(!do_after(user, initial(rune_to_scribe.scribe_delay)-scribereduct, target = get_turf(user)))
for(var/V in shields)
var/obj/machinery/shield/S = V
if(S && !qdeleted(S))
if(S && !QDELETED(S))
qdel(S)
return
if(locate(/obj/effect/rune) in runeturf)
@@ -309,7 +309,7 @@
"<span class='cult'>You finish drawing the arcane markings of [ticker.mode.cultdat.entity_title3].</span>")
for(var/V in shields)
var/obj/machinery/shield/S = V
if(S && !qdeleted(S))
if(S && !QDELETED(S))
qdel(S)
var/obj/effect/rune/R = new rune_to_scribe(runeturf, chosen_keyword)
R.blood_DNA = list()
+6 -6
View File
@@ -238,7 +238,7 @@ structure_check() searches for nearby cultist structures required for the invoca
possible_talismans[talisman_cult_name] = J //This is to allow the menu to let cultists select talismans by name
entered_talisman_name = input(user, "Choose a talisman to imbue.", "Talisman Choices") as null|anything in possible_talismans
talisman_type = possible_talismans[entered_talisman_name]
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated() || rune_in_use || !talisman_type)
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated() || rune_in_use || !talisman_type)
return
..()
visible_message("<span class='warning'>Dark power begins to channel into the paper!.</span>")
@@ -303,7 +303,7 @@ var/list/teleport_runes = list()
var/input_rune_key = input(user, "Choose a rune to teleport to.", "Rune to Teleport to") as null|anything in potential_runes //we know what key they picked
var/obj/effect/rune/teleport/actual_selected_rune = potential_runes[input_rune_key] //what rune does that key correspond to?
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated() || !actual_selected_rune)
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated() || !actual_selected_rune)
fail_invoke()
return
@@ -406,7 +406,7 @@ var/list/teleport_runes = list()
var/mob/offering
if(possible_targets.len > 1) //If there's more than one target, allow choice
offering = input(user, "Choose an offering to sacrifice.", "Unholy Tribute") as null|anything in possible_targets
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated())
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated())
return
else if(possible_targets.len) //Otherwise, if there's a target at all, pick the only one
offering = possible_targets[possible_targets.len]
@@ -654,7 +654,7 @@ var/list/teleport_runes = list()
log_game("Raise Dead rune failed - no catalyst corpse")
return
mob_to_sacrifice = input(user, "Choose a corpse to sacrifice.", "Corpse to Sacrifice") as null|anything in potential_sacrifice_mobs
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated() || !mob_to_sacrifice || rune_in_use)
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated() || !mob_to_sacrifice || rune_in_use)
return
for(var/mob/living/M in T.contents)
if(M.stat == DEAD)
@@ -664,7 +664,7 @@ var/list/teleport_runes = list()
log_game("Raise Dead rune failed - no corpse to revive")
return
mob_to_revive = input(user, "Choose a corpse to revive.", "Corpse to Revive") as null|anything in potential_revive_mobs
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated() || rune_in_use || !mob_to_revive)
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated() || rune_in_use || !mob_to_revive)
return
if(!in_range(mob_to_sacrifice,src))
to_chat(user, "<span class='cultitalic'>The sacrificial target has been moved!</span>")
@@ -858,7 +858,7 @@ var/list/teleport_runes = list()
if(!(M.current in invokers) && M.current && M.current.stat != DEAD)
cultists |= M.current
var/mob/living/cultist_to_summon = input(user, "Who do you wish to call to [src]?", "Followers of [ticker.mode.cultdat.entity_title3]") as null|anything in cultists
if(!Adjacent(user) || !src || qdeleted(src) || user.incapacitated())
if(!Adjacent(user) || !src || QDELETED(src) || user.incapacitated())
return
if(!cultist_to_summon)
to_chat(user, "<span class='cultitalic'>You require a summoning target!</span>")
+1 -1
View File
@@ -148,7 +148,7 @@
var/input_rune_key = input(user, "Choose a rune to teleport to.", "Rune to Teleport to") as null|anything in potential_runes //we know what key they picked
var/obj/effect/rune/teleport/actual_selected_rune = potential_runes[input_rune_key] //what rune does that key correspond to?
if(!src || qdeleted(src) || !user || user.l_hand != src && user.r_hand != src || user.incapacitated() || !actual_selected_rune)
if(!src || QDELETED(src) || !user || user.l_hand != src && user.r_hand != src || user.incapacitated() || !actual_selected_rune)
return ..(user, 0)
user.visible_message("<span class='warning'>Dust flows from [user]'s hand, and they disappear in a flash of red light!</span>", \
@@ -331,7 +331,7 @@
audible_message("<span class='italics'>You hear a loud electrical buzzing sound!</span>")
to_chat(src, "<span class='warning'>Reprogramming machine behaviour...</span>")
spawn(50)
if(M && !qdeleted(M))
if(M && !QDELETED(M))
new /mob/living/simple_animal/hostile/mimic/copy/machine(get_turf(M), M, src, 1)
else
to_chat(src, "<span class='notice'>Out of uses.</span>")
@@ -134,7 +134,7 @@
if(stat != CONSCIOUS)
return
var/be_borer = alert("Become a cortical borer? (Warning, You can no longer be cloned!)",,"Yes","No")
if(be_borer == "No" || !src || qdeleted(src))
if(be_borer == "No" || !src || QDELETED(src))
return
if(key)
return
@@ -175,7 +175,7 @@
if(!input)
return
if(src && !qdeleted(src) && !qdeleted(host))
if(src && !QDELETED(src) && !QDELETED(host))
var/say_string = (docile) ? "slurs" :"states"
if(host)
to_chat(host, "<span class='changeling'><i>[truename] [say_string]:</i> [input]</span>")
@@ -538,7 +538,7 @@
/mob/living/simple_animal/borer/proc/let_go()
if(!host || !src || qdeleted(host) || qdeleted(src))
if(!host || !src || QDELETED(host) || QDELETED(src))
return
if(!leaving)
return
@@ -605,7 +605,7 @@
to_chat(src, "You begin delicately adjusting your connection to the host brain...")
if(qdeleted(src) || qdeleted(host))
if(QDELETED(src) || QDELETED(host))
return
bonding = TRUE
@@ -805,7 +805,7 @@
if(!candidate || !candidate.mob)
return
if(!qdeleted(candidate) || !qdeleted(candidate.mob))
if(!QDELETED(candidate) || !QDELETED(candidate.mob))
var/datum/mind/M = create_borer_mind(candidate.ckey)
M.transfer_to(src)
candidate.mob = src
@@ -47,7 +47,7 @@
if(crit_fail)//in case it depowers while ghost is looking at yes/no
to_chat(user, "<span class='warning'>This swarmer shell is completely depowered. You cannot activate it.</span>")
return
if(qdeleted(src))
if(QDELETED(src))
to_chat(user, "Swarmer has been occupied by someone else.")
return
var/mob/living/simple_animal/hostile/swarmer/S = new /mob/living/simple_animal/hostile/swarmer(get_turf(loc))
@@ -53,12 +53,12 @@
removechains()
/mob/living/simple_animal/hostile/guardian/beam/proc/cleardeletedchains()
if(summonerchain && qdeleted(summonerchain))
if(summonerchain && QDELETED(summonerchain))
summonerchain = null
if(enemychains.len)
for(var/chain in enemychains)
var/datum/cd = chain
if(!chain || qdeleted(cd))
if(!chain || QDELETED(cd))
enemychains -= chain
/mob/living/simple_animal/hostile/guardian/beam/proc/shockallchains()
+1 -1
View File
@@ -240,7 +240,7 @@ Made by Xhuis
M.visible_message("<span class='big'>[M] screams and contorts!</span>", \
"<span class='userdanger'>THE LIGHT-- YOUR MIND-- <i>BURNS--</i></span>")
spawn(30)
if(!M || qdeleted(M))
if(!M || QDELETED(M))
return
M.visible_message("<span class='warning'>[M] suddenly bloats and explodes!</span>", \
"<span class='warning'><b>AAAAAAAAA<font size=3>AAAAAAAAAAAAA</font><font size=4>AAAAAAAAAAAA----</font></span>")
+2 -2
View File
@@ -91,7 +91,7 @@
cameranet.addCamera(src)
emped = 0 //Resets the consecutive EMP count
spawn(100)
if(!qdeleted(src))
if(!QDELETED(src))
cancelCameraAlarm()
for(var/mob/O in mob_list)
if(O.client && O.client.eye == src)
@@ -275,7 +275,7 @@
change_msg = "reactivates"
triggerCameraAlarm()
spawn(100)
if(!qdeleted(src))
if(!QDELETED(src))
cancelCameraAlarm()
if(displaymessage)
if(user)
+5 -5
View File
@@ -118,7 +118,7 @@
add_fingerprint(usr)
switch(href_list["action"])
if("toggle")
if(qdeleted(src))
if(QDELETED(src))
return
if(!active)
if(stop > world.time)
@@ -143,7 +143,7 @@
for(var/datum/track/S in songs)
available[S.song_name] = S
var/selected = input(usr, "Choose your song", "Track:") as null|anything in available
if(qdeleted(src) || !selected || !istype(available[selected], /datum/track))
if(QDELETED(src) || !selected || !istype(available[selected], /datum/track))
return
selection = available[selected]
updateUsrDialog()
@@ -165,7 +165,7 @@
deejay('sound/ai/harmalarm.ogg')
/obj/machinery/disco/proc/deejay(S)
if(qdeleted(src) || !active || charge < 5)
if(QDELETED(src) || !active || charge < 5)
to_chat(usr, "<span class='warning'>The device is not able to play more DJ sounds at this time.</span>")
return
charge -= 5
@@ -241,7 +241,7 @@
/obj/machinery/disco/proc/lights_spin()
for(var/i in 1 to 25)
if(qdeleted(src) || !active)
if(QDELETED(src) || !active)
return
var/obj/effect/overlay/sparkles/S = new /obj/effect/overlay/sparkles(src)
S.alpha = 0
@@ -266,7 +266,7 @@
reveal.alpha = 255
while(active)
for(var/obj/item/device/flashlight/spotlight/glow in spotlights) // The multiples reflects custom adjustments to each colors after dozens of tests
if(qdeleted(src) || !active || qdeleted(glow))
if(QDELETED(src) || !active || QDELETED(glow))
return
if(glow.light_color == "red")
glow.light_color = "nw"
+1 -1
View File
@@ -79,7 +79,7 @@ for reference:
user.visible_message("<span class='notice'>[user] is prying apart \the [src].</span>", "<span class='notice'>You begin to pry apart \the [src].</span>")
playsound(src, W.usesound, 200, 1)
if(do_after(user, 300 * W.toolspeed, target = src) && src && !src.gcDestroyed)
if(do_after(user, 300 * W.toolspeed, target = src) && !QDELETED(src))
user.visible_message("<span class='notice'>[user] pries apart \the [src].</span>", "<span class='notice'>You pry apart \the [src].</span>")
dismantle()
return
+2 -2
View File
@@ -180,7 +180,7 @@ About the new airlock wires panel:
/obj/machinery/door/airlock/autoclose()
autoclose_timer = 0
if(!qdeleted(src) && !density && !operating && !locked && !welded && autoclose)
if(!QDELETED(src) && !density && !operating && !locked && !welded && autoclose)
close()
return
@@ -1161,7 +1161,7 @@ About the new airlock wires panel:
operating = TRUE
update_icon(AIRLOCK_EMAG, 1)
sleep(6)
if(qdeleted(src))
if(QDELETED(src))
return
operating = FALSE
if(!open())
+1 -1
View File
@@ -353,7 +353,7 @@
/obj/machinery/door/proc/autoclose()
autoclose_timer = 0
if(!qdeleted(src) && !density && !operating && !locked && !welded && autoclose)
if(!QDELETED(src) && !density && !operating && !locked && !welded && autoclose)
close()
/obj/machinery/door/proc/update_freelook_sight()
+3 -3
View File
@@ -71,7 +71,7 @@
to_chat(user, "<span class='warning'>The panel must be closed before operating this machine!</span>")
return
if(!linked_pad || qdeleted(linked_pad))
if(!linked_pad || QDELETED(linked_pad))
to_chat(user, "<span class='warning'>There is no linked pad!</span>")
return
@@ -108,14 +108,14 @@
teleporting = 1
spawn(teleport_speed)
if(!src || qdeleted(src))
if(!src || QDELETED(src))
teleporting = 0
return
if(stat & NOPOWER)
to_chat(user, "<span class='warning'>[src] is unpowered!</span>")
teleporting = 0
return
if(!linked_pad || qdeleted(linked_pad) || linked_pad.stat & NOPOWER)
if(!linked_pad || QDELETED(linked_pad) || linked_pad.stat & NOPOWER)
to_chat(user, "<span class='warning'>Linked pad is not responding to ping. Teleport aborted.</span>")
teleporting = 0
return
+1 -1
View File
@@ -112,7 +112,7 @@ var/const/SAFETY_COOLDOWN = 100
for(var/i in to_eat)
var/atom/movable/AM = i
if(!exists(AM))
if(QDELETED(AM))
continue
else if(isliving(AM))
if(emagged)
+3 -3
View File
@@ -162,11 +162,11 @@
var/data[0]
if(!recharge_port)
reconnect()
if(recharge_port && !qdeleted(recharge_port))
if(recharge_port && !QDELETED(recharge_port))
data["recharge_port"] = list("mech" = null)
if(recharge_port.recharging_mecha && !qdeleted(recharge_port.recharging_mecha))
if(recharge_port.recharging_mecha && !QDELETED(recharge_port.recharging_mecha))
data["recharge_port"]["mech"] = list("health" = recharge_port.recharging_mecha.health, "maxhealth" = initial(recharge_port.recharging_mecha.health), "cell" = null)
if(recharge_port.recharging_mecha.cell && !qdeleted(recharge_port.recharging_mecha.cell))
if(recharge_port.recharging_mecha.cell && !QDELETED(recharge_port.recharging_mecha.cell))
data["has_mech"] = 1
data["mecha_name"] = recharge_port.recharging_mecha || "None"
data["mecha_charge"] = isnull(recharge_port.recharging_mecha) ? 0 : recharge_port.recharging_mecha.cell.charge
+1 -1
View File
@@ -1275,7 +1275,7 @@
occupant = null
return
else
if(!AI.linked_core || qdeleted(AI.linked_core))
if(!AI.linked_core || QDELETED(AI.linked_core))
to_chat(AI, "<span class='userdanger'>Inactive core destroyed. Unable to return.</span>")
AI.linked_core = null
return
@@ -157,7 +157,7 @@
playsound(D.loc, 'sound/items/poster_being_created.ogg', 100, 1)
if(do_after(user, PLACE_SPEED, target = src))
if(!D || qdeleted(D))
if(!D || QDELETED(D))
return
if(iswallturf(src) && user && user.loc == temp_loc) //Let's check if everything is still there
+1 -1
View File
@@ -485,7 +485,7 @@ var/global/image/fire_overlay = image("icon" = 'icons/goonstation/effects/fire.d
else ..()
/obj/item/throw_impact(atom/A)
if(A && !qdeleted(A))
if(A && !QDELETED(A))
var/itempush = 1
if(w_class < WEIGHT_CLASS_BULKY)
itempush = 0 // too light to push anything
@@ -132,7 +132,7 @@
/obj/item/weapon/grenade/plastic/c4/prime()
var/turf/location
if(target)
if(!qdeleted(target))
if(!QDELETED(target))
location = get_turf(target)
target.overlays -= image_overlay
else
@@ -159,7 +159,7 @@
/obj/item/weapon/grenade/plastic/x4/prime()
var/turf/location
if(target)
if(!qdeleted(target))
if(!QDELETED(target))
location = get_turf(target)
target.overlays -= image_overlay
else
@@ -193,7 +193,7 @@
/obj/item/weapon/grenade/plastic/c4_shaped/prime()
var/turf/location
if(target)
if(!qdeleted(target))
if(!QDELETED(target))
location = get_turf(target)
target.overlays -= image_overlay
else
@@ -67,7 +67,7 @@
walk_away(P,loc,rand(1,4))
spawn(rand(15,60))
if(P && isnull(P.gcDestroyed))
if(!QDELETED(P))
if(istype(P, /obj/item/weapon/grenade))
P.prime()
qdel(src)
+1 -1
View File
@@ -130,7 +130,7 @@
/obj/singularity_act()
ex_act(1)
if(src && !qdeleted(src))
if(src && !QDELETED(src))
qdel(src)
return 2
+2 -2
View File
@@ -363,7 +363,7 @@
icon_state = "crema_active"
for(var/mob/living/M in search_contents_for(/mob/living))
if(!M || !isnull(M.gcDestroyed))
if(QDELETED(M))
continue
if(M.stat!=2)
M.emote("scream")
@@ -372,7 +372,7 @@
user.create_attack_log("<font color='red'>Cremated [M.name] ([M.ckey])</font>")
log_attack("[user.name] ([user.ckey]) cremated [M.name] ([M.ckey])")
M.death(1)
if(!M || !isnull(M.gcDestroyed))
if(QDELETED(M))
continue // Re-check for mobs that delete themselves on death
M.ghostize()
qdel(M)
-1
View File
@@ -159,7 +159,6 @@ var/list/admin_verbs_debug = list(
/client/proc/restart_controller,
/client/proc/enable_debug_verbs,
/client/proc/toggledebuglogs,
/client/proc/qdel_toggle,
/client/proc/cmd_display_del_log,
/client/proc/debugNatureMapGenerator,
/client/proc/check_bomb_impacts,
+32
View File
@@ -748,6 +748,38 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
if("Respawnable Mobs")
to_chat(usr, jointext(respawnable_list,","))
/client/proc/cmd_display_del_log()
set category = "Debug"
set name = "Display del() Log"
set desc = "Display del's log of everything that's passed through it."
if(!check_rights(R_DEBUG))
return
var/list/dellog = list("<B>List of things that have gone through qdel this round</B><BR><BR><ol>")
sortTim(SSgarbage.items, cmp=/proc/cmp_qdel_item_time, associative = TRUE)
for(var/path in SSgarbage.items)
var/datum/qdel_item/I = SSgarbage.items[path]
dellog += "<li><u>[path]</u><ul>"
if(I.failures)
dellog += "<li>Failures: [I.failures]</li>"
dellog += "<li>qdel() Count: [I.qdels]</li>"
dellog += "<li>Destroy() Cost: [I.destroy_time]ms</li>"
if(I.hard_deletes)
dellog += "<li>Total Hard Deletes [I.hard_deletes]</li>"
dellog += "<li>Time Spent Hard Deleting: [I.hard_delete_time]ms</li>"
if(I.slept_destroy)
dellog += "<li>Sleeps: [I.slept_destroy]</li>"
if(I.no_respect_force)
dellog += "<li>Ignored force: [I.no_respect_force]</li>"
if(I.no_hint)
dellog += "<li>No hint: [I.no_hint]</li>"
dellog += "</ul></li>"
dellog += "</ol>"
usr << browse(dellog.Join(), "window=dellog")
/client/proc/cmd_admin_toggle_block(var/mob/M,var/block)
if(!check_rights(R_SPAWN))
@@ -136,13 +136,13 @@
// Objects loop
if(!(flags & DMM_IGNORE_OBJS))
for(var/obj/O in model.contents)
if(O.dont_save || !isnull(O.gcDestroyed))
if(O.dont_save || QDELETED(O))
continue
obj_template += "[O.type][check_attributes(O,use_json=use_json)],"
// Mobs Loop
for(var/mob/M in model.contents)
if(M.dont_save || !isnull(M.gcDestroyed))
if(M.dont_save || QDELETED(M))
continue
if(M.client)
if(!(flags & DMM_IGNORE_PLAYERS))
+7
View File
@@ -20,6 +20,13 @@
var/adminhelped = 0
var/gc_destroyed //Time when this object was destroyed.
#ifdef TESTING
var/running_find_references
var/last_find_references = 0
#endif
///////////////
//SOUND STUFF//
///////////////
+1 -1
View File
@@ -42,7 +42,7 @@
return
/obj/effect/countdown/process()
if(!attached_to || qdeleted(attached_to))
if(!attached_to || QDELETED(attached_to))
qdel(src)
forceMove(get_turf(attached_to))
var/new_val = get_value()
+2 -2
View File
@@ -32,7 +32,7 @@
log_debug("Original brand intelligence machine: [originMachine] [ADMIN_VV(originMachine)] [ADMIN_JMP(originMachine)]")
/datum/event/brand_intelligence/tick()
if(!originMachine || !isnull(originMachine.gcDestroyed) || originMachine.shut_up || originMachine.wires.IsAllCut()) //if the original vending machine is missing or has it's voice switch flipped
if(!originMachine || QDELETED(originMachine) || originMachine.shut_up || originMachine.wires.IsAllCut()) //if the original vending machine is missing or has it's voice switch flipped
for(var/obj/machinery/vending/saved in infectedMachines)
saved.shoot_inventory = 0
if(originMachine)
@@ -43,7 +43,7 @@
if(!vendingMachines.len) //if every machine is infected
for(var/obj/machinery/vending/upriser in infectedMachines)
if(prob(70) && isnull(upriser.gcDestroyed))
if(prob(70) && !QDELETED(upriser))
var/mob/living/simple_animal/hostile/mimic/copy/M = new(upriser.loc, upriser, null, 1) // it will delete upriser on creation and override any machine checks
M.faction = list("profit")
M.speak = rampant_speeches.Copy()
+1 -1
View File
@@ -607,7 +607,7 @@
var/list/obj/structure/spacevine/queue_end = list()
for(var/obj/structure/spacevine/SV in growth_queue)
if(qdeleted(SV))
if(QDELETED(SV))
continue
i++
queue_end += SV
+1 -1
View File
@@ -264,7 +264,7 @@ Gunshots/explosions/opening doors/less rare audio (done)
to_chat(T, "<span class='changeling'><i>Primary [rand(1000,9999)] states:</i> [pick("Hello","Hi","You're my slave now!","Don't try to get rid of me...")]</span>")
break
sleep(4)
if(!qdeleted(borer))
if(!QDELETED(borer))
qdel(borer)
qdel(src)
+1 -1
View File
@@ -1248,7 +1248,7 @@
return 1
/obj/item/weapon/reagent_containers/food/snacks/monkeycube/proc/Expand()
if(isnull(gcDestroyed))
if(!QDELETED(src))
visible_message("<span class='notice'>[src] expands!</span>")
new/mob/living/carbon/human(get_turf(src), monkey_type)
qdel(src)
@@ -1,65 +0,0 @@
/*
===============================================================================
How Garbage Collection Works
===============================================================================
In BYOND, there are exactly two ways anything gets deleted:
- A "soft delete", which occurs when an object's reference count hits 0, meaning it is not being referenced by anything
in the world (more on references further down). When an object is unreferenced, nothing can access it, so it has no
reason to exist, and BYOND's garbage collector simply deletes the object. This is fast.
- A "hard delete", which occurs when the del keyword is used to directly delete an object. This forces BYOND to find
every reference to the object being deleted, and null them all out. This is slow.
A reference is anything that refers to an object, and can thus be used to access it. A variable on another object, a
variable in a proc, an argument to a proc, the src of a running proc, the locs of contained objects, another object's
contents list (but not the world's automatic contents list), anything. BYOND keeps track of how many references there
are to an object - the reference count goes up when you make a new reference to an object, and back down when that
reference gets set to null, goes out of scope, or is deleted. When the reference count hits 0, there are no remaining
references to the object, and it is deleted by BYOND's garbage collector.
When you have something that exists in the world, and you want it to stop existing (maybe it got blown up, or eaten by
the singularity, or whatever), it needs to be deleted. You can use the del keyword to make sure it's deleted instantly,
but del is slow, and you want things to be deleted as quickly as possible, especially if you're deleting a whole lot of
them. You want soft deletes.
That's where the garbage collection system comes in - it prepares things to be soft deleted, and hard deletes anything
that can't be. There are two main procs involved in this process:
/proc/qdel(datumToDelete)
This is, effectively, a replacement for del that tells an object to prepare itself to be soft deleted by calling its
Destroy() proc. Depending on the qdel hint returned by Destroy(), qdel will queue the object in the garbage collector
(to be hard deleted if it isn't soft deleted), directly delete the object, or ignore the object and
assume it will handle deleting itself. An object passed into qdel will have its gcDestroyed var set, so
isnull(gcDestroyed) will be true if an object is not being destroyed, and false if it is (which means you should get
rid of the reference you have to it).
Note that qdel can only work with datum-based objects, which excludes the world (deleting this shuts down the world),
clients (deleting these disconnect the client), lists, and savefiles. If any of these are passed to qdel, they will
be directly deleted, just as if they had been passed straight to del, so it should never be unsafe to qdel anything
you could del.
/datum/proc/Destroy()
This is, effectively, a replacement for Del() (with some exceptions) which is also responsible for nulling out
references to or on the object it is called on. Unlike Del, the Destroy proc will only be called by qdel; generally,
this should only happen to datums that are no longer referenced by anything, which shouldn't be an issue.
The exceptions where Destroy cannot replace Del are for the same non-datum types mentioned under qdel, above. Those
should use a Del proc for any necessary cleanup, as a Destroy proc on them will not automatically get called.
When called by qdel, Destroy is expected to return a qdel hint, which determines whether the object should be directly
deleted, queued for deletion, or ignored entirely; which of these are appropriate will vary, though objects
should be pooled or queued whenever possible. The full list of qdel hints are in the code for the qdel proc.
As mentioned above, gcDestroyed can be used to tell whether an object is being destroyed. This is important, because if
an object is being destroyed, it WILL still exist, and any code that references it should stop referencing it as soon as
possible. In the same places that you check whether a reference still exists, you should also check for something like
isnull(myRefVar.gcDestroyed), which will be false if your object is being destroyed, meaning you should throw out the
reference immediately.
The inner workings of the GC itself and the stuff related to testing it probably don't need a detailed description - if
you intend to work with them, it would be best to read the code to understand what they do.
*/
@@ -1,185 +0,0 @@
// Main garbage collection code
// For general information about how the GC works and how to use it, see __gc_info.dm
#define GC_COLLECTIONS_PER_TICK 150 // Was 100.
#define GC_COLLECTION_TIMEOUT (30 SECONDS)
#define GC_FORCE_DEL_PER_TICK 30
//#define GC_DEBUG
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
// The time a datum was destroyed by the GC, or null if it hasn't been
/datum/var/gcDestroyed
// Whether a datum was hard-deleted by the GC; 0 if not, 1 if it was queued, -1 if directly deleted
/datum/var/hard_deleted = 0
/datum/controller/process/garbage_collector
var/list/queue = new
var/del_everything = 0
// To let them know how hardworking am I :^).
var/dels_count = 0
var/hard_dels = 0
var/soft_dels = 0
// all types that did not respect qdel(A, force=TRUE) and returned one
// of the immortality qdel hints
var/list/noforcerespect = list()
/datum/controller/process/garbage_collector/proc/addTrash(var/datum/D)
if(!istype(D) || del_everything)
del(D)
hard_dels++
dels_count++
return
queue -= "\ref[D]" // If this is a re-used ref, remove the old ref from the queue
queue["\ref[D]"] = world.time
/datum/controller/process/garbage_collector/proc/processGarbage()
var/remainingCollectionPerTick = GC_COLLECTIONS_PER_TICK
var/remainingForceDelPerTick = GC_FORCE_DEL_PER_TICK
var/collectionTimeScope = world.time - GC_COLLECTION_TIMEOUT
while(queue.len && --remainingCollectionPerTick >= 0)
var/refID = queue[1]
var/destroyedAtTime = queue[refID]
if(destroyedAtTime > collectionTimeScope)
break
var/datum/D = locate(refID)
// If the object still exists, and it's the same object, hard del it
if(D && D.gcDestroyed == destroyedAtTime)
if(remainingForceDelPerTick <= 0)
break
#ifdef GC_DEBUG
gcwarning("GC process force delete [D.type]")
#endif
hardDel(D)
queue.Cut(1, 2)
remainingForceDelPerTick--
else // Otherwise, it was GC'd - remove it from the queue
queue.Cut(1, 2)
soft_dels++
dels_count++
SCHECK
#ifdef GC_DEBUG
#undef GC_DEBUG
#endif
#undef GC_FORCE_DEL_PER_TICK
#undef GC_COLLECTION_TIMEOUT
#undef GC_COLLECTIONS_PER_TICK
/datum/controller/process/garbage_collector/proc/hardDel(var/datum/D)
didntgc["[D.type]"]++
D.hard_deleted = 1
if(!D.gcDestroyed)
spawn(-1)
D.Destroy()
D.gcDestroyed = world.time
del(D)
hard_dels++
dels_count++
// Effectively replaces del for any datum-based type
/proc/qdel(datum/D, force=FALSE)
if(isnull(D))
return
if(!istype(D)) // A non-datum was passed into qdel - just delete it outright.
// warning("qdel() passed object of type [D.type]. qdel() can only handle /datum/ types.")
del(D)
return
if(isnull(garbageCollector))
D.Destroy()
del(D)
return
if(isnull(D.gcDestroyed))
// Let our friend know they're about to get fucked up.
var/hint
D.gcDestroyed = world.time
try
hint = D.Destroy(force)
catch(var/exception/e)
if(istype(e))
log_runtime(e, D, "Caught by qdel() destroying [D.type]")
else
gcwarning("qdel() caught runtime destroying [D.type]: [e]")
// Destroy runtimed? Panic! Hard delete!
D.hard_deleted = -1
del(D)
if(garbageCollector)
garbageCollector.dels_count++
return
if(!isnull(D.gcDestroyed) && D.gcDestroyed != world.time)
gcwarning("Sleep detected in Destroy() call of [D.type]")
sleptDestroy["[D.type]"]++
D.gcDestroyed = world.time
switch(hint)
if(QDEL_HINT_QUEUE) //qdel should queue the object for deletion
garbageCollector.addTrash(D)
if (QDEL_HINT_LETMELIVE, QDEL_HINT_IWILLGC) //qdel should let the object live after calling destory.
if(!force)
return
// Returning LETMELIVE after being told to force destroy
// indicates the objects Destroy() does not respect force
if(!("[D.type]" in garbageCollector.noforcerespect))
garbageCollector.noforcerespect += "[D.type]"
gcwarning("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.")
garbageCollector.addTrash(D)
if(QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
D.hard_deleted = -1 // -1 means "this hard del skipped the queue", used for profiling
del(D)
if(garbageCollector)
garbageCollector.dels_count++
if(QDEL_HINT_FINDREFERENCE)//qdel will, if TESTING is enabled, display all references to this object, then queue the object for deletion.
#ifdef TESTING
D.find_references(remove_from_queue = FALSE)
#endif
garbageCollector.addTrash(D)
else
if(!noqdelhint["[D.type]"])
noqdelhint["[D.type]"] = "[D.type]"
gcwarning("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.")
garbageCollector.addTrash(D)
// Returns 1 if the object has been queued for deletion.
/proc/qdeleted(var/datum/D)
if(!istype(D))
return 0
if(!isnull(D.gcDestroyed))
return 1
return 0
/*
* Like Del(), but for qdel.
* Called BEFORE qdel moves shit.
*/
/datum/proc/Destroy(force=FALSE)
tag = null
return QDEL_HINT_QUEUE // Garbage Collect everything.
/proc/gcwarning(msg)
log_to_dd("## GC WARNING: [msg]")
log_runtime(EXCEPTION(msg))
@@ -1,105 +0,0 @@
// Garbage collection testing/debugging/profiling code
/client/proc/qdel_toggle()
set name = "(GC) Toggle Queueing"
set desc = "Toggle qdel usage between normal and force del()."
set category = "Debug"
if(!check_rights(R_DEBUG))
return
garbageCollector.del_everything = !garbageCollector.del_everything
// to_chat(world, "<b>GC: qdel turned [garbageCollector.del_everything ? "off" : "on"].</b>")
log_admin("[key_name(usr)] turned qdel [garbageCollector.del_everything ? "off" : "on"].")
message_admins("<span class='notice'>[key_name_admin(usr)] turned qdel [garbageCollector.del_everything ? "off" : "on"].</span>", 1)
/client/proc/cmd_display_del_log()
set category = "Debug"
set name = "(GC) Display del() Log"
set desc = "Displays a list of things that have failed to GC this round"
if(!check_rights(R_DEBUG))
return
var/dat = "<B>List of things that failed to GC this round</B><BR><BR>"
for(var/path in didntgc)
dat += "[path] - [didntgc[path]] times<BR>"
dat += "<B>List of paths that did not return a qdel hint in Destroy()</B><BR><BR>"
for(var/path in noqdelhint)
dat += "[path]<BR>"
dat += "<B>List of paths that slept in Destroy()</B><BR><BR>"
for(var/path in sleptDestroy)
dat += "[path]<BR>"
usr << browse(dat, "window=dellog")
#ifdef TESTING
/client/var/running_find_references
/datum/var/running_find_references
/datum/verb/find_references(remove_from_queue = TRUE as num)
set category = "Debug"
set name = "Find References"
set background = 1
set src in world
running_find_references = type
if(usr && 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
return
if(alert("Running this will create a lot of lag until it finishes. You can cancel it by running it again. Would you like to begin the search?", "Find References", "Yes", "No") == "No")
running_find_references = null
return
// Remove this object from the list of things to be auto-deleted.
if(remove_from_queue && garbageCollector && ("\ref[src]" in garbageCollector.queue))
garbageCollector.queue -= "\ref[src]"
if(usr && usr.client)
usr.client.running_find_references = type
testing("Beginning search for references to a [type].")
var/list/things = list()
for(var/client/thing)
things |= thing
for(var/datum/thing)
things |= thing
testing("Collected list of things in search for references to a [type]. ([things.len] Thing\s)")
for(var/datum/thing in things)
if(usr && usr.client && !usr.client.running_find_references) return
for(var/varname in thing.vars)
var/variable = thing.vars[varname]
if(variable == src)
testing("Found [src.type] \ref[src] in [thing.type]'s [varname] var.")
else if(islist(variable))
if(src in variable)
testing("Found [src.type] \ref[src] in [thing.type]'s [varname] list var.")
testing("Completed search for references to a [type].")
if(usr && usr.client)
usr.client.running_find_references = null
running_find_references = null
/client/verb/purge_all_destroyed_objects()
set category = "Debug"
if(garbageCollector)
while(garbageCollector.queue.len)
var/datum/o = locate(garbageCollector.queue[1])
if(istype(o) && !isnull(o.gcDestroyed))
del(o)
garbageCollector.dels_count++
garbageCollector.queue.Cut(1, 2)
/datum/verb/qdel_then_find_references()
set category = "Debug"
set name = "qdel() then Find References"
set background = 1
set src in world
qdel(src)
if(!running_find_references)
find_references(remove_from_queue = FALSE)
#endif
+1 -1
View File
@@ -131,7 +131,7 @@
awakening = 1
spawn(30)
if(!qdeleted(src))
if(!QDELETED(src))
var/mob/living/simple_animal/hostile/killertomato/K = new /mob/living/simple_animal/hostile/killertomato(get_turf(loc))
K.maxHealth += round(seed.endurance / 3)
K.melee_damage_lower += round(seed.potency / 10)
@@ -427,7 +427,7 @@
/obj/structure/closet/stasis/process()
if(holder_animal)
if(holder_animal.stat == DEAD && !qdeleted(holder_animal))
if(holder_animal.stat == DEAD && !QDELETED(holder_animal))
dump_contents()
holder_animal.gib()
return
@@ -455,7 +455,7 @@
L.disabilities &= ~MUTE
L.status_flags &= ~GODMODE
L.notransform = 0
if(holder_animal && !qdeleted(holder_animal))
if(holder_animal && !QDELETED(holder_animal))
holder_animal.mind.transfer_to(L)
L.mind.RemoveSpell(/obj/effect/proc_holder/spell/targeted/exit_possession)
if(kill || !isanimal(loc))
+1 -1
View File
@@ -993,7 +993,7 @@
return 1
/mob/living/proc/harvest(mob/living/user)
if(qdeleted(src))
if(QDELETED(src))
return
if(butcher_results)
for(var/path in butcher_results)
+1 -1
View File
@@ -1236,7 +1236,7 @@ var/list/ai_verbs_default = list(
malfhacking = 0
clear_alert("hackingapc")
if(!istype(apc) || qdeleted(apc) || apc.stat & BROKEN)
if(!istype(apc) || QDELETED(apc) || apc.stat & BROKEN)
to_chat(src, "<span class='danger'>Hack aborted. The designated APC no longer exists on the power network.</span>")
playsound(get_turf(src), 'sound/machines/buzz-two.ogg', 50, 1)
else if(apc.aidisabled)
+1 -1
View File
@@ -21,7 +21,7 @@
death()
return 0
if(!eyeobj || qdeleted(eyeobj) || !eyeobj.loc)
if(!eyeobj || QDELETED(eyeobj) || !eyeobj.loc)
view_core()
if(machine)
@@ -218,7 +218,7 @@ Difficulty: Medium
fire_rain()
icon_state = "dragon"
if(swoop_target && !qdeleted(swoop_target) && swoop_target.z == src.z)
if(swoop_target && !QDELETED(swoop_target) && swoop_target.z == src.z)
tturf = get_turf(swoop_target)
else
tturf = get_turf(src)
@@ -233,7 +233,7 @@ Difficulty: Medium
L.gib()
else
L.adjustBruteLoss(75)
if(L && !qdeleted(L)) // Some mobs are deleted on death
if(L && !QDELETED(L)) // Some mobs are deleted on death
var/throw_dir = get_dir(src, L)
if(L.loc == loc)
throw_dir = pick(alldirs)
@@ -446,7 +446,7 @@ Difficulty: Hard
if(!currently_seeking)
currently_seeking = TRUE
targetturf = get_turf(target)
while(target && src && !qdeleted(src) && currently_seeking && x && y && targetturf) //can this target actually be sook out
while(target && src && !QDELETED(src) && currently_seeking && x && y && targetturf) //can this target actually be sook out
if(!moving) //we're out of tiles to move, find more and where the target is!
more_previouser_moving_dir = previous_moving_dir
previous_moving_dir = moving_dir
@@ -31,7 +31,7 @@
return
if(error_on_humanize == "")
var/spider_ask = alert(humanize_prompt, "Join as Terror Spider?", "Yes", "No")
if(spider_ask == "No" || !src || qdeleted(src))
if(spider_ask == "No" || !src || QDELETED(src))
return
else
to_chat(user, "Cannot inhabit spider: [error_on_humanize]")
+2 -2
View File
@@ -146,7 +146,7 @@
/obj/machinery/power/solar/ex_act(severity, target)
..()
if(isnull(gcDestroyed))
if(!QDELETED(src))
switch(severity)
if(2)
if(prob(50) && broken())
@@ -516,7 +516,7 @@
/obj/machinery/power/solar_control/ex_act(severity, target)
..()
if(isnull(gcDestroyed))
if(!QDELETED(src))
switch(severity)
if(2)
if(prob(50))
+1 -1
View File
@@ -75,7 +75,7 @@
/obj/structure/reagent_dispensers/fueltank/bullet_act(obj/item/projectile/P)
..()
if(!qdeleted(src)) //wasn't deleted by the projectile's effects.
if(!QDELETED(src)) //wasn't deleted by the projectile's effects.
if(!P.nodamage && ((P.damage_type == BURN) || (P.damage_type == BRUTE)))
message_admins("[key_name_admin(P.firer)] triggered a fueltank explosion.")
log_game("[key_name(P.firer)] triggered a fueltank explosion.")
+1 -1
View File
@@ -34,7 +34,7 @@
var/target_area
target_area = input("Area to land", "Select a Landing Zone", target_area) in teleportlocs
var/area/picked_area = teleportlocs[target_area]
if(!src || qdeleted(src))
if(!src || QDELETED(src))
return
var/turf/T = safepick(get_area_turfs(picked_area))
+2 -3
View File
@@ -190,7 +190,6 @@
#include "code\controllers\Processes\alarm.dm"
#include "code\controllers\Processes\event.dm"
#include "code\controllers\Processes\fast_process.dm"
#include "code\controllers\Processes\garbage.dm"
#include "code\controllers\Processes\lighting.dm"
#include "code\controllers\Processes\machinery.dm"
#include "code\controllers\Processes\mob.dm"
@@ -206,6 +205,7 @@
#include "code\controllers\ProcessScheduler\core\processScheduler.dm"
#include "code\controllers\subsystem\air.dm"
#include "code\controllers\subsystem\fires.dm"
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\nanoui.dm"
#include "code\controllers\subsystem\spacedrift.dm"
#include "code\controllers\subsystem\sun.dm"
@@ -219,6 +219,7 @@
#include "code\datums\cargoprofile.dm"
#include "code\datums\computerfiles.dm"
#include "code\datums\datacore.dm"
#include "code\datums\datum.dm"
#include "code\datums\datumvars.dm"
#include "code\datums\gas_mixture.dm"
#include "code\datums\hud.dm"
@@ -1412,8 +1413,6 @@
#include "code\modules\food_and_drinks\recipes\recipes_microwave.dm"
#include "code\modules\food_and_drinks\recipes\recipes_oven.dm"
#include "code\modules\food_and_drinks\recipes\tablecraft\recipes_table.dm"
#include "code\modules\garbage_collection\garbage_collector.dm"
#include "code\modules\garbage_collection\gc_testing.dm"
#include "code\modules\holiday\christmas.dm"
#include "code\modules\holiday\holiday.dm"
#include "code\modules\hydroponics\biogenerator.dm"