mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 20:48:56 +01:00
Del The World: Unit testing for hard deletes (#59612)
Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com>
This commit is contained in:
co-authored by
SteelSlayer
parent
0304206af3
commit
cd576ab519
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ..()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
..()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
. = ..()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 ..()
|
||||
|
||||
|
||||
@@ -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 ..()
|
||||
|
||||
@@ -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)
|
||||
. = ..()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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!"))
|
||||
|
||||
@@ -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
|
||||
@@ -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?
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user