Ported Jump-Point-Search pathing from TG (#31114)

This commit is contained in:
DamianX
2021-10-26 07:30:38 +02:00
committed by GitHub
parent cb50736d8e
commit 2ab8f80082
45 changed files with 596 additions and 73 deletions

View File

@@ -1,2 +1,2 @@
// Set a callback's thing_to_call to GLOBAL_PROC in order to call a global proc.
#define GLOBAL_PROC "fuck you duny"
#define GLOBAL_PROC "ryll is cool"

View File

@@ -28,6 +28,7 @@
#define SS_INIT_MINIMAP -23
#define SS_INIT_PERSISTENCE_MAP -98
#define SS_INIT_PERSISTENCE_MISC -99
#define SS_INIT_PATHFINDER -100
#define SS_INIT_DAYNIGHT -200
#define SS_PRIORITY_TIMER 1000

80
code/__HELPERS/heap.dm Normal file
View File

@@ -0,0 +1,80 @@
//////////////////////
//datum/heap object
//////////////////////
/datum/heap
var/list/L
var/cmp
/datum/heap/New(compare)
L = new()
cmp = compare
/datum/heap/Destroy(force, ...)
for(var/i in L) // because this is before the list helpers are loaded
qdel(i)
L = null
return ..()
/datum/heap/proc/is_empty()
return !length(L)
//insert and place at its position a new node in the heap
/datum/heap/proc/insert(atom/A)
L.Add(A)
swim(length(L))
//removes and returns the first element of the heap
//(i.e the max or the min dependant on the comparison function)
/datum/heap/proc/pop()
if(!length(L))
return 0
. = L[1]
L[1] = L[length(L)]
L.Cut(length(L))
if(length(L))
sink(1)
//Get a node up to its right position in the heap
/datum/heap/proc/swim(index)
var/parent = round(index * 0.5)
while(parent > 0 && (call(cmp)(L[index],L[parent]) > 0))
L.Swap(index,parent)
index = parent
parent = round(index * 0.5)
//Get a node down to its right position in the heap
/datum/heap/proc/sink(index)
var/g_child = get_greater_child(index)
while(g_child > 0 && (call(cmp)(L[index],L[g_child]) < 0))
L.Swap(index,g_child)
index = g_child
g_child = get_greater_child(index)
//Returns the greater (relative to the comparison proc) of a node children
//or 0 if there's no child
/datum/heap/proc/get_greater_child(index)
if(index * 2 > length(L))
return 0
if(index * 2 + 1 > length(L))
return index * 2
if(call(cmp)(L[index * 2],L[index * 2 + 1]) < 0)
return index * 2 + 1
else
return index * 2
//Replaces a given node so it verify the heap condition
/datum/heap/proc/resort(atom/A)
var/index = L.Find(A)
swim(index)
sink(index)
/datum/heap/proc/List()
. = L.Copy()

359
code/__HELPERS/path.dm Normal file
View File

@@ -0,0 +1,359 @@
/**
* This file contains the stuff you need for using JPS (Jump Point Search) pathing, an alternative to A* that skips
* over large numbers of uninteresting tiles resulting in much quicker pathfinding solutions. Mind that diagonals
* cost the same as cardinal moves currently, so paths may look a bit strange, but should still be optimal.
*/
/**
* This is the proc you use whenever you want to have pathfinding more complex than "try stepping towards the thing".
* If no path was found, returns an empty list, which is important for bots like medibots who expect an empty list rather than nothing.
*
* Arguments:
* * caller: The movable atom that's trying to find the path
* * end: What we're trying to path to. It doesn't matter if this is a turf or some other atom, we're gonna just path to the turf it's on anyway
* * max_distance: The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
* * mintargetdistance: Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example.
* * id: An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
* * simulated_only: Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
* * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
* * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures.
*/
/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE)
if(!caller || !get_turf(end))
return
var/l = SSpathfinder.mobs.getfree(caller)
while(!l)
stoplag(3)
l = SSpathfinder.mobs.getfree(caller)
var/list/path
var/datum/pathfind/pathfind_datum = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude)
path = pathfind_datum.search()
qdel(pathfind_datum)
SSpathfinder.mobs.found(l)
if(!path)
path = list()
if(length(path) > 0 && skip_first)
path.Cut(1,2)
return path
/**
* A helper macro to see if it's possible to step from the first turf into the second one, minding things like door access and directional windows.
* Note that this can only be used inside the [datum/pathfind][pathfind datum] since it uses variables from said datum.
* If you really want to optimize things, optimize this, cuz this gets called a lot.
*/
#define CAN_STEP(cur_turf, next) (next && !next.density && cur_turf.Adjacent(next) && !(simulated_only && SSpathfinder.space_type_cache[next.type]) && !cur_turf.LinkBlockedWithAccess(next,caller, id) && (next != avoid))
/// Another helper macro for JPS, for telling when a node has forced neighbors that need expanding
#define STEP_NOT_HERE_BUT_THERE(cur_turf, dirA, dirB) ((!CAN_STEP(cur_turf, get_step(cur_turf, dirA)) && CAN_STEP(cur_turf, get_step(cur_turf, dirB))))
/// The JPS Node datum represents a turf that we find interesting enough to add to the open list and possibly search for new tiles from
/datum/jps_node
/// The turf associated with this node
var/turf/tile
/// The node we just came from
var/datum/jps_node/previous_node
/// The A* node weight (f_value = number_of_tiles + heuristic)
var/f_value
/// The A* node heuristic (a rough estimate of how far we are from the goal)
var/heuristic
/// How many steps it's taken to get here from the start (currently pulling double duty as steps taken & cost to get here, since all moves incl diagonals cost 1 rn)
var/number_tiles
/// How many steps it took to get here from the last node
var/jumps
/// Nodes store the endgoal so they can process their heuristic without a reference to the pathfind datum
var/turf/node_goal
/datum/jps_node/New(turf/our_tile, datum/jps_node/incoming_previous_node, jumps_taken, turf/incoming_goal)
tile = our_tile
jumps = jumps_taken
if(incoming_goal) // if we have the goal argument, this must be the first/starting node
node_goal = incoming_goal
else if(incoming_previous_node) // if we have the parent, this is from a direct lateral/diagonal scan, we can fill it all out now
previous_node = incoming_previous_node
number_tiles = previous_node.number_tiles + jumps
node_goal = previous_node.node_goal
heuristic = get_dist(tile, node_goal)
f_value = number_tiles + heuristic
// otherwise, no parent node means this is from a subscan lateral scan, so we just need the tile for now until we call [datum/jps/proc/update_parent] on it
/datum/jps_node/Destroy()
previous_node = null
return ..()
/datum/jps_node/proc/update_parent(datum/jps_node/new_parent)
previous_node = new_parent
node_goal = previous_node.node_goal
jumps = get_dist(tile, previous_node.tile)
number_tiles = previous_node.number_tiles + jumps
heuristic = get_dist(tile, node_goal)
f_value = number_tiles + heuristic
/// TODO: Macro this to reduce proc overhead
/proc/HeapPathWeightCompare(datum/jps_node/a, datum/jps_node/b)
return b.f_value - a.f_value
/// The datum used to handle the JPS pathfinding, completely self-contained
/datum/pathfind
/// The thing that we're actually trying to path for
var/atom/movable/caller
/// The turf where we started at
var/turf/start
/// The turf we're trying to path to (note that this won't track a moving target)
var/turf/end
/// The open list/stack we pop nodes out from (TODO: make this a normal list and macro-ize the heap operations to reduce proc overhead)
var/datum/heap/open
///An assoc list that serves as the closed list & tracks what turfs came from where. Key is the turf, and the value is what turf it came from
var/list/sources
/// The list we compile at the end if successful to pass back
var/list/path
// general pathfinding vars/args
/// An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
var/obj/item/weapon/card/id/id
/// How far away we have to get to the end target before we can call it quits
var/mintargetdist = 0
/// I don't know what this does vs , but they limit how far we can search before giving up on a path
var/max_distance = 30
/// Space is big and empty, if this is TRUE then we ignore pathing through unsimulated tiles
var/simulated_only
/// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through
var/turf/avoid
/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid)
src.caller = caller
end = get_turf(goal)
open = new /datum/heap(/proc/HeapPathWeightCompare)
sources = new()
src.id = id
src.max_distance = max_distance
src.mintargetdist = mintargetdist
src.simulated_only = simulated_only
src.avoid = avoid
/**
* search() is the proc you call to kick off and handle the actual pathfinding, and kills the pathfind datum instance when it's done.
*
* If a valid path was found, it's returned as a list. If invalid or cross-z-level params are entered, or if there's no valid path found, we
* return null, which [/proc/get_path_to] translates to an empty list (notable for simple bots, who need empty lists)
*/
/datum/pathfind/proc/search()
start = get_turf(caller)
if(!start || !end)
stack_trace("Invalid A* start or destination")
return
if(start.z != end.z || start == end ) //no pathfinding between z levels
return
if(max_distance && (max_distance < get_dist(start, end))) //if start turf is farther than max_distance from end turf, no need to do anything
return
//initialization
var/datum/jps_node/current_processed_node = new (start, -1, 0, end)
open.insert(current_processed_node)
sources[start] = start // i'm sure this is fine
//then run the main loop
while(!open.is_empty() && !path)
if(!caller)
return
current_processed_node = open.pop() //get the lower f_value turf in the open list
if(max_distance && (current_processed_node.number_tiles > max_distance))//if too many steps, don't process that path
continue
var/turf/current_turf = current_processed_node.tile
for(var/scan_direction in list(EAST, WEST, NORTH, SOUTH))
lateral_scan_spec(current_turf, scan_direction, current_processed_node)
for(var/scan_direction in list(NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST))
diag_scan_spec(current_turf, scan_direction, current_processed_node)
CHECK_TICK
//we're done! reverse the path to get it from start to finish
if(path)
for(var/i = 1 to round(0.5 * length(path)))
path.Swap(i, length(path) - i + 1)
sources = null
qdel(open)
return path
/// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search]
/datum/pathfind/proc/unwind_path(datum/jps_node/unwind_node)
path = new()
var/turf/iter_turf = unwind_node.tile
path.Add(iter_turf)
while(unwind_node.previous_node)
var/dir_goal = get_dir(iter_turf, unwind_node.previous_node.tile)
for(var/i = 1 to unwind_node.jumps)
iter_turf = get_step(iter_turf,dir_goal)
path.Add(iter_turf)
unwind_node = unwind_node.previous_node
/**
* For performing lateral scans from a given starting turf.
*
* These scans are called from both the main search loop, as well as subscans for diagonal scans, and they treat finding interesting turfs slightly differently.
* If we're doing a normal lateral scan, we already have a parent node supplied, so we just create the new node and immediately insert it into the heap, ezpz.
* If we're part of a subscan, we still need for the diagonal scan to generate a parent node, so we return a node datum with just the turf and let the diag scan
* proc handle transferring the values and inserting them into the heap.
*
* Arguments:
* * original_turf: What turf did we start this scan at?
* * heading: What direction are we going in? Obviously, should be cardinal
* * parent_node: Only given for normal lateral scans, if we don't have one, we're a diagonal subscan.
*/
/datum/pathfind/proc/lateral_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node)
var/steps_taken = 0
var/turf/current_turf = original_turf
var/turf/lag_turf = original_turf
while(TRUE)
if(path)
return
lag_turf = current_turf
current_turf = get_step(current_turf, heading)
steps_taken++
if(!CAN_STEP(lag_turf, current_turf))
return
if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist)))
var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken)
sources[current_turf] = original_turf
if(parent_node) // if this is a direct lateral scan we can wrap up, if it's a subscan from a diag, we need to let the diag make their node first, then finish
unwind_path(final_node)
return final_node
else if(sources[current_turf]) // already visited, essentially in the closed list
return
else
sources[current_turf] = original_turf
if(parent_node && parent_node.number_tiles + steps_taken > max_distance)
return
var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list?
switch(heading)
if(NORTH)
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST))
interesting = TRUE
if(SOUTH)
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST))
interesting = TRUE
if(EAST)
if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST))
interesting = TRUE
if(WEST)
if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST))
interesting = TRUE
if(interesting)
var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken)
if(parent_node) // if we're a diagonal subscan, we'll handle adding ourselves to the heap in the diag
open.insert(newnode)
return newnode
/**
* For performing diagonal scans from a given starting turf.
*
* Unlike lateral scans, these only are called from the main search loop, so we don't need to worry about returning anything,
* though we do need to handle the return values of our lateral subscans of course.
*
* Arguments:
* * original_turf: What turf did we start this scan at?
* * heading: What direction are we going in? Obviously, should be diagonal
* * parent_node: We should always have a parent node for diagonals
*/
/datum/pathfind/proc/diag_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node)
var/steps_taken = 0
var/turf/current_turf = original_turf
var/turf/lag_turf = original_turf
while(TRUE)
if(path)
return
lag_turf = current_turf
current_turf = get_step(current_turf, heading)
steps_taken++
if(!CAN_STEP(lag_turf, current_turf))
return
if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist)))
var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken)
sources[current_turf] = original_turf
unwind_path(final_node)
return
else if(sources[current_turf]) // already visited, essentially in the closed list
return
else
sources[current_turf] = original_turf
if(parent_node.number_tiles + steps_taken > max_distance)
return
var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list?
var/datum/jps_node/possible_child_node // otherwise, did one of our lateral subscans turn up something?
switch(heading)
if(NORTHWEST)
if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST))
interesting = TRUE
else
possible_child_node = (lateral_scan_spec(current_turf, WEST) || lateral_scan_spec(current_turf, NORTH))
if(NORTHEAST)
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST))
interesting = TRUE
else
possible_child_node = (lateral_scan_spec(current_turf, EAST) || lateral_scan_spec(current_turf, NORTH))
if(SOUTHWEST)
if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST))
interesting = TRUE
else
possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, WEST))
if(SOUTHEAST)
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST))
interesting = TRUE
else
possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, EAST))
if(interesting || possible_child_node)
var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken)
open.insert(newnode)
if(possible_child_node)
possible_child_node.update_parent(newnode)
open.insert(possible_child_node)
if(possible_child_node.tile == end || (mintargetdist && (get_dist(possible_child_node.tile, end) <= mintargetdist)))
unwind_path(possible_child_node)
return
/**
* For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags
*
* Arguments:
* * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach
* * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf
* * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space?
*/
/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID)
var/actual_dir = get_dir(src, destination_turf)
for(var/obj/structure/window/iter_window in src)
if(!iter_window.CanAStarPass(ID, actual_dir))
return TRUE
for(var/obj/machinery/door/window/iter_windoor in src)
if(!iter_windoor.CanAStarPass(ID, actual_dir))
return TRUE
var/reverse_dir = get_dir(destination_turf, src)
for(var/obj/iter_object in destination_turf)
if(!iter_object.CanAStarPass(ID, reverse_dir, caller))
return TRUE
return FALSE
#undef CAN_STEP
#undef STEP_NOT_HERE_BUT_THERE

