Del The World: Unit testing for hard deletes (#59612)

Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com>
This commit is contained in:
LemonInTheDark
2021-08-15 21:09:26 -07:00
committed by GitHub
co-authored by SteelSlayer
parent 0304206af3
commit cd576ab519
91 changed files with 788 additions and 168 deletions
+4
View File
@@ -688,6 +688,10 @@ This is good:
Setting `is_red` in args is simple, and directly names the variable the argument sets.
### Don't create code that hangs references
This is part of the larger issue of hard deletes, read this file for more info: [Guide to Harddels](HARDDEL_GUIDE.md))
### Other Notes
* Code should be modular where possible; if you are working on a new addition, then strongly consider putting it in its own file unless it makes sense to put it with similar ones (i.e. a new tool would go in the "tools.dm" file)
+265
View File
@@ -0,0 +1,265 @@
# Hard Deletes
1. [What is hard deletion](#What-is-hard-deletion)
2. [Causes of hard deletes](#causes-of-hard-deletes)
3. [Detecting hard deletes](#detecting-hard-deletes)
4. [Techniques for fixing hard deletes](#techniques-for-fixing-hard-deletes)
5. [Help my code is erroring how fix](#help-my-code-is-erroring-how-fix)
## What is Hard Deletion
Hard deletion is a very expensive operation that basically clears all references to some "thing" from memory. Objects that undergo this process are referred to as hard deletes, or simply harddels
What follows is a discussion of the theory behind this, why we would ever do it, and the what we do to avoid doing it as often as possible
I'm gonna be using words like references and garbage collection, but don't worry, it's not complex, just a bit hard to pierce
### Why do we need to Hard Delete?
Ok so let's say you're some guy called Jerry, and you're writing a programming language
You want your coders to be able to pass around objects without doing a full copy. So you'll store the pack of data somewhere in memory
```dm
/someobject
var/id = 42
var/name = "some shit"
```
Then you want them to be able to pass that object into say a proc, without doing a full copy. So you let them pass in the object's location in memory instead
This is called passing something by reference
```dm
someshit(someobject) //This isn't making a copy of someobject, it's passing in a reference to it
```
This of course means they can store that location in memory in another object's vars, or in a list, or whatever
```dm
/datum
var/reference
/proc/someshit(mem_location)
var/datum/some_obj = new()
some_obj.reference = mem_location
```
But what happens when you get rid of the object we're passing around references to? If we just cleared it out from memory, everything that holds a reference to it would suddenly be pointing to nowhere, or worse, something totally different!
So then, you've gotta do something to clean up these references when you want to delete an object
We could hold a list of references to everything that references us, but god, that'd get really expensive wouldn't it
Why not keep count of how many times we're referenced then? If an object's ref count is ever 0, nothing whatsoever cares about it, so we can freely get rid of it
But if something's holding onto a reference to us, we're not gonna have any idea where or what it is
So I guess you should scan all of memory for that reference?
```dm
del(someobject) //We now need to scan memory until we find the thing holding a ref to us, and clear it
```
This pattern is about how BYOND handles this problem of hanging references, or Garbage Collection
It's not a broken system, but as you can imagine scanning all of memory gets expensive fast
What can we do to help that?
### How we can avoid hard deletes
If hard deletion is so slow, we're gonna need to clean up all our references ourselves
In our codebase we do this with `/datum/proc/Destroy()`, a proc called by `qdel()`, whose purpose I will explain later
This procs only job is cleaning up references to the object it's called on. Nothing more, nothing else. Don't let me catch you giving it side effects
There's a long long list of things this does, since we use it a TON. So I can't really give you a short description. It will always move the object to nullspace though
## Causes Of Hard Deletes
Now that you know the theory, let's go over what can actually cause hard deletes. Some of this is obvious, some of it's much less so.
The BYOND reference has a list [Here](https://secure.byond.com/docs/ref/#/DM/garbage), but it's not a complete one
* Stored in a var
* An item in a list, or associated with a list item
* Has a tag
* Is on the map (always true for turfs)
* Inside another atom's contents
* Inside an atom's vis_contents
* A temporary value in a still-running proc
* Is a mob with a key
* Is an image object attached to an atom
Let's briefly go over the more painful ones yeah?
### Sleeping procs
Any proc that calls `sleep()`, `spawn()`, or anything that creates a seperate "thread" (not technically a thread, but it's the same in these terms. Not gonna cause any race conditions tho) will hang references to any var inside it. This includes the usr it started from, the src it was called on, and any vars created as a part of processing
### Static vars
`/static` and `/global` vars count for this too, they'll hang references just as well as anything. Be wary of this, these suckers can be a pain to solve
### Range() and View() like procs
Some internal BYOND procs will hold references to objects passed into them for a time after the proc is finished doing work, because they cache the returned info to make some code faster. You should never run into this issue, since we wait for what should be long enough to avoid this issue as a part of garbage collection
This is what `qdel()` does by the by, it literally just means queue deletion. A reference to the object gets put into a queue, and if it still exists after 5 minutes or so, we hard delete it
### Walk() procs
Calling `walk()` on something will put it in an internal queue, which it'll remain in until `walk(thing, 0)` is called on it, which removes it from the queue
This sort is very cheap to harddel, since BYOND prioritizes checking this queue first when it's clearing refs, but it should be avoided since it causes false positives
You can read more about how BYOND prioritizes these things [Here](https://www.patreon.com/posts/diving-for-35855766)
## Detecting Hard Deletes
For very simple hard deletes, simple inspection should be enough to find them. Look at what the object does during `Initialize()`, and see if it's doing anything it doesn't undo later.
If that fails, search the object's typepath, and look and see if anything is holding a reference to it without regard for the object deleting
BYOND currently doesn't have the capability to give us information about where a hard delete is. Fortunately we can search for most all of then ourselves.
The procs to perform this search are hidden behind compile time defines, since they'd be way too risky to expose to admin button pressing
If you're having issues solving a harddel and want to perform this check yourself, go to `_compile_options.dm` and uncomment `TESTING`, `REFERENCE_TRACKING`, and `GC_FAILURE_HARD_LOOKUP`
You can read more about what each of these do in that file, but the long and short of it is if something would hard delete our code will search for the reference (This will look like your game crashing, just hold out) and print information about anything it finds to the runtime log, which you can find inside the round folder inside `/data/logs/year/month/day`
It'll tell you what object is holding the ref if it's in an object, or what pattern of list transversal was required to find the ref if it's hiding in a list of some sort
## Techniques For Fixing Hard Deletes
Once you've found the issue, it becomes a matter of making sure the ref is cleared as a part of Destroy(). I'm gonna walk you through a few patterns and discuss how you might go about fixing them
### Our Tools
First and simplest we have `Destroy()`. Use this to clean up after yourself for simple cases
```dm
/someobject/Initialize()
. = ..()
GLOB.somethings += src //We add ourselves to some global list
/someobject/Destroy()
GLOB.somethings -= src //So when we Destroy() clean yourself from the list
return ..()
```
Next, and slightly more complex, pairs of objects that reference each other
This is helpful when for cases where both objects "own" each other
```dm
/someobject
var/someotherobject/buddy
/someotherobject
var/someobject/friend
/someobject/Initialize()
if(!buddy)
buddy = new()
buddy.friend = src
/someotherobject/Initialize()
if(!friend)
friend = new()
friend.buddy = src
/someobject/Destroy()
if(buddy)
buddy.friend = null //Make sure to clear their ref to you
buddy = null //We clear our ref to them to make sure nothing goes wrong
/someotherobject/Destroy()
if(friend)
friend.buddy = null //Make sure to clear their ref to you
friend = null //We clear our ref to them to make sure nothing goes wrong
```
Something similar can be accomplished with `QDELETED()`, a define that checks to see if something has started being `Destroy()`'d yet, and `QDEL_NULL()`, a define that `qdel()`'s a var and then sets it to null
Now let's discuss something a bit more complex, weakrefs
You'll need a bit of context, so let's do that now
BYOND has an internal bit of behavior that looks like this
`var/string = "\ref[someobject]"`
This essentially gets that object's position in memory directly. Unlike normal references, this doesn't count for hard deletes. You can retrieve the object in question by using `locate()`
`var/someobject/someobj = locate(string)`
This has some flaws however, since the bit of memory we're pointing to might change, which would cause issues. Fortunately we've developed a datum to handle worrying about this for you, `/datum/weakref`
You can create one using the `WEAKREF()` proc, and use weakref.resolve() to retrieve the actual object
This should be used for things that your object doesn't "own", but still cares about
For instance, a paper bin would own the paper inside it, but the paper inside it would just hold a weakref to the bin
There's no need to clean these up, just make sure you account for it being null, since it'll return that if the object doesn't exist or has been queued for deletion
```dm
/someobject
var/datum/weakref/our_coin
/someobject/proc/set_coin(/obj/item/coin/new_coin)
our_coin = WEAKREF(new_coin)
/someobject/proc/get_value()
if(!our_coin)
return 0
var/obj/item/coin/potential_coin = our_coin.resolve()
if(!potential_coin)
our_coin = null //Remember to clear the weakref if we get nothing
return 0
return potential_coin.value
```
Now, for the worst case scenario
Let's say you've got a var that's used too often to be weakref'd without making the code too expensive
You can't hold a paired reference to it because it's not like it would ever care about you outside of just clearing the ref
So then, we want to temporarily remember to clear a reference when it's deleted
This is where I might lose you, but we're gonna use signals
`qdel()`, the proc that sets off this whole deletion business, sends a signal called `COMSIG_PARENT_QDELETING`
We can listen for that signal, and if we hear it clear whatever reference we may have
Here's an example
```dm
/somemob
var/mob/target
/somemob/proc/set_target(new_target)
if(target)
UnregisterSignal(target, COMSIG_PARENT_QDELETING) //We need to make sure any old signals are cleared
target = new_target
if(target)
RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/clear_target) //Call clear_target if target is ever qdel()'d
/somemob/proc/clear_target(datum/source)
SIGNAL_HANDLER
set_target(null)
```
This really should be your last resort, since signals have some limitations. If some subtype of somemob also registered for parent_qdeleting on the same target you'd get a runtime, since signals don't support it
But if you can't do anything else for reasons of conversion ease, or hot code, this will work
## Help My Code Is Erroring How Fix
First, do a quick check.
Are you doing anything to the object in `Initialize()` that you don't undo in `Destroy()`? I don't mean like, setting its name, but are you adding it to any lists, stuff like that
If this fails, you're just gonna have to read over this doc. You can skip the theory if you'd like, but it's all pretty important for having an understanding of this problem
+1
View File
@@ -9,6 +9,7 @@ reset_label = "GBP: Reset"
"Feedback" = 2
"Fix" = 3
"Grammar and Formatting" = 1
"Hard Deletes" = 12
"Logging" = 1
"Performance" = 12
"Priority: CRITICAL" = 20
+1 -1
View File
@@ -582,7 +582,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define HIGHLANDER_TRAIT "highlander"
///generic atom traits
/// Trait from [/datum/component/rust]. Its rusty and should be applying a special overlay to denote this.
/// Trait from [/datum/element/rust]. Its rusty and should be applying a special overlay to denote this.
#define TRAIT_RUSTY "rust_trait"
#define DO_NOT_SPLASH "do_not_splash"
+6 -5
View File
@@ -55,14 +55,15 @@
animate(src, alpha = 255, time = CREDIT_EASE_DURATION, flags = ANIMATION_PARALLEL)
addtimer(CALLBACK(src, .proc/FadeOut), CREDIT_ROLL_SPEED - CREDIT_EASE_DURATION)
QDEL_IN(src, CREDIT_ROLL_SPEED)
P.screen += src
if(parent)
parent.screen += src
/atom/movable/screen/credit/Destroy()
var/client/P = parent
P.screen -= src
icon = null
LAZYREMOVE(P.credits, src)
parent = null
if(parent)
parent.screen -= src
LAZYREMOVE(parent.credits, src)
parent = null
return ..()
/atom/movable/screen/credit/proc/FadeOut()
+4 -1
View File
@@ -6,8 +6,11 @@
var/datum/hud/owner_hud
///Ensures that all the planes are correctly in the controlled_planes list.
/atom/movable/plane_master_controller/New(hud)
/atom/movable/plane_master_controller/New(datum/hud/hud)
. = ..()
if(!istype(hud))
return
owner_hud = hud
var/assoc_controlled_planes = list()
for(var/i in controlled_planes)
+6 -4
View File
@@ -287,8 +287,9 @@
return ..()
/atom/movable/screen/robot/lamp/Destroy()
robot.lampButton = null
robot = null
if(robot)
robot.lampButton = null
robot = null
return ..()
/atom/movable/screen/robot/modPC
@@ -303,8 +304,9 @@
robot.modularInterface?.interact(robot)
/atom/movable/screen/robot/modPC/Destroy()
robot.interfaceButton = null
robot = null
if(robot)
robot.interfaceButton = null
robot = null
return ..()
/atom/movable/screen/robot/alerts
+2
View File
@@ -675,6 +675,8 @@
/atom/movable/screen/splash/New(client/C, visible, use_previous_title) //TODO: Make this use INITIALIZE_IMMEDIATE, except its not easy
. = ..()
if(!istype(C))
return
holder = C
+4
View File
@@ -49,6 +49,10 @@ SUBSYSTEM_DEF(garbage)
var/list/queues
#ifdef REFERENCE_TRACKING
var/list/reference_find_on_fail = list()
#ifdef REFERENCE_TRACKING_DEBUG
//Should we save found refs. Used for unit testing
var/should_save_refs = FALSE
#endif
#endif
+4 -1
View File
@@ -89,6 +89,9 @@
to_chat(src, span_notice("You cannot directly influence the world around you, but you can see what [owner] cannot."))
/mob/camera/imaginary_friend/Initialize(mapload, _trauma)
if(!_trauma)
stack_trace("Imaginary friend created without trauma, wtf")
return INITIALIZE_HINT_QDEL
. = ..()
trauma = _trauma
@@ -131,7 +134,7 @@
client.images |= current_image
/mob/camera/imaginary_friend/Destroy()
if(owner.client)
if(owner?.client)
owner.client.images.Remove(human_image)
if(client)
client.images.Remove(human_image)
+6
View File
@@ -3,6 +3,7 @@
var/area/bound_area
var/turf/reset_turf
var/datum/movement_detector/move_tracker
var/moving = FALSE //Used to prevent infinite recursion if your reset turf places you somewhere on enter or something
/datum/component/areabound/Initialize()
if(!ismovable(parent))
@@ -18,7 +19,12 @@
if(!reset_turf || reset_turf.loc != bound_area)
stack_trace("Invalid areabound configuration") //qdel(src)
return
if(moving)
stack_trace("Moved during a reset move, giving up to prevent infinite recursion. Turf: [reset_turf.type] at [reset_turf.x], [reset_turf.y], [reset_turf.z]")
return
moving = TRUE
AM.forceMove(reset_turf)
moving = FALSE
/datum/component/areabound/Destroy(force, silent)
QDEL_NULL(move_tracker)
+2
View File
@@ -127,6 +127,8 @@
name = "nutrient sac"
/obj/item/udder/gutlunch/initial_conditions()
if(!udder_mob)
return
if(udder_mob.gender == FEMALE)
START_PROCESSING(SSobj, src)
RegisterSignal(udder_mob, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, .proc/on_mob_attacking)
+6 -1
View File
@@ -103,8 +103,13 @@
continue
qdel(timer)
//BEGIN: ECS SHIT
#ifdef REFERENCE_TRACKING
#ifdef REFERENCE_TRACKING_DEBUG
found_refs = null
#endif
#endif
//BEGIN: ECS SHIT
var/list/dc = datum_components
if(dc)
var/all_components = dc[/datum/component]
+14 -1
View File
@@ -116,7 +116,20 @@
/datum/element/connect_loc_behalf/proc/on_moved(atom/movable/tracked, atom/old_loc)
SIGNAL_HANDLER
var/datum/listener = targets[old_loc][tracked]
var/list/objects_in_old_loc = targets[old_loc]
//You may ask yourself, isn't this just silencing an error?
//The answer is yes, but there's no good cheap way to fix it
//What happens is the tracked object or hell the listener gets say, deleted, which makes targets[old_loc] return a null
//The null results in a bad index, because of course it does
//It's not a solvable problem though, since both actions, the destroy and the move, are sourced from the same signal send
//And sending a signal should be agnostic of the order of listeners
//So we need to either pick the order agnositic, or destroy safe
//And I picked destroy safe. Let's hope this is the right path!
if(!objects_in_old_loc)
return
var/datum/listener = objects_in_old_loc[tracked]
if(!listener) //See above
return
unregister_signals(listener, tracked, old_loc)
update_signals(listener, tracked)
+3 -2
View File
@@ -84,10 +84,11 @@
/datum/merger/proc/Refresh()
var/list/tips = list()
tips[origin] = NORTH|EAST|SOUTH|WEST
var/list/checked_turfs = list()
var/list/new_members = list()
new_members[origin] = NONE
if(origin)
tips[origin] = NORTH|EAST|SOUTH|WEST
new_members[origin] = NONE
while(length(tips))
var/atom/focus = tips[length(tips)]
var/dirs_to_check = tips[focus]
+16 -6
View File
@@ -7,9 +7,11 @@
flags_1 = NODECONSTRUCT_1
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
var/obj/structure/industrial_lift/tram/tram_part
light_color = LIGHT_COLOR_GREEN
///Weakref to the tram piece we control
var/datum/weakref/tram_ref
/obj/machinery/computer/tram_controls/Initialize(mapload, obj/item/circuitboard/C)
. = ..()
AddComponent(/datum/component/usb_port, list(/obj/item/circuit_component/tram_controls))
@@ -26,12 +28,14 @@
* Locates tram parts in the lift global list after everything is done.
*/
/obj/machinery/computer/tram_controls/proc/find_tram()
tram_part = GLOB.central_tram //possibly setting to something null, that's fine
tram_ref = WEAKREF(GLOB.central_tram)
/obj/machinery/computer/tram_controls/ui_state(mob/user)
return GLOB.not_incapacitated_state
/obj/machinery/computer/tram_controls/ui_status(mob/user,/datum/tgui/ui)
var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
if(tram_part?.travelling)
return UI_CLOSE
if(!in_range(user, src) && !isobserver(user))
@@ -45,6 +49,7 @@
ui.open()
/obj/machinery/computer/tram_controls/ui_data(mob/user)
var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
var/list/data = list()
data["moving"] = tram_part?.travelling
data["broken"] = tram_part ? FALSE : TRUE
@@ -94,9 +99,12 @@
/// Attempts to sends the tram to the given destination
/obj/machinery/computer/tram_controls/proc/try_send_tram(obj/effect/landmark/tram/to_where)
var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
if(!tram_part)
return FALSE
if(tram_part.travelling)
return FALSE
if(tram_part.controls_locked || tram_part.travelling) // someone else started
if(tram_part.controls_locked) // someone else started
return FALSE
tram_part.tram_travel(to_where)
return TRUE
@@ -131,12 +139,14 @@
. = ..()
if (istype(parent, /obj/machinery/computer/tram_controls))
computer = parent
RegisterSignal(computer.tram_part, COMSIG_TRAM_SET_TRAVELLING, .proc/on_tram_set_travelling)
RegisterSignal(computer.tram_part, COMSIG_TRAM_TRAVEL, .proc/on_tram_travel)
var/obj/structure/industrial_lift/tram/central/tram_part = computer.tram_ref?.resolve()
RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, .proc/on_tram_set_travelling)
RegisterSignal(tram_part, COMSIG_TRAM_TRAVEL, .proc/on_tram_travel)
/obj/item/circuit_component/tram_controls/unregister_usb_parent(atom/movable/parent)
var/obj/structure/industrial_lift/tram/central/tram_part = computer.tram_ref?.resolve()
computer = null
UnregisterSignal(computer.tram_part, list(COMSIG_TRAM_SET_TRAVELLING, COMSIG_TRAM_TRAVEL))
UnregisterSignal(tram_part, list(COMSIG_TRAM_SET_TRAVELLING, COMSIG_TRAM_TRAVEL))
return ..()
/obj/item/circuit_component/tram_controls/input_received(datum/port/input/port)
+1 -1
View File
@@ -35,7 +35,7 @@ GLOBAL_LIST_EMPTY(cryopod_computers)
/obj/machinery/computer/cryopod/Destroy()
GLOB.cryopod_computers -= src
..()
return ..()
/obj/machinery/computer/cryopod/update_icon_state()
if(machine_stat & (NOPOWER|BROKEN))
+5 -2
View File
@@ -229,7 +229,9 @@
briefcase = _briefcase
/obj/machinery/launchpad/briefcase/Destroy()
QDEL_NULL(briefcase)
if(!QDELETED(briefcase))
qdel(briefcase)
briefcase = null
return ..()
/obj/machinery/launchpad/briefcase/isAvailable()
@@ -269,7 +271,8 @@
/obj/item/storage/briefcase/launchpad/Destroy()
if(!QDELETED(pad))
QDEL_NULL(pad)
qdel(pad)
pad = null
return ..()
/obj/item/storage/briefcase/launchpad/PopulateContents()
+2
View File
@@ -79,6 +79,8 @@
/obj/machinery/navbeacon/proc/glob_lists_register(init=FALSE)
if(!init)
glob_lists_deregister()
if(!codes)
return
if(codes["patrol"])
if(!GLOB.navbeacons["[z]"])
GLOB.navbeacons["[z]"] = list()
+1 -1
View File
@@ -46,4 +46,4 @@ GLOBAL_LIST(gang_tags)
/obj/effect/decal/cleanable/crayon/gang/Destroy()
LAZYREMOVE(GLOB.gang_tags, src)
..()
return ..()
@@ -16,7 +16,7 @@
/obj/effect/shield/Destroy()
var/turf/location = get_turf(src)
location.heat_capacity=old_heat_capacity
..()
return ..()
/obj/effect/shield/singularity_act()
return
@@ -173,5 +173,7 @@
return
/obj/effect/dummy/chameleon/Destroy()
master.disrupt(0)
if(master)
master.disrupt(0)
master = null
return ..()
@@ -113,8 +113,9 @@
/obj/structure/projected_forcefield/Destroy()
visible_message(span_warning("[src] flickers and disappears!"))
playsound(src,'sound/weapons/resonator_blast.ogg',25,TRUE)
generator.current_fields -= src
generator = null
if(generator)
generator.current_fields -= src
generator = null
return ..()
/obj/structure/projected_forcefield/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
@@ -123,4 +124,5 @@
/obj/structure/projected_forcefield/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
if(sound_effect)
play_attack_sound(damage_amount, damage_type, damage_flag)
generator.shield_integrity = max(generator.shield_integrity - damage_amount, 0)
if(generator)
generator.shield_integrity = max(generator.shield_integrity - damage_amount, 0)
+3
View File
@@ -263,6 +263,9 @@
/obj/item/food/deepfryholder/Initialize(mapload, obj/item/fried)
if(!fried)
stack_trace("A deepfried object was created with no fried target")
return INITIALIZE_HINT_QDEL
. = ..()
name = fried.name //We'll determine the other stuff when it's actually removed
appearance = fried.appearance
@@ -70,7 +70,8 @@
/////////////////////////////////
/obj/effect/payload_spawner/Initialize(mapload, type, numspawned)
..()
spawn_payload(type, numspawned)
if(type && isnum(numspawned))
spawn_payload(type, numspawned)
return INITIALIZE_HINT_QDEL
/obj/effect/payload_spawner/proc/spawn_payload(type, numspawned)
@@ -505,10 +505,11 @@
/obj/item/borg/upgrade/defib/backpack/proc/on_defib_instance_qdel_or_moved(obj/item/defibrillator/D)
SIGNAL_HANDLER
defib_instance = null
qdel(src)
if(!QDELETED(src))
qdel(src)
/obj/item/borg/upgrade/defib/backpack/Destroy()
if(defib_instance)
if(!QDELETED(defib_instance))
QDEL_NULL(defib_instance)
return ..()
+2 -2
View File
@@ -237,9 +237,9 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
/obj/item/claymore/highlander/robot/Initialize()
var/obj/item/robot_model/kiltkit = loc
robot = kiltkit.loc
. = ..()
if(!istype(robot))
qdel(src)
return ..()
return INITIALIZE_HINT_QDEL
/obj/item/claymore/highlander/robot/process()
loc.layer = LARGE_MOB_LAYER
@@ -112,6 +112,9 @@
var/obj/machinery/computer/camera_advanced/base_construction/linked_console
/mob/camera/ai_eye/remote/base_construction/Initialize(mapload, obj/machinery/computer/camera_advanced/console_link)
if(!linked_console)
stack_trace("A base consturuction drone was created with no linked console")
return INITIALIZE_HINT_QDEL
linked_console = console_link
return ..()
@@ -5,7 +5,7 @@
/obj/structure/closet/secure_closet/freezer/Destroy()
recursive_organ_check(src)
..()
return ..()
/obj/structure/closet/secure_closet/freezer/Initialize()
. = ..()
@@ -40,7 +40,7 @@
/obj/machinery/deployable_turret/Destroy()
target = null
target_turf = null
..()
return ..()
/// Undeploying, for when you want to move your big dakka around
/obj/machinery/deployable_turret/wrench_act(mob/living/user, obj/item/wrench/used_wrench)
@@ -232,7 +232,7 @@
/obj/item/gun_control/Destroy()
turret = null
..()
return ..()
/obj/item/gun_control/CanItemAutoclick()
return TRUE
@@ -458,6 +458,10 @@ GLOBAL_DATUM(central_tram, /obj/structure/industrial_lift/tram/central)
SStramprocess.can_fire = TRUE
GLOB.central_tram = src
/obj/structure/industrial_lift/tram/central/Destroy()
GLOB.central_tram = null
return ..()
/obj/structure/industrial_lift/tram/LateInitialize()
. = ..()
find_our_location()
+3
View File
@@ -130,6 +130,9 @@
/obj/structure/trap/stun/hunter/flare()
..()
var/turf/our_turf = get_turf(src)
if(!our_turf)
return
stored_item.forceMove(get_turf(src))
forceMove(stored_item)
if(caught)
+3
View File
@@ -161,6 +161,9 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr
/turf/open/openspace/icemoon/Initialize()
. = ..()
var/turf/T = below()
//I wonder if I should error here
if(!T)
return
if(T.turf_flags & NO_RUINS && protect_ruin)
ChangeTurf(replacement_turf, null, CHANGETURF_IGNORE_AIR)
return
@@ -55,7 +55,7 @@
/datum/proc/DoSearchVar(potential_container, container_name, recursive_limit = 64, search_time = world.time)
#ifdef REFERENCE_TRACKING_DEBUG
if(!found_refs)
if(!found_refs && SSgarbage.should_save_refs)
found_refs = list()
#endif
@@ -89,7 +89,8 @@
if(variable == src)
#ifdef REFERENCE_TRACKING_DEBUG
found_refs[varname] = TRUE
if(SSgarbage.should_save_refs)
found_refs[varname] = TRUE
#endif
log_reftracker("Found [type] \ref[src] in [datum_container.type]'s \ref[datum_container] [varname] var. [container_name]")
continue
@@ -107,7 +108,8 @@
//Check normal entrys
if(element_in_list == src)
#ifdef REFERENCE_TRACKING_DEBUG
found_refs[potential_cache] = TRUE
if(SSgarbage.should_save_refs)
found_refs[potential_cache] = TRUE
#endif
log_reftracker("Found [type] \ref[src] in list [container_name].")
continue
@@ -118,7 +120,8 @@
//Check assoc entrys
if(assoc_val == src)
#ifdef REFERENCE_TRACKING_DEBUG
found_refs[potential_cache] = TRUE
if(SSgarbage.should_save_refs)
found_refs[potential_cache] = TRUE
#endif
log_reftracker("Found [type] \ref[src] in list [container_name]\[[element_in_list]\]")
continue
@@ -46,7 +46,8 @@
if(atmosblock)
air_update_turf(TRUE, TRUE)
ConsumeTile()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOB, CELL_VIRUS_TABLE_GENERIC, 2, 2)
if(!QDELETED(src)) //Consuming our tile can in rare cases cause us to del
AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOB, CELL_VIRUS_TABLE_GENERIC, 2, 2)
/obj/structure/blob/proc/creation_action() //When it's created by the overmind, do this.
return
+5 -4
View File
@@ -359,9 +359,10 @@
var/datum/action/innate/cult/blood_spell/source
/obj/item/melee/blood_magic/New(loc, spell)
source = spell
uses = source.charges
health_cost = source.health_cost
if(spell)
source = spell
uses = source.charges
health_cost = source.health_cost
..()
/obj/item/melee/blood_magic/Destroy()
@@ -376,7 +377,7 @@
source.desc = source.base_desc
source.desc += "<br><b><u>Has [uses] use\s remaining</u></b>."
source.UpdateButtonIcon()
..()
return ..()
/obj/item/melee/blood_magic/attack_self(mob/living/user)
afterattack(user, user, TRUE)
@@ -484,7 +484,7 @@
/obj/item/ectoplasm/revenant/Destroy()
if(!QDELETED(revenant))
qdel(revenant)
..()
return ..()
//objectives
/datum/objective/revenant
@@ -305,10 +305,10 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module))
/obj/machinery/doomsday_device/Initialize()
. = ..()
owner = loc
if(!istype(owner))
if(!isAI(loc))
stack_trace("Doomsday created outside an AI somehow, shit's fucking broke. Anyway, we're just gonna qdel now. Go make a github issue report.")
qdel(src)
return INITIALIZE_HINT_QDEL
owner = loc
countdown = new(src)
/obj/machinery/doomsday_device/Destroy()
@@ -318,7 +318,7 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module))
SSshuttle.clearHostileEnvironment(src)
SSmapping.remove_nuke_threat(src)
set_security_level("red")
for(var/mob/living/silicon/robot/borg in owner.connected_robots)
for(var/mob/living/silicon/robot/borg in owner?.connected_robots)
borg.lamp_doom = FALSE
borg.toggle_headlamp(FALSE, TRUE) //forces borg lamp to update
owner?.doomsday_device = null
@@ -183,9 +183,10 @@
///Unregister the connected_machine (either when qdel this or the sender)
/obj/machinery/bluespace_vendor/proc/unregister_machine()
SIGNAL_HANDLER
UnregisterSignal(connected_machine, COMSIG_PARENT_QDELETING)
LAZYREMOVE(connected_machine.vendors, src)
connected_machine = null
if(connected_machine)
UnregisterSignal(connected_machine, COMSIG_PARENT_QDELETING)
LAZYREMOVE(connected_machine.vendors, src)
connected_machine = null
mode = BS_MODE_OFF
update_appearance()
@@ -117,10 +117,14 @@
*/
/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/unregister_signals(only_signals = FALSE)
SIGNAL_HANDLER
UnregisterSignal(linked_interface, COMSIG_PARENT_QDELETING)
UnregisterSignal(linked_input, COMSIG_PARENT_QDELETING)
UnregisterSignal(linked_output, COMSIG_PARENT_QDELETING)
UnregisterSignal(linked_moderator, COMSIG_PARENT_QDELETING)
if(linked_interface)
UnregisterSignal(linked_interface, COMSIG_PARENT_QDELETING)
if(linked_input)
UnregisterSignal(linked_input, COMSIG_PARENT_QDELETING)
if(linked_output)
UnregisterSignal(linked_output, COMSIG_PARENT_QDELETING)
if(linked_moderator)
UnregisterSignal(linked_moderator, COMSIG_PARENT_QDELETING)
for(var/obj/machinery/hypertorus/corner/corner in corners)
UnregisterSignal(corner, COMSIG_PARENT_QDELETING)
if(!only_signals)
@@ -89,7 +89,6 @@
if(gas_type)
FillToPressure(gas_type)
setPipingLayer(piping_layer)
QUEUE_SMOOTH(src)
QUEUE_SMOOTH_NEIGHBORS(src)
@@ -132,7 +131,6 @@
air_contents.assert_gas(gastype)
air_contents.gases[gastype][MOLES] += moles_to_add
air_contents.archive()
update_parents()
/obj/machinery/atmospherics/components/tank/process_atmos()
if(air_contents.react(src))
@@ -231,8 +229,6 @@
if(dir & initialize_directions & merger.members[src])
ToggleSidePort(dir)
update_parents()
///////////////////////////////////////////////////////////////////
// Appearance stuff
@@ -546,5 +542,6 @@
var/list/new_custom_materials = list()
new_custom_materials[material_end_product] = 20000
new_tank.set_custom_materials(new_custom_materials)
new_tank.on_construction(new_tank.pipe_color, new_tank.piping_layer)
to_chat(user, "<span class='notice'>[new_tank] has been sealed and is ready to accept gases.</span>")
qdel(src)
+3
View File
@@ -3,6 +3,9 @@
var/client/cl
/obj/effect/buildmode_line/New(client/C, atom/atom_a, atom/atom_b, linename)
if(!C || !atom_a || !atom_b)
stack_trace("Buildmode effect created with odd inputs")
return
name = linename
abstract_move(get_turf(atom_a))
I = image('icons/misc/mark.dmi', src, "line", 19.0)
+1 -1
View File
@@ -51,6 +51,7 @@
/obj/item/ctf/proc/reset_flag(capture = FALSE)
SIGNAL_HANDLER
STOP_PROCESSING(SSobj, src)
var/turf/our_turf = get_turf(src.reset)
if(!our_turf)
@@ -61,7 +62,6 @@
if(istype(mob_area, game_area))
if(!capture)
to_chat(M, span_userdanger("[src] has been returned to the base!"))
STOP_PROCESSING(SSobj, src)
return TRUE //so if called by a signal, it doesn't delete
//working with attack hand feels like taking my brain and putting it through an industrial pill press so i'm gonna be a bit liberal with the comments
+4 -1
View File
@@ -30,6 +30,9 @@
var/obj/structure/closet/supplypod/centcompod/linked_pod
/mob/living/simple_animal/pet/gondola/gondolapod/Initialize(mapload, pod)
if(!pod)
stack_trace("Gondola pod created with no pod")
return INITIALIZE_HINT_QDEL
linked_pod = pod
name = linked_pod.name
desc = linked_pod.desc
@@ -73,6 +76,6 @@
update_appearance()
/mob/living/simple_animal/pet/gondola/gondolapod/death()
qdel(linked_pod) //Will cause the open() proc for the linked supplypod to be called with the "broken" parameter set to true, meaning that it will dump its contents on death
QDEL_NULL(linked_pod) //Will cause the open() proc for the linked supplypod to be called with the "broken" parameter set to true, meaning that it will dump its contents on death
qdel(src)
..()
+6
View File
@@ -552,6 +552,9 @@
/obj/effect/pod_landingzone_effect/Initialize(mapload, obj/structure/closet/supplypod/pod)
. = ..()
if(!pod)
stack_trace("Pod landingzone effect created with no pod")
return INITIALIZE_HINT_QDEL
transform = matrix() * 1.5
animate(src, transform = matrix()*0.01, time = pod.delays[POD_TRANSIT]+pod.delays[POD_FALLING])
@@ -570,6 +573,9 @@
/obj/effect/pod_landingzone/Initialize(mapload, podParam, single_order = null, clientman)
. = ..()
if(!podParam)
stack_trace("Pod landingzone created with no pod")
return INITIALIZE_HINT_QDEL
if (ispath(podParam)) //We can pass either a path for a pod (as expressconsoles do), or a reference to an instantiated pod (as the centcom_podlauncher does)
podParam = new podParam() //If its just a path, instantiate it
pod = podParam
+3
View File
@@ -113,6 +113,9 @@ GLOBAL_LIST_INIT(hallucination_list, list(
/obj/effect/hallucination/simple/Initialize(mapload, mob/living/carbon/T)
. = ..()
if(!T)
stack_trace("A hallucination was created with no target")
return INITIALIZE_HINT_QDEL
target = T
current_image = GetImage()
if(target.client)
@@ -122,13 +122,14 @@
/obj/machinery/food_cart/obj_break(damage_flag)
. = ..()
pack_up()
if(cart_griddle)
if(!QDELETED(cart_griddle))
QDEL_NULL(cart_griddle)
if(cart_smartfridge)
if(!QDELETED(cart_smartfridge))
QDEL_NULL(cart_smartfridge)
if(cart_table)
if(!QDELETED(cart_table))
QDEL_NULL(cart_table)
QDEL_NULL(cart_tent)
if(!QDELETED(cart_tent))
QDEL_NULL(cart_tent)
/obj/effect/food_cart_stand
name = "food cart tent"
@@ -142,6 +142,8 @@
return TRUE
/obj/machinery/oven/proc/update_baking_audio()
if(!oven_loop)
return
if(!open && used_tray?.contents.len)
oven_loop.start()
else
@@ -174,7 +174,7 @@
/obj/machinery/processor/dump_inventory_contents()
. = ..()
if(!LAZYLEN(processor_contents))
processor_contents.Cut()
processor_contents = null
/obj/machinery/processor/container_resist_act(mob/living/user)
user.forceMove(drop_location())
+3
View File
@@ -51,6 +51,9 @@
// This is for adminspawn or map-placed growns. They get the default stats of their seed type.
seed = new seed()
seed.adjust_potency(50-seed.potency)
else if(!seed)
stack_trace("Grown object created without a seed. WTF")
return INITIALIZE_HINT_QDEL
pixel_x = base_pixel_x + rand(-5, 5)
pixel_y = base_pixel_y + rand(-5, 5)
+7 -4
View File
@@ -283,11 +283,14 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava)
new /obj/item/toy/balloon/corgi(thing)
else
openturfs += thing
//cake + knife to cut it!
var/turf/food_turf = get_turf(pick(table))
new /obj/item/kitchen/knife(food_turf)
var/obj/item/food/cake/birthday/iancake = new(food_turf)
iancake.desc = "Happy birthday, Ian!"
if(length(table))
var/turf/food_turf = get_turf(pick(table))
new /obj/item/kitchen/knife(food_turf)
var/obj/item/food/cake/birthday/iancake = new(food_turf)
iancake.desc = "Happy birthday, Ian!"
//some balloons! this picks an open turf and pops a few balloons in and around that turf, yay.
for(var/i in 1 to balloon_clusters)
var/turf/clusterspot = pick_n_take(openturfs)
+1 -1
View File
@@ -170,7 +170,7 @@ GLOBAL_LIST_EMPTY(total_extraction_beacons)
/obj/structure/extraction_point/Destroy()
GLOB.total_extraction_beacons -= src
..()
return ..()
/obj/effect/extraction_holder
name = "extraction holder"
+1 -1
View File
@@ -44,7 +44,7 @@
/mob/living/simple_animal/hostile/mining_drone/Initialize()
. = ..()
AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, vary = TRUE)
AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE)
stored_gun = new(src)
var/datum/action/innate/minedrone/toggle_light/toggle_light_action = new()
+2
View File
@@ -329,6 +329,8 @@
var/obj/effect/decal/cleanable/blood/B = locate() in T
if(!B)
B = new /obj/effect/decal/cleanable/blood/splatter(T, get_static_viruses())
if(QDELETED(B)) //Give it up
return
B.bloodiness = min((B.bloodiness + BLOOD_AMOUNT_PER_DECAL), BLOOD_POOL_MAX)
B.transfer_mob_blood_dna(src) //give blood info to the blood decal.
if(temp_blood_DNA)
@@ -458,7 +458,7 @@
/datum/species/jelly/luminescent/proc/update_glow(mob/living/carbon/C, intensity)
if(intensity)
glow_intensity = intensity
glow.set_light(glow_intensity, glow_intensity, C.dna.features["mcolor"])
glow.set_light_range_power_color(glow_intensity, glow_intensity, C.dna.features["mcolor"])
/obj/effect/dummy/luminescent_glow
name = "luminescent glow"
@@ -62,7 +62,8 @@
/obj/item/storage/backpack/snail/dropped(mob/user, silent)
. = ..()
emptyStorage()
qdel(src)
if(!QDELETED(src))
qdel(src)
/obj/item/storage/backpack/snail/Initialize()
. = ..()
+6
View File
@@ -83,6 +83,12 @@
release(TRUE, FALSE)
return
/obj/item/clothing/head/mob_holder/drone/Initialize(mapload, mob/living/M, worn_state, head_icon, lh_icon, rh_icon, worn_slot_flags = NONE)
//If we're not being put onto a drone, end it all
if(!isdrone(M))
return INITIALIZE_HINT_QDEL
return ..()
/obj/item/clothing/head/mob_holder/drone/deposit(mob/living/L)
. = ..()
if(!isdrone(L))
+1 -1
View File
@@ -1001,9 +1001,9 @@
return
/mob/living/silicon/ai/spawned/Initialize(mapload, datum/ai_laws/L, mob/target_ai)
. = ..()
if(!target_ai)
target_ai = src //cheat! just give... ourselves as the spawned AI, because that's technically correct
. = ..()
/mob/living/silicon/ai/proc/camera_visibility(mob/camera/ai_eye/moved_eye)
GLOB.cameranet.visibility(moved_eye, client, all_eyes, TRUE)
@@ -389,7 +389,7 @@
/mob/living/silicon/robot/proc/SetLockdown(state = TRUE)
// They stay locked down if their wire is cut.
if(wires.is_cut(WIRE_LOCKDOWN))
if(wires?.is_cut(WIRE_LOCKDOWN))
state = TRUE
if(state)
throw_alert("locked", /atom/movable/screen/alert/locked)
@@ -26,7 +26,7 @@
ADD_TRAIT(src, TRAIT_NOMOBSWAP, INNATE_TRAIT) //dont push me bitch
ADD_TRAIT(src, TRAIT_NO_TELEPORT, INNATE_TRAIT) //dont teleport me bitch
ADD_TRAIT(src, TRAIT_STRONG_GRABBER, INNATE_TRAIT) //strong arms bitch
AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, vary = TRUE)
AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE)
var/datum/customer_data/customer_info = SSrestaurant.all_customers[customer_data]
clothes_set = pick(customer_info.clothing_sets)
ai_controller = customer_info.ai_controller_used
@@ -100,7 +100,7 @@
/obj/structure/receiving_pad/New(loc, mob/living/simple_animal/hostile/guardian/healer/G)
. = ..()
if(G.guardiancolor)
if(G?.guardiancolor)
add_atom_colour(G.guardiancolor, FIXED_COLOUR_PRIORITY)
/obj/structure/receiving_pad/proc/disappear()
@@ -142,6 +142,8 @@ Difficulty: Hard
/// Slams the ground around the source throwing back enemies caught nearby, delay is for the radius increase
/proc/wendigo_slam(atom/source, range, delay, throw_range)
var/turf/orgin = get_turf(source)
if(!orgin)
return
var/list/all_turfs = RANGE_TURFS(range, orgin)
for(var/i = 0 to range)
playsound(orgin,'sound/effects/bamf.ogg', 600, TRUE, 10)
@@ -57,7 +57,8 @@
/mob/living/simple_animal/hostile/asteroid/curseblob/proc/check_for_target()
if(QDELETED(set_target) || set_target.stat != CONSCIOUS || z != set_target.z)
qdel(src)
if(!QDELETED(src))
qdel(src)
return TRUE
/mob/living/simple_animal/hostile/asteroid/curseblob/GiveTarget(new_target)
@@ -67,7 +67,6 @@
/obj/item/modular_computer/Destroy()
kill_program(forced = TRUE)
STOP_PROCESSING(SSobj, src)
QDEL_NULL(soundloop)
for(var/H in all_components)
var/obj/item/computer_hardware/CH = all_components[H]
if(CH.holder == src)
@@ -75,6 +74,8 @@
CH.holder = null
all_components.Remove(CH.device_type)
qdel(CH)
//Some components will actually try and interact with this, so let's do it later
QDEL_NULL(soundloop)
physical = null
return ..()
+8 -7
View File
@@ -256,17 +256,18 @@
if(malfai && operating)
malfai.malf_picker.processing_time = clamp(malfai.malf_picker.processing_time - 10,0,1000)
area.power_light = FALSE
area.power_equip = FALSE
area.power_environ = FALSE
area.power_change()
if(area)
area.power_light = FALSE
area.power_equip = FALSE
area.power_environ = FALSE
area.power_change()
QDEL_NULL(alarm_manager)
if(occupier)
malfvacate(1)
qdel(wires)
wires = null
if(wires)
QDEL_NULL(wires)
if(cell)
qdel(cell)
QDEL_NULL(cell)
if(terminal)
disconnect_terminal()
. = ..()
@@ -29,8 +29,12 @@
AddElement(/datum/element/connect_loc, loc_connections)
/obj/machinery/field/containment/Destroy()
field_gen_1.fields -= src
field_gen_2.fields -= src
if(field_gen_1)
field_gen_1.fields -= src
field_gen_1 = null
if(field_gen_2)
field_gen_2.fields -= src
field_gen_2 = null
CanAtmosPass = ATMOS_PASS_YES
air_update_turf(TRUE, FALSE)
return ..()
+1 -1
View File
@@ -98,7 +98,7 @@
QDEL_NULL(chambered)
if(azoom)
QDEL_NULL(azoom)
if(isatom(suppressed))
if(isatom(suppressed)) //SUPPRESSED IS USED AS BOTH A TRUE/FALSE AND AS A REF, WHAT THE FUCKKKKKKKKKKKKKKKKK
QDEL_NULL(suppressed)
return ..()
@@ -183,11 +183,12 @@
/obj/item/gun/energy/dueling/Destroy()
. = ..()
if(duel.gun_A == src)
duel.gun_A = null
if(duel.gun_B == src)
duel.gun_B = null
duel = null
if(duel)
if(duel.gun_A == src)
duel.gun_A = null
if(duel.gun_B == src)
duel.gun_B = null
duel = null
/obj/item/gun/energy/dueling/can_trigger_gun(mob/living/user)
. = ..()
+2 -1
View File
@@ -53,7 +53,8 @@
/obj/item/gun/magic/Initialize()
. = ..()
charges = max_charges
chambered = new ammo_type(src)
if(ammo_type)
chambered = new ammo_type(src)
if(can_charge)
START_PROCESSING(SSobj, src)
RegisterSignal(src, COMSIG_ITEM_RECHARGED, .proc/instant_recharge)
+4 -4
View File
@@ -168,16 +168,16 @@
var/wound_falloff_tile
///How much we want to drop the embed_chance value, if we can embed, per tile, for falloff purposes
var/embed_falloff_tile
var/static/list/projectile_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
/obj/projectile/Initialize()
. = ..()
decayedRange = range
if(embedding)
updateEmbedding()
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
AddElement(/datum/element/connect_loc, loc_connections)
AddElement(/datum/element/connect_loc, projectile_connections)
/obj/projectile/proc/Range()
range--
@@ -33,7 +33,7 @@
. = ..()
var/obj/item/beacon/teletarget = null
for(var/obj/machinery/computer/teleporter/com in GLOB.machines)
var/atom/target = com.target_ref.resolve()
var/atom/target = com.target_ref?.resolve()
if(target)
if(com.power_station && com.power_station.teleporter_hub && com.power_station.engaged)
teletarget = target
@@ -280,6 +280,7 @@
/obj/projectile/magic/locker/Destroy()
locker_suck = FALSE
RemoveElement(/datum/element/connect_loc, projectile_connections) //We do this manually so the forcemoves don't "hit" us. This behavior is kinda dumb, someone refactor this
for(var/atom/movable/AM in contents)
AM.forceMove(get_turf(src))
. = ..()
@@ -15,7 +15,7 @@
. = ..()
var/obj/item/ammo_casing/energy/gravity/repulse/C = loc
if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
power = min(C.gun.power, 15)
power = min(C.gun?.power, 15)
/obj/projectile/gravityrepulse/on_hit()
. = ..()
@@ -50,7 +50,7 @@
. = ..()
var/obj/item/ammo_casing/energy/gravity/attract/C = loc
if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
power = min(C.gun.power, 15)
power = min(C.gun?.power, 15)
/obj/projectile/gravityattract/on_hit()
. = ..()
@@ -84,7 +84,7 @@
. = ..()
var/obj/item/ammo_casing/energy/gravity/chaos/C = loc
if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
power = min(C.gun.power, 15)
power = min(C.gun?.power, 15)
/obj/projectile/gravitychaos/on_hit()
. = ..()
@@ -28,7 +28,7 @@
hal_target.client.images += fake_icon
/obj/projectile/hallucination/Destroy()
if(hal_target.client)
if(hal_target?.client)
hal_target.client.images -= fake_icon
QDEL_NULL(fake_icon)
return ..()
@@ -242,7 +242,8 @@
/obj/structure/puzzle_element/Moved()
. = ..()
source.validate()
if(source)
source.validate()
//Admin abuse version so you can pick the icon before it sets up
/obj/effect/sliding_puzzle/admin
+2 -1
View File
@@ -605,7 +605,8 @@
/obj/machinery/computer/shuttle/pod/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
. = ..()
possible_destinations += ";[port.id]_lavaland"
if(port)
possible_destinations += ";[port.id]_lavaland"
/**
* Signal handler for checking if we should lock or unlock escape pods accordingly to a newly set security level
+1 -2
View File
@@ -33,8 +33,7 @@
if(!mapload)
connect_to_shuttle(SSshuttle.get_containing_shuttle(src))
for(var/port_id in SSshuttle.stationary)
var/obj/docking_port/stationary/S = SSshuttle.stationary[port_id]
for(var/obj/docking_port/stationary/S as anything in SSshuttle.stationary)
if(S.id == shuttleId)
jumpto_ports[S.id] = TRUE
@@ -89,6 +89,9 @@
/obj/item/phylactery/Initialize(mapload, datum/mind/newmind)
. = ..()
if(!mind)
stack_trace("A phylactery was created with no target mind")
return INITIALIZE_HINT_QDEL
mind = newmind
name = "phylactery of [mind.name]"
@@ -145,7 +145,8 @@
source = _source
shape = loc
if(!istype(shape))
CRASH("shapeshift holder created outside mob/living")
stack_trace("shapeshift holder created outside mob/living")
return INITIALIZE_HINT_QDEL
stored = caster
if(stored.mind)
stored.mind.transfer_to(shape)
@@ -12,7 +12,7 @@
if(action?.owner)
var/mob/guy_who_needs_to_know = action.owner
to_chat(guy_who_needs_to_know, span_notice("The power of the spell dissipates from your hand."))
..()
return ..()
/obj/effect/proc_holder/spell/targeted/touch/proc/remove_hand(recharge = FALSE)
QDEL_NULL(attached_hand)
+1 -4
View File
@@ -34,12 +34,9 @@
*/
/obj/item/organ/external/Initialize(mapload, mob_sprite)
. = ..()
if(mob_sprite)
set_sprite(mob_sprite)
cache_key = generate_icon_cache()
/obj/item/organ/external/Insert(mob/living/carbon/reciever, special, drop_if_replaced)
var/obj/item/bodypart/limb = reciever.get_bodypart(zone)
@@ -75,7 +72,7 @@
/obj/item/organ/external/proc/get_overlays(list/overlay_list, image_dir, image_layer, body_type, image_color)
if(!sprite_datum)
return
if(HAS_TRAIT(owner, TRAIT_INVISIBLE_MAN))
if(owner && HAS_TRAIT(owner, TRAIT_INVISIBLE_MAN))
return
var/gender = (body_type == FEMALE) ? "f" : "m"
var/finished_icon_state = (sprite_datum.gender_specific ? gender : "m") + "_" + preference + "_" + sprite_datum.icon_state + mutant_bodyparts_layertext(image_layer)
+3
View File
@@ -438,6 +438,9 @@
/obj/structure/ethereal_crystal/Initialize(mapload, obj/item/organ/heart/ethereal/ethereal_heart)
. = ..()
if(!ethereal_heart)
stack_trace("Our crystal has no related heart")
return INITIALIZE_HINT_QDEL
src.ethereal_heart = ethereal_heart
ethereal_heart.owner.visible_message(span_notice("The crystals fully encase [ethereal_heart.owner]!"))
to_chat(ethereal_heart.owner, span_notice("You are encased in a huge crystal!"))
+4 -2
View File
@@ -37,6 +37,9 @@
#define UNIT_TEST_FAILED 1
#define UNIT_TEST_SKIPPED 2
#define TEST_DEFAULT 1
#define TEST_DEL_WORLD INFINITY
/// A trait source when adding traits through unit tests
#define TRAIT_SOURCE_UNIT_TESTS "unit_tests"
@@ -52,7 +55,7 @@
#include "connect_loc.dm"
#include "confusion.dm"
#include "crayons.dm"
#include "deletions.dm"
#include "create_and_destroy.dm"
#include "designs.dm"
#include "dynamic_ruleset_sanity.dm"
#include "egg_glands.dm"
@@ -62,7 +65,6 @@
#include "heretic_knowledge.dm"
#include "holidays.dm"
#include "hydroponics_harvest.dm"
#include "initialize_sanity.dm"
#include "keybinding_init.dm"
#include "machine_disassembly.dm"
#include "medical_wounds.dm"
@@ -0,0 +1,189 @@
///Delete one of every type, sleep a while, then check to see if anything has gone fucky
/datum/unit_test/create_and_destroy
//You absolutely must run last
priority = TEST_DEL_WORLD
/datum/unit_test/create_and_destroy/Run()
//We'll spawn everything here
var/turf/spawn_at = run_loc_floor_bottom_left
var/list/ignore = list(
//Never meant to be created, errors out the ass for mobcode reasons
/mob/living/carbon,
//Nother template type, doesn't like being created with no seed
/obj/item/food/grown,
//And another
/obj/item/slimecross/recurring,
//This should be obvious
/obj/machinery/doomsday_device,
//Yet more templates
/obj/machinery/restaurant_portal,
//Template type
/obj/effect/mob_spawn,
//Template type
/obj/structure/holosign/robot_seat,
//Say it with me now, type template
/obj/effect/mapping_helpers/component_injector,
//template type
/obj/effect/mapping_helpers/trait_injector,
//Singleton
/mob/dview,
//Template,
/obj/effect/mapping_helpers/custom_icon,
)
//This turf existing is an error in and of itself
ignore += typesof(/turf/baseturf_skipover)
ignore += typesof(/turf/baseturf_bottom)
//This demands a borg, so we'll let if off easy
ignore += typesof(/obj/item/modular_computer/tablet/integrated)
//This one demands a computer, ditto
ignore += typesof(/obj/item/modular_computer/processor)
//Needs special input, let's be nice
ignore += typesof(/obj/effect/abstract/proximity_checker)
//Very finiky, blacklisting to make things easier
ignore += typesof(/obj/item/poster/wanted)
//We can't pass a mind into this
ignore += typesof(/obj/item/phylactery)
//This expects a seed, we can't pass it
ignore += typesof(/obj/item/food/grown)
//Nothing to hallucinate if there's nothing to hallicinate
ignore += typesof(/obj/effect/hallucination)
//These want fried food to take on the shape of, we can't pass that in
ignore += typesof(/obj/item/food/deepfryholder)
//Can't pass in a thing to glow
ignore += typesof(/obj/effect/abstract/eye_lighting)
//It wants a lot more context then we have
ignore += typesof(/obj/effect/buildmode_line)
//We don't have a pod
ignore += typesof(/obj/effect/pod_landingzone_effect)
ignore += typesof(/obj/effect/pod_landingzone)
//We don't have a disease to pass in
ignore += typesof(/obj/effect/mapping_helpers/component_injector/infective)
//It's a trapdoor to nowhere
ignore += typesof(/obj/effect/mapping_helpers/trapdoor_placer)
//There's no shapeshift to hold
ignore += typesof(/obj/shapeshift_holder)
//No tauma to pass in
ignore += typesof(/mob/camera/imaginary_friend)
//No pod to gondola
ignore += typesof(/mob/living/simple_animal/pet/gondola/gondolapod)
//No heart to give
ignore += typesof(/obj/structure/ethereal_crystal)
//No linked console
ignore += typesof(/mob/camera/ai_eye/remote/base_construction)
//See above
ignore += typesof(/mob/camera/ai_eye/remote/shuttle_docker)
//Hangs a ref post invoke async, which we don't support. Could put a qdeleted check but it feels hacky
ignore += typesof(/obj/effect/anomaly/grav/high)
//See above
ignore += typesof(/obj/effect/timestop)
//Invoke async in init, skippppp
ignore += typesof(/mob/living/silicon/robot/model)
//This lad also sleeps
ignore += typesof(/obj/item/hilbertshotel)
//this boi spawns turf changing stuff, and it stacks and causes pain. Let's just not
ignore += typesof(/obj/effect/sliding_puzzle)
//Stacks baseturfs, can't be tested here
ignore += typesof(/obj/effect/temp_visual/lava_warning)
//Stacks baseturfs, can't be tested here
ignore += typesof(/obj/effect/landmark/ctf)
//Our system doesn't support it without warning spam from unregister calls on things that never registered
ignore += typesof(/obj/docking_port)
//Asks for a shuttle that may not exist, let's leave it alone
ignore += typesof(/obj/item/pinpointer/shuttle)
//This spawns beams as a part of init, which can sleep past an async proc. This hangs a ref, and fucks us. It's only a problem here because the beam sleeps with CHECK_TICK
ignore += typesof(/obj/structure/alien/resin/flower_bud)
//Needs a linked mecha
ignore += typesof(/obj/effect/skyfall_landingzone)
//Leads to errors as a consequence of the logic behind moving back to a tile that's moving you somewhere else
ignore += typesof(/obj/effect/mapping_helpers/component_injector/areabound)
//Expects a mob to holderize, we have nothing to give
ignore += typesof(/obj/item/clothing/head/mob_holder)
var/list/cached_contents = spawn_at.contents.Copy()
var/baseturf_count = length(spawn_at.baseturfs)
for(var/type_path in typesof(/atom/movable, /turf) - ignore) //No areas please
if(ispath(type_path, /turf))
spawn_at.ChangeTurf(type_path, /turf/baseturf_skipover)
//We change it back to prevent pain, please don't ask
spawn_at.ChangeTurf(/turf/open/floor/wood, /turf/baseturf_skipover)
if(baseturf_count != length(spawn_at.baseturfs))
Fail("[type_path] changed the amount of baseturfs we have [baseturf_count] -> [length(spawn_at.baseturfs)]")
baseturf_count = length(spawn_at.baseturfs)
else
var/atom/creation = new type_path(spawn_at)
if(QDELETED(creation))
continue
//Go all in
qdel(creation, force = TRUE)
//This will hold a ref to the last thing we process unless we set it to null
//Yes byond is fucking sinful
creation = null
//There's a lot of stuff that either spawns stuff in on create, or removes stuff on destroy. Let's cut it all out so things are easier to deal with
var/list/to_del = spawn_at.contents - cached_contents
if(length(to_del))
for(var/atom/to_kill in to_del)
qdel(to_kill)
//Hell code, we're bound to have ended the round somehow so let's stop if from ending while we work
SSticker.delay_end = TRUE
//Prevent the garbage subsystem from harddeling anything, if only to save time
SSgarbage.collection_timeout[GC_QUEUE_HARDDELETE] = 10000 HOURS
//Clear it, just in case
cached_contents.Cut()
//Now that we've qdel'd everything, let's sleep until the gc has processed all the shit we care about
var/time_needed = SSgarbage.collection_timeout[GC_QUEUE_CHECK]
var/start_time = world.time
var/garbage_queue_processed = FALSE
sleep(time_needed)
while(!garbage_queue_processed)
var/list/queue_to_check = SSgarbage.queues[GC_QUEUE_CHECK]
//How the hell did you manage to empty this? Good job!
if(!length(queue_to_check))
garbage_queue_processed = TRUE
break
var/list/oldest_packet = queue_to_check[1]
//Pull out the time we deld at
var/qdeld_at = oldest_packet[1]
//If we've found a packet that got del'd later then we finished, then all our shit has been processed
if(qdeld_at > start_time)
garbage_queue_processed = TRUE
break
if(world.time > start_time + time_needed + 8 MINUTES)
Fail("Something has gone horribly wrong, the garbage queue has been processing for well over 10 minutes. What the hell did you do")
break
//Immediately fire the gc right after
SSgarbage.next_fire = 1
//Unless you've seriously fucked up, queue processing shouldn't take "that" long. Let her run for a bit, see if anything's changed
sleep(20 SECONDS)
//Alright, time to see if anything messed up
var/list/cache_for_sonic_speed = SSgarbage.items
for(var/path in cache_for_sonic_speed)
var/datum/qdel_item/item = cache_for_sonic_speed[path]
if(item.failures)
Fail("[item.name] hard deleted [item.failures] times out of a total del count of [item.qdels]")
if(item.no_respect_force)
Fail("[item.name] failed to respect force deletion [item.no_respect_force] times out of a total del count of [item.qdels]")
if(item.no_hint)
Fail("[item.name] failed to return a qdel hint [item.no_hint] times out of a total del count of [item.qdels]")
cache_for_sonic_speed = SSatoms.BadInitializeCalls
for(var/path in cache_for_sonic_speed)
var/fails = cache_for_sonic_speed[path]
if(fails & BAD_INIT_NO_HINT)
Fail("[path] didn't return an Initialize hint")
if(fails & BAD_INIT_QDEL_BEFORE)
Fail("[path] qdel'd in New()")
if(fails & BAD_INIT_SLEPT)
Fail("[path] slept during Initialize()")
SSticker.delay_end = FALSE
//This shouldn't be needed, but let's be polite
SSgarbage.collection_timeout[GC_QUEUE_HARDDELETE] = 10 SECONDS
-7
View File
@@ -1,7 +0,0 @@
/// This is for regression tests of deletions that used to runtime.
/// This would ideally be replaced by Del The World, unit testing every single deletion.
/datum/unit_test/deletion_regressions
/datum/unit_test/deletion_regressions/Run()
qdel(new /obj/item/gun/energy/kinetic_accelerator/crossbow)
qdel(new /obj/item/gun/syringe/syndicate)
@@ -6,20 +6,34 @@
var/list/test_list = list()
var/list/test_assoc_list = list()
/atom/movable/ref_holder/Destroy()
test = null
test_list.Cut()
test_assoc_list.Cut()
return ..()
/atom/movable/ref_test
var/atom/movable/ref_test/self_ref
/atom/movable/ref_test/Destroy(force)
self_ref = null
return ..()
/datum/unit_test/find_reference_sanity/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Sanity check
victim.DoSearchVar(testbed, "Sanity Check", search_time = 1) //We increment search time to get around an optimization
TEST_ASSERT(!victim.found_refs.len, "The ref-tracking tool found a ref where none existed")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_baseline/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Set up for the first round of tests
testbed.test = victim
testbed.test_list += victim
@@ -30,10 +44,12 @@
TEST_ASSERT(victim.found_refs["test"], "The ref-tracking tool failed to find a regular value")
TEST_ASSERT(victim.found_refs[testbed.test_list], "The ref-tracking tool failed to find a list entry")
TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list value")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_exotic/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Second round, bit harder this time
testbed.overlays += victim
@@ -46,10 +62,12 @@
TEST_ASSERT(!victim.found_refs[testbed.overlays], "The ref-tracking tool found an overlays entry? That shouldn't be possible")
TEST_ASSERT(victim.found_refs[testbed.vis_contents], "The ref-tracking tool failed to find a vis_contents entry")
TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list key")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_esoteric/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Let's get a bit esoteric
victim.self_ref = victim
@@ -63,10 +81,12 @@
TEST_ASSERT(victim.found_refs["self_ref"], "The ref-tracking tool failed to find a self reference")
TEST_ASSERT(victim.found_refs[to_find], "The ref-tracking tool failed to find a nested list entry")
TEST_ASSERT(victim.found_refs[to_find_assoc], "The ref-tracking tool failed to find a nested assoc list entry")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_null_key_entry/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Calm before the storm
testbed.test_assoc_list = list(null = victim)
@@ -77,6 +97,8 @@
/datum/unit_test/find_reference_assoc_investigation/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Let's do some more complex assoc list investigation
var/list/to_find_in_key = list(victim)
testbed.test_assoc_list[to_find_in_key] = list("memes")
@@ -86,3 +108,4 @@
victim.DoSearchVar(testbed, "Fifth Run", search_time = 6)
TEST_ASSERT(victim.found_refs[to_find_in_key], "The ref-tracking tool failed to find a nested assoc list key")
TEST_ASSERT(victim.found_refs[to_find_null_assoc_nested], "The ref-tracking tool failed to find a null key'd nested assoc list entry")
SSgarbage.should_save_refs = FALSE
@@ -1,11 +0,0 @@
/datum/unit_test/initialize_sanity/Run()
if(length(SSatoms.BadInitializeCalls))
Fail("Bad Initialize() calls detected. Please read logs.")
var/list/init_failures_to_text = list(
"[BAD_INIT_QDEL_BEFORE]" = "Qdeleted Before Initialized",
"[BAD_INIT_DIDNT_INIT]" = "Did Not Initialize",
"[BAD_INIT_SLEPT]" = "Initialize() Slept",
"[BAD_INIT_NO_HINT]" = "No Initialize() Hint Returned",
)
for(var/failure in SSatoms.BadInitializeCalls)
log_world("[failure]: [init_failures_to_text["[SSatoms.BadInitializeCalls[failure]]"]]") // You like stacked brackets?
+37 -27
View File
@@ -24,7 +24,8 @@ GLOBAL_VAR(test_log)
/// The top right floor turf of the testing zone
var/turf/run_loc_floor_top_right
///The priority of the test, the larger it is the later it fires
var/priority = TEST_DEFAULT
//internal shit
var/focus = FALSE
var/succeeded = TRUE
@@ -33,6 +34,9 @@ GLOBAL_VAR(test_log)
var/static/datum/space_level/reservation
/proc/cmp_unit_test_priority(datum/unit_test/a, datum/unit_test/b)
return initial(a.priority) - initial(b.priority)
/datum/unit_test/New()
if (isnull(reservation))
var/datum/map_template/unit_tests/template = new
@@ -78,49 +82,55 @@ GLOBAL_VAR(test_log)
allocated += instance
return instance
/proc/RunUnitTest(test_path, list/test_results)
var/datum/unit_test/test = new test_path
GLOB.current_test = test
var/duration = REALTIMEOFDAY
test.Run()
duration = REALTIMEOFDAY - duration
GLOB.current_test = null
GLOB.failed_any_test |= !test.succeeded
var/list/log_entry = list("[test.succeeded ? "PASS" : "FAIL"]: [test_path] [duration / 10]s")
var/list/fail_reasons = test.fail_reasons
for(var/J in 1 to LAZYLEN(fail_reasons))
log_entry += "\tREASON #[J]: [fail_reasons[J]]"
var/message = log_entry.Join("\n")
log_test(message)
test_results[test_path] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = test_path)
qdel(test)
/proc/RunUnitTests()
CHECK_TICK
var/tests_to_run = subtypesof(/datum/unit_test)
var/list/tests_to_run = subtypesof(/datum/unit_test)
for (var/_test_to_run in tests_to_run)
var/datum/unit_test/test_to_run = _test_to_run
if (initial(test_to_run.focus))
tests_to_run = list(test_to_run)
break
tests_to_run = sortTim(tests_to_run, /proc/cmp_unit_test_priority)
var/list/test_results = list()
for(var/I in tests_to_run)
var/datum/unit_test/test = new I
GLOB.current_test = test
var/duration = REALTIMEOFDAY
test.Run()
duration = REALTIMEOFDAY - duration
GLOB.current_test = null
GLOB.failed_any_test |= !test.succeeded
var/list/log_entry = list("[test.succeeded ? "PASS" : "FAIL"]: [I] [duration / 10]s")
var/list/fail_reasons = test.fail_reasons
for(var/J in 1 to LAZYLEN(fail_reasons))
log_entry += "\tREASON #[J]: [fail_reasons[J]]"
var/message = log_entry.Join("\n")
log_test(message)
test_results[I] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = I)
qdel(test)
CHECK_TICK
for(var/unit_path in tests_to_run)
CHECK_TICK //We check tick first because the unit test we run last may be so expensive that checking tick will lock up this loop forever
RunUnitTest(unit_path, test_results)
var/file_name = "data/unit_tests.json"
fdel(file_name)
file(file_name) << json_encode(test_results)
SSticker.force_ending = TRUE
//We have to call this manually because del_text can preceed us, and SSticker doesn't fire in the post game
SSticker.standard_reboot()
/datum/map_template/unit_tests
name = "Unit Tests Zone"
+4 -1
View File
@@ -40,7 +40,10 @@
. = ..()
if(!turret)
return
turret.forceMove(get_turf(src))
var/turf/our_turf = get_turf(src)
if(!our_turf)
return
turret.forceMove(our_turf)
switch(dir)
if(NORTH)
turret.pixel_x = base_pixel_x
@@ -372,6 +372,9 @@
/obj/effect/skyfall_landingzone/Initialize(mapload, obj/vehicle/sealed/mecha/combat/mecha)
. = ..()
if(!mecha)
stack_trace("Skyfall landing zone created without mecha")
return INITIALIZE_HINT_QDEL
src.mecha = mecha
animate(src, alpha = 255, TOTAL_SKYFALL_LEAP_TIME/2, easing = CIRCULAR_EASING|EASE_OUT)
RegisterSignal(mecha, COMSIG_MOVABLE_MOVED, .proc/follow)
+2
View File
@@ -54,6 +54,8 @@
* Updates the value of the input and calls input_received on the connected component
*/
/datum/port/input/proc/set_input(value)
if(QDELETED(src)) //Pain
return
set_value(value)
if(trigger)
TRIGGER_CIRCUIT_COMPONENT(connected_component, src)