View File

@@ -0,0 +1,52 @@
var/datum/subsystem/pathfinder/SSpathfinder
/datum/subsystem/pathfinder
name = "Pathfinder"
init_order = SS_INIT_PATHFINDER
flags = SS_NO_FIRE
var/datum/flowcache/mobs
var/static/space_type_cache
/datum/subsystem/pathfinder/New()
NEW_SS_GLOBAL(SSpathfinder)
/datum/subsystem/pathfinder/Initialize()
space_type_cache = typesof(/turf/space) + typesof(/turf/simulated/open)
generate_type_list_cache(space_type_cache)
mobs = new(10)
return ..()
/datum/flowcache
var/lcount
var/run
var/free
var/list/flow
/datum/flowcache/New(n)
. = ..()
lcount = n
run = 0
free = 1
flow = new/list(lcount)
/datum/flowcache/proc/getfree(atom/M)
if(run < lcount)
run += 1
while(flow[free])
CHECK_TICK
free = (free % lcount) + 1
var/t = add_timer(new /callback(src, .proc/toolong, free), 15 SECONDS)
flow[free] = t
flow[t] = M
return free
else
return 0
/datum/flowcache/proc/toolong(l)
log_game("Pathfinder route took longer than 15 seconds, src bot [flow[flow[l]]]")
found(l)
/datum/flowcache/proc/found(l)
del_timer(flow[l])
flow[l] = null
run -= 1

View File

@@ -47,7 +47,9 @@ var/timer_id = 1
return id_str
#define del_timer(id) qdel(timers_by_id[id])
/proc/del_timer(id)
qdel(timers_by_id[id])
/datum/timer
var/callback/callback
var/when

View File

@@ -196,6 +196,7 @@
sound_damaged = 'sound/effects/stone_hit.ogg'
sound_destroyed = 'sound/effects/stone_crumble.ogg'
layer = TABLE_LAYER
pass_flags_self = PASSTABLE
var/obj/item/weapon/melee/soulblade/blade = null
var/lock_type = /datum/locking_category/buckle/bed
var/altar_task = ALTARTASK_NONE
@@ -345,7 +346,7 @@
var/mob/M = mover
if(M.flying)
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
else
return 0

View File

@@ -9,7 +9,8 @@ var/global/list/ghdel_profiling = list()
var/ghost_read = 1 // All ghosts can read
var/ghost_write = 0 // Only aghosts can write
var/blessed=0 // Chaplain did his thing. (set by bless() proc, which is called by holywater)
/// pass_flags that this atom has. If any of this matches a pass_flag on a moving thing, by default, we let them through.
var/pass_flags_self = NONE
var/flags = FPRINT
var/flow_flags = 0
var/list/fingerprints

View File

@@ -8,7 +8,6 @@
layer = BLOB_SHIELD_LAYER
spawning = 0
destroy_sound = "sound/effects/blobsplat.ogg"
icon_new = "strong"
icon_classic = "blob_idle"
@@ -21,7 +20,7 @@
return
/obj/effect/blob/shield/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(istype(mover) && mover.checkpass(PASSBLOB))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return 0

View File

@@ -19,6 +19,7 @@ var/list/blob_overminds = list()
anchored = 1
penetration_dampening = 17
mouse_opacity = 1
pass_flags_self = PASSBLOB
var/health = 20
var/maxhealth = 20
var/health_timestamp = 0
@@ -108,7 +109,7 @@ var/list/blob_overminds = list()
/obj/effect/blob/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSBLOB))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return 0

View File

@@ -341,6 +341,7 @@ var/global/list/datum/stack_recipe/snow_recipes = list (
anchored = 1.0
density = 1.0
health = 50.0
pass_flags_self = PASSTABLE
var/maxhealth = 50.0
materialtype = /obj/item/stack/sheet/snow
@@ -397,7 +398,7 @@ var/global/list/datum/stack_recipe/snow_recipes = list (
/obj/structure/window/barricade/snow/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)//So bullets will fly over and stuff.
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
else
return 0

View File

@@ -12,7 +12,7 @@
var/strapped = 0.0
throwpass = 1 //so Adjacent passes.
var/rating = 1 //Use this for upgrades some day
pass_flags_self = PASSTABLE
var/obj/machinery/computer/operating/computer = null
/obj/machinery/optable/New()
@@ -60,7 +60,7 @@
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
else
return 0

View File

@@ -7,7 +7,6 @@
// This code allows for airlocks to be controlled externally by setting an id_tag and comm frequency (disables ID access)
/obj/machinery/door/airlock
var/frequency
var/shockedby = list()
var/datum/radio_frequency/radio_connection

View File

@@ -22,6 +22,7 @@ var/list/apiaries_list = list()
icon_state = "hydrotray3"
density = 1
anchored = 1
pass_flags_self = PASSTABLE
var/apiary_icon = "apiary"
var/beezeez = 0//beezeez removes 1 toxic and adds 1 nutrilevel per cycle
var/nutrilevel = 0//consumed every round based on how many bees the apiary is sustaining.
@@ -128,8 +129,7 @@ var/list/apiaries_list = list()
/obj/machinery/apiary/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
else
return 0
@@ -725,4 +725,4 @@ var/list/apiaries_list = list()
#undef EXILE_RESTRICTION
#undef APIARY_WILDERNESS_NONE
#undef APIARY_WILDERNESS_WILD
#undef APIARY_WILDERNESS_DANGER
#undef APIARY_WILDERNESS_DANGER

View File

@@ -10,7 +10,7 @@
var/processing = 0
var/empproof = FALSE // For plasma glass builds
machine_flags = EMAGGABLE | SCREWTOGGLE | WRENCHMOVE | FIXED2WORK | MULTITOOL_MENU | SHUTTLEWRENCH
pass_flags_self = PASSMACHINE
use_auto_lights = 1
light_power_on = 1
light_range_on = 3
@@ -25,7 +25,7 @@
initialize()
/obj/machinery/computer/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(istype(mover) && mover.checkpass(PASSMACHINE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return ..()

View File

@@ -11,6 +11,7 @@
anchored = FALSE
density = TRUE
icon_state = "barrier0"
pass_flags_self = PASSTABLE
var/health = 140
var/maxhealth = 140
@@ -82,7 +83,7 @@
/obj/machinery/deployable/barrier/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)//So bullets will fly over and stuff.
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
else
return 0

View File

@@ -23,7 +23,7 @@ var/list/all_doors = list()
var/autoclose = 0
var/glass = 0
var/normalspeed = 1
pass_flags_self = PASSDOOR
machine_flags = SCREWTOGGLE
// for glass airlocks/opacity firedoors
@@ -328,6 +328,8 @@ var/list/all_doors = list()
/obj/machinery/door/New()
. = ..()
if(!opacity)
pass_flags_self |= PASSGLASS
all_doors += src
if(density)
@@ -377,7 +379,7 @@ var/list/all_doors = list()
open()
return ..()
/obj/machinery/door/proc/CanAStarPass(var/obj/item/weapon/card/id/ID)
/obj/machinery/door/CanAStarPass(var/obj/item/weapon/card/id/ID)
return !density || check_access(ID)

View File

@@ -521,6 +521,7 @@
anchored = 1
density = 1
luminosity = 3
pass_flags_self = PASSGLASS
var/needs_power = 0
var/active = 1
var/obj/machinery/shieldwallgen/gen_primary
@@ -606,7 +607,7 @@
if(!mover)
return
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return prob(20)
else
if (istype(mover, /obj/item/projectile))

View File

@@ -20,6 +20,7 @@ var/global/num_vending_terminals = 1
anchored = 1
density = 1
layer = OPEN_DOOR_LAYER //This is below BELOW_OBJ_LAYER because vendors can contain crates/closets
pass_flags_self = PASSMACHINE
var/health = 100
var/maxhealth = 100 //Kicking feature
var/active = 1 //No sales pitches if off!
@@ -207,7 +208,7 @@ var/global/num_vending_terminals = 1
to_chat(user, "<span class='notice'>Its small, red segmented display reads $[num2septext(currently_vending.price - credits_held)]</span>")
/obj/machinery/vending/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(istype(mover) && mover.checkpass(PASSMACHINE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if(seconds_electrified > 0)
if(istype(mover, /obj/item))

View File

@@ -11,6 +11,7 @@
anchored = 0
opacity = 0
mouse_opacity = 1 //So we can actually click these
pass_flags_self = PASSTABLE
var/list/welder_salvage = list(/obj/item/stack/sheet/plasteel,/obj/item/stack/sheet/metal,/obj/item/stack/rods)
var/list/wirecutters_salvage = list(/obj/item/stack/cable_coil)
var/list/crowbar_salvage
@@ -18,7 +19,7 @@
/obj/effect/decal/mecha_wreckage/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group)
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return ..()

View File

@@ -24,7 +24,7 @@
name = "resin"
desc = "Looks like some kind of slimy growth."
icon_state = "resin"
pass_flags_self = PASSGLASS
density = 1
opacity = 1
anchored = 1
@@ -163,7 +163,7 @@
/obj/effect/alien/resin/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group)
return 0
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return !opacity
return !density

View File

@@ -18,6 +18,7 @@
anchored = 1
density = 1
layer = ABOVE_DOOR_LAYER
pass_flags_self = PASSGLASS
var/icon_base
var/robot_compatibility
@@ -195,7 +196,7 @@
return 1
if(air_group || (height == 0))
return 1
if((mover.checkpass(PASSGLASS) || istype(mover, /obj/item/projectile/meteor) || mover.throwing == 1))
if((mover.checkpass(pass_flags_self) || istype(mover, /obj/item/projectile/meteor) || mover.throwing == 1))
return 1
else
return 0

View File

@@ -781,3 +781,19 @@ a {
H.updatehealth()
to_chat(AM, "<span class='[danger ? "danger" : "notice"]'>You step in \the [src]!</span>")
/**
* This proc is used for telling whether something can pass by this object in a given direction, for use by the pathfinding system.
*
* Trying to generate one long path across the station will call this proc on every single object on every single tile that we're seeing if we can move through, likely
* multiple times per tile since we're likely checking if we can access said tile from multiple directions, so keep these as lightweight as possible.
*
* Arguments:
* * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference
* * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions
* * caller- The movable we're checking pass flags for, if we're making any such checks
**/
/obj/proc/CanAStarPass(obj/item/weapon/card/id/ID, to_dir, atom/movable/caller)
if(istype(caller) && (caller.pass_flags & pass_flags_self))
return TRUE
. = !density

View File

@@ -15,6 +15,7 @@
opacity = 1 //Wood isn't transparent, the last time I checked
health = 60 //Fairly strong
layer = ABOVE_DOOR_LAYER
pass_flags_self = PASSGLASS
var/busy = 0 //Oh god fucking do_after's
var/materialtype = /obj/item/stack/sheet/wood
@@ -95,10 +96,9 @@
..() //Weapon checks for weapons without brute or burn damage type and grab check
/obj/structure/window/barricade/Cross(atom/movable/mover, turf/target, height = 1.5, air_group = 0)
if(air_group || !height) //The mover is an airgroup
return 1 //We aren't airtight, only exception to PASSGLASS
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if(get_dir(loc, target) == dir || get_dir(loc, mover) == dir)
return !density
@@ -147,10 +147,9 @@
return 1
/obj/structure/window/barricade/full/Cross(atom/movable/mover, turf/target, height = 1.5, air_group = 0)
if(air_group || !height) //The mover is an airgroup
return 1 //We aren't airtight, only exception to PASSGLASS
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return 0

View File

@@ -6,6 +6,7 @@
density = 1
anchored = 1
health = 0 //destroying the statue kills the mob within
pass_flags_self = PASSTABLE
var/intialTox = 0 //these are here to keep the mob from taking damage from things that logically wouldn't affect a rock
var/intialFire = 0 //it's a little sloppy I know but it was this or the GODMODE flag. Lesser of two evils.
var/intialBrute = 0
@@ -108,7 +109,7 @@
/obj/structure/closet/statue/Cross(atom/movable/mover, turf/target, height = 1.5, air_group = 0)
if(air_group || (height == 0))
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return ..()

View File

@@ -18,7 +18,7 @@
desc = "A chain link fence. Not as effective as a wall, but generally it keeps people out."
density = 1
anchored = 1
pass_flags_self = PASSGRILLE
icon = 'icons/obj/structures/fence.dmi'
icon_state = "straight"
@@ -157,7 +157,7 @@
/obj/structure/fence/Cross(atom/movable/mover, turf/target, height = 1.5, air_group = 0)
if(air_group || (height == 0))
return 1
if(istype(mover) && mover.checkpass(PASSGRILLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
else
if(istype(mover, /obj/item/projectile))

View File

@@ -7,6 +7,7 @@
anchored = 1
plane = ABOVE_HUMAN_PLANE
explosion_resistance = 5
pass_flags_self = PASSGLASS
var/airtight = 0
/obj/structure/plasticflaps/attackby(obj/item/I as obj, mob/user as mob)
@@ -41,7 +42,7 @@
to_chat(user, "It appears to be [anchored? "anchored to" : "unachored from"] the floor, [airtight? "and it seems to be airtight as well." : "but it does not seem to be airtight."]")
/obj/structure/plasticflaps/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return prob(60)
var/obj/structure/bed/B = mover
@@ -74,4 +75,4 @@
/obj/structure/plasticflaps/cultify()
new /obj/structure/grille/cult(get_turf(src))
..()
..()

View File

@@ -4,7 +4,6 @@
#define WINDOWSECURE 3
/obj/structure/window/full
name = "window"
var/base_state = "window" //Base icon for update_icon
icon_state = "window0" //Specifically for the map
@@ -15,6 +14,7 @@
cracked_base = "fcrack"
is_fulltile = TRUE
disperse_coeff = 0.95
pass_flags_self = PASSGLASS
/obj/structure/window/full/New(loc)
@@ -26,7 +26,7 @@
return 1
/obj/structure/window/full/Cross(atom/movable/mover, turf/target, height = 1.5, air_group = 0)
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
dim_beam(mover)
return 1
return 0

View File

@@ -5,7 +5,7 @@
var/state = 0
var/material = /obj/item/stack/sheet/metal
var/construction_length = 40
pass_flags_self = PASSGIRDER
/obj/structure/girder/wood
icon_state = "girder_wood"
name = "wooden girder"
@@ -13,7 +13,7 @@
construction_length = 20
/obj/structure/girder/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(istype(mover) && mover.checkpass(PASSGIRDER))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return ..()

View File

@@ -10,6 +10,7 @@
pressure_resistance = 5*ONE_ATMOSPHERE
layer = BELOW_OBJ_LAYER
explosion_resistance = 5
pass_flags_self = PASSGRILLE
var/health = 20 //Relatively "strong" since it's hard to dismantle via brute force
var/broken = 0
var/grille_material = /obj/item/stack/rods
@@ -170,7 +171,7 @@
/obj/structure/grille/Cross(atom/movable/mover, turf/target, height = 1.5, air_group = 0)
if(air_group || (height == 0))
return 1
if(istype(mover) && mover.checkpass(PASSGRILLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
else
if(istype(mover, /obj/item/projectile))

View File

@@ -134,7 +134,7 @@
opacity = 0
icon = 'icons/obj/inflatable.dmi'
icon_state = "wall"
pass_flags_self = PASSGLASS
var/undeploy_path = null
var/spawn_undeployed = TRUE
var/tmp/deflating = 0
@@ -250,7 +250,7 @@
/obj/structure/inflatable/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group)
return 0
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return !density

View File

@@ -7,6 +7,7 @@
anchored = 1.0
layer = LATTICE_LAYER
plane = ABOVE_PLATING_PLANE
pass_flags_self = PASSGRILLE
// flags = CONDUCT

View File

@@ -9,6 +9,7 @@
anchored = 1
sheet_type = /obj/item/stack/sheet/metal
sheet_amt = 1
pass_flags_self = PASSTABLE
var/mob_lock_type = /datum/locking_category/buckle/bed
var/buckle_range = 1 // The distance a spessman needs to be within in order
// to be able to use the buckle_in_out verb
@@ -25,7 +26,7 @@
/obj/structure/bed/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSTABLE)) //NOTE: This includes ALL chairs as well! Vehicles have their own override.
if(istype(mover) && mover.checkpass(pass_flags_self)) //NOTE: This includes ALL chairs as well! Vehicles have their own override.
return 1
return ..()

View File

@@ -20,6 +20,7 @@
anchored = 1.0
layer = TABLE_LAYER
throwpass = 1 //You can throw objects over this, despite its density.
pass_flags_self = PASSTABLE
var/parts = /obj/item/weapon/table_parts
var/flipped = 0
var/health = 100
@@ -311,7 +312,7 @@
var/mob/M = mover
if(M.flying)
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if(flipped)
if(get_dir(loc, target) == dir || get_dir(loc, mover) == dir)
@@ -352,7 +353,7 @@
/obj/structure/table/Uncross(atom/movable/mover as mob|obj, target as turf)
if(locate(/obj/effect/unwall_field) in loc) //Annoying workaround for this -kanef
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if(flow_flags & ON_BORDER)
if(target) //Are we doing a manual check to see
@@ -735,6 +736,7 @@
anchored = 1.0
throwpass = 1 //You can throw objects over this, despite its density.
layer = TABLE_LAYER //So items are always layered over it
pass_flags_self = PASSTABLE
var/parts = /obj/item/weapon/rack_parts
var/offset_step = 0
var/health = 20
@@ -789,7 +791,7 @@
/obj/structure/rack/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSTABLE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return !density

View File

@@ -15,7 +15,7 @@
anchored = FALSE
density = FALSE
dir = NORTH
pass_flags_self = PASSDOOR|PASSGLASS
var/obj/item/weapon/circuitboard/airlock/electronics = null
var/windoor_type = /obj/machinery/door/window
var/secure_type = /obj/machinery/door/window/brigdoor
@@ -46,7 +46,7 @@
icon_state = "[facing]_[secure ? "secure_":""]windoor_assembly[wired ? "02":"01"]"
/obj/structure/windoor_assembly/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(istype(mover) && (mover.checkpass(PASSDOOR|PASSGLASS)))
if(istype(mover) && mover.checkpass(pass_flags_self))
return TRUE
if(get_dir(target, mover) == dir) //Make sure looking at appropriate border
if(air_group)
@@ -56,7 +56,7 @@
return TRUE
/obj/structure/windoor_assembly/Uncross(atom/movable/mover, turf/target)
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return TRUE
if(flow_flags & ON_BORDER)
if(target) //Are we doing a manual check to see

View File

@@ -26,7 +26,7 @@ var/list/one_way_windows
var/sheetamount = 1 //Number of sheets needed to build this window (determines how much shit is spawned via Destroy())
var/reinforced = 0 //Used for deconstruction steps
penetration_dampening = 1
pass_flags_self = PASSGLASS
var/obj/abstract/Overlays/damage_overlay
var/image/oneway_overlay
var/cracked_base = "crack"
@@ -186,7 +186,7 @@ var/list/one_way_windows
/obj/structure/window/Uncross(var/atom/movable/mover, var/turf/target)
if(locate(/obj/effect/unwall_field) in loc) //Annoying workaround for this
return 1
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if(flow_flags & ON_BORDER)
if(target) //Are we doing a manual check to see
@@ -201,7 +201,7 @@ var/list/one_way_windows
/obj/structure/window/Cross(atom/movable/mover, turf/target, height = 0)
if(locate(/obj/effect/unwall_field) in loc) //Annoying workaround for this
return 1
if(istype(mover) && mover.checkpass(PASSGLASS))//checking for beam dispersion both in and out, since beams do not trigger Uncross.
if(istype(mover) && mover.checkpass(pass_flags_self))//checking for beam dispersion both in and out, since beams do not trigger Uncross.
if((get_dir(loc, target) & dir) || (get_dir(loc, mover) & dir) || (get_dir(loc, target) & reverse_direction(dir)) || (get_dir(loc, mover) & reverse_direction(dir)))
dim_beam(mover)
return 1

View File

@@ -109,7 +109,7 @@
anchored = TRUE
throwpass = FALSE // This thing is the size of a wall, you can't throw past it.
circuitboard = /obj/item/weapon/circuitboard/fishwall
pass_flags_self = PASSGLASS
tank_type = "wall"
water_capacity = 500 // This thing fills an entire tile,5 large beakers worth
max_fish = 10 // Plenty of room for a lot of fish
@@ -124,7 +124,7 @@
food_level = MAX_FOOD
/obj/machinery/fishtank/wall/Cross(atom/movable/mover, turf/target, height = 1.5, air_group = 0) // Prevents airflow. Copied from windows.
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return TRUE
return FALSE
@@ -829,4 +829,4 @@
if(anchored)
connect_to_network()
else
disconnect_from_network()
disconnect_from_network()

View File

@@ -18,6 +18,7 @@
)
starting_materials = list() //Makes the new datum
allowed_types = list(/obj/item/stack/ore)
pass_flags_self = PASSGLASS
var/stack_amt = 50 //Amount to stack before releasing
var/obj/item/weapon/card/id/inserted_id
var/credits = 0
@@ -161,6 +162,6 @@
/obj/machinery/mineral/ore_redemption/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(air_group)
return 0
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return !opacity
return !density

View File

@@ -6,16 +6,8 @@
var/list/path = list()
/mob/living/clickbot/ClickOn(var/atom/A, var/params)
make_astar_path(A)
/mob/living/clickbot/make_astar_path(var/atom/target, var/callback = new /callback(src, .get_astar_path))
AStar(src, callback, get_turf(src), target, /turf/proc/CardinalTurfsWithAccess, /turf/proc/Distance, 30, 30, debug = TRUE)
/mob/living/clickbot/get_astar_path(var/list/L)
.=..()
if(.)
path = .
path = get_path_to(src, A)
pathers += src
/mob/living/clickbot/process_astar_path()
if(gcDestroyed || stat == DEAD)
@@ -30,7 +22,6 @@
return FALSE
return TRUE
/mob/living/clickbot/drop_astar_path()
path.Cut()
.=..()

View File

@@ -3,6 +3,7 @@
/mob
plane = MOB_PLANE
pass_flags_self = PASSMOB
var/said_last_words = 0 // All mobs can now whisper as they die
var/list/alerts = list()

View File

@@ -2,7 +2,7 @@
if(air_group || (height==0))
return 1
if(istype(mover) && mover.checkpass(PASSMOB))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if(ismob(mover))

View File

@@ -74,7 +74,7 @@
/obj/structure/lattice/CanFallThru(atom/movable/mover as mob|obj, turf/target as turf)
if(target.z >= z)
return TRUE // We don't block sideways or upward movement.
else if(istype(mover) && mover.checkpass(PASSGRILLE))
else if(istype(mover) && mover.checkpass(pass_flags_self))
return TRUE // Anything small enough to pass a grille will pass a lattice
if(!isturf(mover.loc))
return FALSE // Only let loose floor items fall. No more snatching things off people's hands.
@@ -83,6 +83,6 @@
// So you'll slam when falling onto a grille
/obj/structure/lattice/CheckFall(var/atom/movable/falling_atom)
if(istype(falling_atom) && falling_atom.checkpass(PASSGRILLE))
if(istype(falling_atom) && falling_atom.checkpass(pass_flags_self))
return FALSE
return falling_atom.fall_impact(src)
return falling_atom.fall_impact(src)

View File

@@ -16,6 +16,7 @@
anchored = 1
use_power = 0
idle_power_usage = 0
pass_flags_self = PASSGLASS
var/count_power = 0 //How much power have we produced SO FAR this count?
var/tick_power = 0 //How much power did we produce last count?
var/power_efficiency = 1 //Based on parts
@@ -85,7 +86,7 @@
/obj/machinery/power/treadmill/Uncross(var/atom/movable/mover, var/turf/target)
if(locate(/obj/effect/unwall_field) in loc) //Annoying workaround for this -kanef
return 1
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if((flow_flags & ON_BORDER) && (mover.dir == dir))
powerwalk(mover)
@@ -95,7 +96,7 @@
/obj/machinery/power/treadmill/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(locate(/obj/effect/unwall_field) in loc) //Annoying workaround for this -kanef
return 1
if(istype(mover) && mover.checkpass(PASSGLASS))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
if(get_dir(loc, target) == dir || get_dir(loc, mover) == dir)
if(air_group)

View File

@@ -9,6 +9,7 @@ var/global/list/rnd_machines = list()
density = 1
anchored = 1
use_power = 1
pass_flags_self = PASSMACHINE
var/busy = 0
var/hacked = 0
var/disabled = 0
@@ -61,7 +62,7 @@ var/global/list/rnd_machines = list()
shocked--
/obj/machinery/r_n_d/Cross(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(istype(mover) && mover.checkpass(PASSMACHINE))
if(istype(mover) && mover.checkpass(pass_flags_self))
return 1
return ..()

View File

@@ -93,6 +93,7 @@
#include "code\__HELPERS\game.dm"
#include "code\__HELPERS\gender.dm"
#include "code\__HELPERS\global_lists.dm"
#include "code\__HELPERS\heap.dm"
#include "code\__HELPERS\icons.dm"
#include "code\__HELPERS\iteration.dm"
#include "code\__HELPERS\lists.dm"
@@ -101,6 +102,7 @@
#include "code\__HELPERS\maths.dm"
#include "code\__HELPERS\mobs.dm"
#include "code\__HELPERS\names.dm"
#include "code\__HELPERS\path.dm"
#include "code\__HELPERS\priority_queue.dm"
#include "code\__HELPERS\sanitize_values.dm"
#include "code\__HELPERS\text.dm"
@@ -239,6 +241,7 @@
#include "code\controllers\subsystem\mob.dm"
#include "code\controllers\subsystem\nanoui.dm"
#include "code\controllers\subsystem\objects.dm"
#include "code\controllers\subsystem\pathfinder.dm"
#include "code\controllers\subsystem\pathing.dm"
#include "code\controllers\subsystem\pipenet.dm"
#include "code\controllers\subsystem\plant.dm"