From 05d93f665a820f0b5559b78bb696385ad3bd16a2 Mon Sep 17 00:00:00 2001
From: Vi3trice <80771500+Vi3trice@users.noreply.github.com>
Date: Sun, 30 Oct 2022 11:54:51 -0400
Subject: [PATCH] [TM] Port TG Jump Point Search and SSpathfinder (#18984)
* move along move along
* Update bot.dm
* Diagonals are now more expensive
* Update path.dm
* Update parrot.dm
* Update path.dm
* Tweaks
* Fix cleanbot, add path safety
* Tweaked, added a safety, removed the previous one
* Update medbot.dm
* path.len isn't very safe as a whole, floorbots also had order of operations wrong
* Update medbot.dm
* clings not even once
* Back to the drawing board
* Update path.dm
* Make mules actually clear the drawn path.
* Make bots use step_towards unconditionally instead of flipping between step_to and Move
* Making extra sure the path is cleared. Somehow path was left over.
* Check for length as get_path_to is always true
* This and that
---
code/__DEFINES/turfs.dm | 6 +
code/__HELPERS/path.dm | 439 ++++++++++++++++++
code/controllers/subsystem/pathfinder.dm | 46 ++
.../datums/spell_targeting/reachable_turfs.dm | 2 +-
code/defines/procs/AStar.dm | 189 --------
code/game/atoms.dm | 27 +-
code/game/gamemodes/blob/theblob.dm | 2 +-
code/game/machinery/doors/airlock.dm | 4 +-
code/game/machinery/doors/windowdoor.dm | 2 +-
code/game/machinery/shieldgen.dm | 2 +-
code/game/objects/objs.dm | 3 -
code/game/objects/structures/girders.dm | 2 +-
code/game/objects/structures/grille.dm | 2 +-
code/game/objects/structures/morgue.dm | 2 +-
code/game/objects/structures/plasticflaps.dm | 4 +-
code/game/objects/structures/railings.dm | 4 +-
code/game/objects/structures/tables_racks.dm | 4 +-
code/game/objects/structures/window.dm | 4 +-
code/game/turfs/turf.dm | 25 +
.../changeling/powers/tiny_prick.dm | 2 +-
.../mob/living/simple_animal/bot/bot.dm | 55 +--
.../mob/living/simple_animal/bot/cleanbot.dm | 14 +-
.../mob/living/simple_animal/bot/floorbot.dm | 33 +-
.../mob/living/simple_animal/bot/medbot.dm | 18 +-
.../mob/living/simple_animal/bot/mulebot.dm | 9 +-
.../mob/living/simple_animal/parrot.dm | 8 +-
code/modules/surgery/surgery.dm | 4 +-
paradise.dme | 4 +-
28 files changed, 630 insertions(+), 286 deletions(-)
create mode 100644 code/__DEFINES/turfs.dm
create mode 100644 code/__HELPERS/path.dm
create mode 100644 code/controllers/subsystem/pathfinder.dm
delete mode 100644 code/defines/procs/AStar.dm
diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm
new file mode 100644
index 00000000000..8d6281f43c8
--- /dev/null
+++ b/code/__DEFINES/turfs.dm
@@ -0,0 +1,6 @@
+/// Turf will be passable if density is 0
+#define TURF_PATHING_PASS_DENSITY 0
+/// Turf will be passable depending on [CanPathfindPass] return value
+#define TURF_PATHING_PASS_PROC 1
+/// Turf is never passable
+#define TURF_PATHING_PASS_NO 2
diff --git a/code/__HELPERS/path.dm b/code/__HELPERS/path.dm
new file mode 100644
index 00000000000..c3ba6fac0a4
--- /dev/null
+++ b/code/__HELPERS/path.dm
@@ -0,0 +1,439 @@
+/**
+ * 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.
+ */
+
+/**
+ * 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.
+ * * diagonal_safety: ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary
+ */
+/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id = null, simulated_only = TRUE, turf/exclude, skip_first = TRUE, diagonal_safety = 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, diagonal_safety)
+ 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.
+ * We do early next.density check despite it being already checked in LinkBlockedWithAccess for short-circuit performance
+ */
+#define CAN_STEP(cur_turf, next) (next && !next.density && !(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
+ 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(force, ...)
+ 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/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
+ /// Ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary
+ var/diagonal_safety = TRUE
+
+/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, diagonal_safety)
+ 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
+ src.diagonal_safety = diagonal_safety
+
+/**
+ * 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.IsEmpty() && !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
+ for(var/I in open.L)
+ qdel(I)
+ open.L = null
+ qdel(open)
+
+ if(diagonal_safety)
+ path = diagonal_movement_safety()
+
+ 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
+
+/datum/pathfind/proc/diagonal_movement_safety()
+ if(length(path) < 2)
+ return
+ var/list/modified_path = list()
+
+ for(var/i in 1 to length(path) - 1)
+ var/turf/current_turf = path[i]
+ var/turf/next_turf = path[i+1]
+ var/movement_dir = get_dir(current_turf, next_turf)
+ if(!(movement_dir & (movement_dir - 1))) //cardinal movement, no need to verify
+ modified_path += current_turf
+ continue
+ //If default diagonal movement step is invalid, replace with alternative two steps
+ if(movement_dir & NORTH)
+ if(!CAN_STEP(current_turf,get_step(current_turf,NORTH)))
+ modified_path += current_turf
+ modified_path += get_step(current_turf, movement_dir & ~NORTH)
+ else
+ modified_path += current_turf
+ else
+ if(!CAN_STEP(current_turf,get_step(current_turf,SOUTH)))
+ modified_path += current_turf
+ modified_path += get_step(current_turf, movement_dir & ~SOUTH)
+ else
+ modified_path += current_turf
+ modified_path += path[length(path)]
+
+ return modified_path
+
+/**
+ * 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
+ *
+ * Assumes destinantion turf is non-dense - check and shortcircuit in code invoking this proc to avoid overhead.
+ *
+ * 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?
+ * * no_id: When true, doors with public access will count as impassible
+*/
+/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID, no_id = FALSE)
+ if(destination_turf.x != x && destination_turf.y != y) //diagonal
+ var/in_dir = get_dir(destination_turf,src) // eg. northwest (1+8) = 9 (00001001)
+ var/first_step_direction_a = in_dir & 3 // eg. north (1+8)&3 (0000 0011) = 1 (0000 0001)
+ var/first_step_direction_b = in_dir & 12 // eg. west (1+8)&12 (0000 1100) = 8 (0000 1000)
+
+ for(var/first_step_direction in list(first_step_direction_a, first_step_direction_b))
+ var/turf/midstep_turf = get_step(destination_turf, first_step_direction)
+ var/way_blocked = midstep_turf.density || LinkBlockedWithAccess(midstep_turf, caller, ID, no_id = no_id) || midstep_turf.LinkBlockedWithAccess(destination_turf, caller, ID, no_id = no_id)
+ if(!way_blocked)
+ return FALSE
+ return TRUE
+
+ var/actual_dir = get_dir(src, destination_turf)
+
+ /// These are generally cheaper than looping contents so they go first
+ switch(destination_turf.pathing_pass_method)
+ if(TURF_PATHING_PASS_DENSITY)
+ if(destination_turf.density)
+ return TRUE
+ if(TURF_PATHING_PASS_PROC)
+ if(!destination_turf.CanPathfindPass(ID, actual_dir, caller, no_id = no_id))
+ return TRUE
+ if(TURF_PATHING_PASS_NO)
+ return TRUE
+
+ // Source border object checks
+ for(var/obj/structure/window/iter_window in src)
+ if(!iter_window.CanPathfindPass(ID, actual_dir, no_id = no_id))
+ return TRUE
+
+ for(var/obj/machinery/door/window/iter_windoor in src)
+ if(!iter_windoor.CanPathfindPass(ID, actual_dir, no_id = no_id))
+ return TRUE
+
+ for(var/obj/structure/railing/iter_rail in src)
+ if(!iter_rail.CanPathfindPass(ID, actual_dir, no_id = no_id))
+ return TRUE
+
+ for(var/obj/machinery/door/firedoor/border_only/firedoor in src)
+ if(!firedoor.CanPathfindPass(ID, actual_dir, no_id = no_id))
+ return TRUE
+
+ // Destination blockers check
+ var/reverse_dir = get_dir(destination_turf, src)
+ for(var/obj/iter_object in destination_turf)
+ if(!iter_object.CanPathfindPass(ID, reverse_dir, caller, no_id = no_id))
+ return TRUE
+
+ for(var/mob/living/iter_mob in destination_turf)
+ if(!iter_mob.CanPathfindPass(ID, reverse_dir, caller, no_id = no_id))
+ return TRUE
+
+ return FALSE
+
+#undef CAN_STEP
+#undef STEP_NOT_HERE_BUT_THERE
diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm
new file mode 100644
index 00000000000..0dd3cc39354
--- /dev/null
+++ b/code/controllers/subsystem/pathfinder.dm
@@ -0,0 +1,46 @@
+SUBSYSTEM_DEF(pathfinder)
+ name = "Pathfinder"
+ init_order = INIT_ORDER_PATH
+ flags = SS_NO_FIRE
+ var/datum/flowcache/mobs
+ var/static/space_type_cache
+
+/datum/controller/subsystem/pathfinder/Initialize()
+ space_type_cache = typecacheof(/turf/space)
+ 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 = addtimer(CALLBACK(src, /datum/flowcache.proc/toolong, free), 150, TIMER_STOPPABLE)
+ flow[free] = t
+ flow[t] = M
+ return free
+ else
+ return 0
+
+/datum/flowcache/proc/toolong(l)
+ log_game("Pathfinder route took longer than 150 ticks, src bot [flow[flow[l]]]")
+ found(l)
+
+/datum/flowcache/proc/found(l)
+ deltimer(flow[l])
+ flow[l] = null
+ run -= 1
diff --git a/code/datums/spell_targeting/reachable_turfs.dm b/code/datums/spell_targeting/reachable_turfs.dm
index 3233ec807e4..df2de0204b9 100644
--- a/code/datums/spell_targeting/reachable_turfs.dm
+++ b/code/datums/spell_targeting/reachable_turfs.dm
@@ -9,7 +9,7 @@
if(length(locs) == max_targets) //we found 2 locations and thats all we need
break
var/turf/T = get_step(user, direction) //getting a loc in that direction
- if(AStar(user, T, /turf/proc/Distance, 1, simulated_only = FALSE)) // if a path exists, so no dense objects in the way its valid salid
+ if(length(get_path_to(user, T, max_distance = 1, simulated_only = FALSE))) // if a path exists, so no dense objects in the way its valid salid
locs += T
// pad with player location
diff --git a/code/defines/procs/AStar.dm b/code/defines/procs/AStar.dm
deleted file mode 100644
index f753e931fde..00000000000
--- a/code/defines/procs/AStar.dm
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
-A Star pathfinding algorithm
-Returns a list of tiles forming a path from A to B, taking dense objects as well as walls, and the orientation of
-windows along the route into account.
-Use:
-your_list = AStar(start location, end location, moving atom, distance proc, max nodes, maximum node depth, minimum distance to target, adjacent proc, atom id, turfs to exclude, check only simulated)
-
-Optional extras to add on (in order):
-Distance proc : the distance used in every A* calculation (length of path and heuristic)
-MaxNodes: The maximum number of nodes the returned path can be (0 = infinite)
-Maxnodedepth: The maximum number of nodes to search (default: 30, 0 = infinite)
-Mintargetdist: 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.
-Adjacent proc : returns the turfs to consider around the actually processed node
-Simulated only : whether to consider unsimulated turfs or not (used by some Adjacent proc)
-
-Also added 'exclude' turf to avoid travelling over; defaults to null
-
-Actual Adjacent procs :
-
- /turf/proc/reachableAdjacentTurfs : returns reachable turfs in cardinal directions (uses simulated_only)
-
- /turf/proc/reachableAdjacentAtmosTurfs : returns turfs in cardinal directions reachable via atmos
-
-*/
-
-//////////////////////
-//PathNode object
-//////////////////////
-
-//A* nodes variables
-/datum/pathnode
- var/turf/source //turf associated with the PathNode
- var/datum/pathnode/prevNode //link to the parent PathNode
- var/f //A* Node weight (f = g + h)
- var/g //A* movement cost variable
- var/h //A* heuristic variable
- var/nt //count the number of Nodes traversed
-
-/datum/pathnode/New(s,p,pg,ph,pnt)
- source = s
- prevNode = p
- g = pg
- h = ph
- f = g + h
- source.PNode = src
- nt = pnt
-
-/datum/pathnode/proc/calc_f()
- f = g + h
-
-//////////////////////
-//A* procs
-//////////////////////
-
-//the weighting function, used in the A* algorithm
-/proc/PathWeightCompare(datum/pathnode/a, datum/pathnode/b)
- return a.f - b.f
-
-//reversed so that the Heap is a MinHeap rather than a MaxHeap
-/proc/HeapPathWeightCompare(datum/pathnode/a, datum/pathnode/b)
- return b.f - a.f
-
-//wrapper that returns an empty list if A* failed to find a path
-/proc/get_path_to(caller, end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableAdjacentTurfs, id = null, turf/exclude = null, simulated_only = TRUE)
- var/list/path = AStar(caller, end, dist, maxnodes, maxnodedepth, mintargetdist, adjacent, id, exclude, simulated_only)
- if(!path)
- path = list()
- return path
-
-//the actual algorithm
-/proc/AStar(caller, end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableAdjacentTurfs, id = null, turf/exclude = null, simulated_only = TRUE)
- //sanitation
- var/start = get_turf(caller)
- if(!start)
- return null
-
- if(maxnodes)
- //if start turf is farther than maxnodes from end turf, no need to do anything
- if(call(start, dist)(end) > maxnodes)
- return null
- maxnodedepth = maxnodes //no need to consider path longer than maxnodes
-
- var/datum/heap/open = new /datum/heap(/proc/HeapPathWeightCompare) //the open list
- var/list/closed = new() //the closed list
- var/list/path = null //the returned path, if any
- var/datum/pathnode/cur //current processed turf
-
- //initialization
- open.Insert(new /datum/pathnode(start,null,0,call(start,dist)(end),0))
-
- //then run the main loop
- while(!open.IsEmpty() && !path)
- //get the lower f node on the open list
- cur = open.Pop() //get the lower f turf in the open list
- closed.Add(cur.source) //and tell we've processed it
-
- //if we only want to get near the target, check if we're close enough
- var/closeenough
- if(mintargetdist)
- closeenough = call(cur.source, dist)(end) <= mintargetdist
-
- //if too many steps, abandon that path
- if(maxnodedepth && (cur.nt > maxnodedepth))
- continue
-
- //found the target turf (or close enough), let's create the path to it
- if(cur.source == end || closeenough)
- path = new()
- path.Add(cur.source)
-
- while(cur.prevNode)
- cur = cur.prevNode
- path.Add(cur.source)
-
- break
-
- //get adjacents turfs using the adjacent proc, checking for access with id
- var/list/L = call(cur.source, adjacent)(caller, id, simulated_only)
- for(var/t in L)
- var/turf/T = t
- if(T == exclude || (T in closed))
- continue
-
- var/newg = cur.g + call(cur.source, dist)(T)
- if(!T.PNode) //is not already in open list, so add it
- open.Insert(new /datum/pathnode(T,cur,newg,call(T,dist)(end),cur.nt+1))
- else //is already in open list, check if it's a better way from the current turf
- if(newg < T.PNode.g)
- T.PNode.prevNode = cur
- T.PNode.g = newg
- T.PNode.calc_f()
- T.PNode.nt = cur.nt + 1
- open.ReSort(T.PNode)//reorder the changed element in the list
-
- //cleaning after us
- for(var/datum/pathnode/PN in open.L)
- PN.source.PNode = null
- for(var/t in closed)
- var/turf/T = t
- T.PNode = null
-
- //reverse the path to get it from start to finish
- if(path)
- for(var/i in 1 to path.len / 2)
- path.Swap(i, path.len - i + 1)
-
- return path
-
-//Returns adjacent turfs in cardinal directions that are reachable
-//simulated_only controls whether only simulated turfs are considered or not
-/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only)
- var/list/L = new()
- var/turf/simulated/T
-
- for(var/dir in GLOB.cardinal)
- T = get_step(src, dir)
- if(!T || (simulated_only && !istype(T)))
- continue
- if(!T.density && !LinkBlockedWithAccess(T, caller, ID))
- L.Add(T)
- return L
-
-//Returns adjacent turfs in cardinal directions that are reachable via atmos
-/turf/proc/reachableAdjacentAtmosTurfs()
- return atmos_adjacent_turfs
-
-/turf/proc/LinkBlockedWithAccess(turf/T, caller, ID)
- var/adir = get_dir(src, T)
- var/rdir = get_dir(T, src)
- var/atom/caller_atom = caller
- if(!istype(caller_atom))
- caller_atom = null
-
- for(var/obj/O in src)
- if(!(O.flags & ON_BORDER))
- continue //skip over things that are not on the edge of the turf
-
- if(!O.CanAStarPass(ID, adir))
- return TRUE
-
- for(var/obj/O in T)
- var/pass_through = FALSE
- if(caller_atom)
- pass_through = caller_atom.CanAStarPassTo(ID, adir, O)
- if(!O.CanAStarPass(ID, rdir, caller) && !pass_through)
- return TRUE
-
- return FALSE
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index dd3b62b28c4..5042410f877 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -10,6 +10,9 @@
///For handling persistent filters
var/list/filter_data
+ /// pass_flags that we are. If any of this matches a pass_flag on a moving thing, by default, we let them through.
+ var/pass_flags_self = NONE
+
var/list/blood_DNA
var/blood_color
var/last_bumped = 0
@@ -1156,10 +1159,10 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
/*
Checks whether this atom can traverse the destination object when used as source for AStar.
- This should only be used as an override to /obj/proc/CanAStarPass. Aka don't use this unless you can't change the object's proc.
+ This should only be used as an override to /obj/proc/CanPathfindPass. Aka don't use this unless you can't change the object's proc.
Returning TRUE here will override the above proc's result.
*/
-/atom/proc/CanAStarPassTo(ID, dir, obj/destination)
+/atom/proc/CanPathfindPassTo(ID, dir, obj/destination)
return FALSE
/** Call this when you want to present a renaming prompt to the user.
@@ -1262,3 +1265,23 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
return
. = density
density = new_value
+
+
+/**
+ * This proc is used for telling whether something can pass by this atom 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.
+ *
+ * For turfs this will only be used if pathing_pass_method is TURF_PATHING_PASS_PROC
+ *
+ * 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
+ * * no_id: When true, doors with public access will count as impassible
+ **/
+/atom/proc/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
+ if(caller && (caller.pass_flags & pass_flags_self))
+ return TRUE
+ . = !density
diff --git a/code/game/gamemodes/blob/theblob.dm b/code/game/gamemodes/blob/theblob.dm
index 735c29f5b36..8f4d2a54dce 100644
--- a/code/game/gamemodes/blob/theblob.dm
+++ b/code/game/gamemodes/blob/theblob.dm
@@ -51,7 +51,7 @@
/obj/structure/blob/CanAtmosPass(turf/T)
return !atmosblock
-/obj/structure/blob/CanAStarPass(ID, dir, caller)
+/obj/structure/blob/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = 0
if(ismovable(caller))
var/atom/movable/mover = caller
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index 26bd957f341..9b21043842c 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -1319,9 +1319,9 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
update_icon()
return 1
-/obj/machinery/door/airlock/CanAStarPass(obj/item/card/id/ID)
+/obj/machinery/door/airlock/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
//Airlock is passable if it is open (!density), bot has access, and is not bolted or welded shut)
- return !density || (check_access(ID) && !locked && !welded && arePowerSystemsOn())
+ return !density || (check_access(ID) && !locked && !welded && arePowerSystemsOn() && !no_id)
/obj/machinery/door/airlock/emag_act(mob/user)
if(!operating && density && arePowerSystemsOn() && !emagged)
diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm
index dc00f7ca1cb..239eb6f78ad 100644
--- a/code/game/machinery/doors/windowdoor.dm
+++ b/code/game/machinery/doors/windowdoor.dm
@@ -157,7 +157,7 @@
return 1
//used in the AStar algorithm to determinate if the turf the door is on is passable
-/obj/machinery/door/window/CanAStarPass(obj/item/card/id/ID, to_dir)
+/obj/machinery/door/window/CanPathfindPass(obj/item/card/id/ID, to_dir, no_id = FALSE)
return !density || (dir != to_dir) || (check_access(ID) && hasPower())
/obj/machinery/door/window/CheckExit(atom/movable/mover, turf/target)
diff --git a/code/game/machinery/shieldgen.dm b/code/game/machinery/shieldgen.dm
index 4ba44b31d1a..eea9854f19c 100644
--- a/code/game/machinery/shieldgen.dm
+++ b/code/game/machinery/shieldgen.dm
@@ -559,7 +559,7 @@
return FALSE
return ..(mover, target, height)
-/obj/machinery/shieldwall/syndicate/CanAStarPass(ID, to_dir, caller)
+/obj/machinery/shieldwall/syndicate/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
if(isliving(caller))
var/mob/living/M = caller
if("syndicate" in M.faction)
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 517c0b8aced..4ce0a65e23b 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -328,9 +328,6 @@ a {
/obj/proc/container_resist(mob/living)
return
-/obj/proc/CanAStarPass(ID, dir, caller)
- . = !density
-
/obj/proc/on_mob_move(dir, mob/user)
return
diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm
index 4e3ee42f466..955517dc758 100644
--- a/code/game/objects/structures/girders.dm
+++ b/code/game/objects/structures/girders.dm
@@ -384,7 +384,7 @@
else
return 0
-/obj/structure/girder/CanAStarPass(ID, dir, caller)
+/obj/structure/girder/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm
index e6f1103fb57..e8687b5e875 100644
--- a/code/game/objects/structures/grille.dm
+++ b/code/game/objects/structures/grille.dm
@@ -118,7 +118,7 @@
else
return !density
-/obj/structure/grille/CanAStarPass(ID, dir, caller)
+/obj/structure/grille/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
diff --git a/code/game/objects/structures/morgue.dm b/code/game/objects/structures/morgue.dm
index 8b59023dc16..a584293e981 100644
--- a/code/game/objects/structures/morgue.dm
+++ b/code/game/objects/structures/morgue.dm
@@ -264,7 +264,7 @@
return FALSE
-/obj/structure/tray/m_tray/CanAStarPass(ID, dir, caller)
+/obj/structure/tray/m_tray/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm
index 3bbdae2e3bf..5237c35c215 100644
--- a/code/game/objects/structures/plasticflaps.dm
+++ b/code/game/objects/structures/plasticflaps.dm
@@ -76,7 +76,7 @@
return ..()
-/obj/structure/plasticflaps/CanAStarPass(ID, to_dir, caller)
+/obj/structure/plasticflaps/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
if(isliving(caller))
if(isbot(caller))
return TRUE
@@ -86,7 +86,7 @@
return FALSE
var/atom/movable/M = caller
if(M && M.pulling)
- return CanAStarPass(ID, to_dir, M.pulling)
+ return CanPathfindPass(ID, to_dir, M.pulling)
return TRUE //diseases, stings, etc can pass
/obj/structure/plasticflaps/deconstruct(disassembled = TRUE)
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index 82f30b0717a..e5b7540d789 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -61,7 +61,7 @@
/obj/structure/railing/corner/CanPass()
return TRUE
-/obj/structure/railing/corner/CanAStarPass(ID, to_dir, caller)
+/obj/structure/railing/corner/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
return TRUE
/obj/structure/railing/corner/CheckExit()
@@ -86,7 +86,7 @@
return density
return FALSE
-/obj/structure/railing/CanAStarPass(ID, to_dir, caller)
+/obj/structure/railing/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
if(to_dir == dir)
return FALSE
if(ordinal_direction_check(to_dir))
diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm
index e7ecab3b8d5..8ecc6dd15a2 100644
--- a/code/game/objects/structures/tables_racks.dm
+++ b/code/game/objects/structures/tables_racks.dm
@@ -138,7 +138,7 @@
return TRUE
return FALSE
-/obj/structure/table/CanAStarPass(ID, dir, caller)
+/obj/structure/table/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
@@ -834,7 +834,7 @@
else
return 0
-/obj/structure/rack/CanAStarPass(ID, dir, caller)
+/obj/structure/rack/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index fe700c82697..6fb7dda2ee0 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -125,10 +125,10 @@
return 0
return 1
-/obj/structure/window/CanAStarPass(ID, to_dir)
+/obj/structure/window/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
if(!density)
return 1
- if((dir == FULLTILE_WINDOW_DIR) || (dir == to_dir))
+ if((dir == FULLTILE_WINDOW_DIR) || (dir == to_dir) || fulltile)
return 0
return 1
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 7134fd8049d..e4c42bb093c 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -39,6 +39,10 @@
var/list/blueprint_data //for the station blueprints, images of objects eg: pipes
+ /// How pathing algorithm will check if this turf is passable by itself (not including content checks). By default it's just density check.
+ /// WARNING: Currently to use a density shortcircuiting this does not support dense turfs with special allow through function
+ var/pathing_pass_method = TURF_PATHING_PASS_DENSITY
+
/turf/Initialize(mapload)
SHOULD_CALL_PARENT(FALSE)
if(initialized)
@@ -578,3 +582,24 @@
/turf/AllowDrop()
return TRUE
+
+/**
+ * Returns adjacent turfs to this turf that are reachable, in all cardinal directions
+ *
+ * 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?
+ * * no_id: When true, doors with public access will count as impassible
+*/
+/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only, no_id = FALSE)
+ var/static/space_type_cache = typecacheof(/turf/space)
+ . = list()
+
+ for(var/iter_dir in GLOB.cardinal)
+ var/turf/turf_to_check = get_step(src, iter_dir)
+ if(!turf_to_check || (simulated_only && space_type_cache[turf_to_check.type]))
+ continue
+ if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, caller, ID, no_id = no_id))
+ continue
+ . += turf_to_check
diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm
index 1a350f7ee1d..6fefb320f64 100644
--- a/code/modules/antagonists/changeling/powers/tiny_prick.dm
+++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm
@@ -41,7 +41,7 @@
user.hud_used.lingstingdisplay.invisibility = 101
/datum/action/changeling/sting/can_sting(mob/user, mob/target)
- if(!..() || !iscarbon(target) || !isturf(user.loc) || !AStar(user, target.loc, /turf/proc/Distance, cling.sting_range, simulated_only = 0))
+ if(!..() || !iscarbon(target) || !isturf(user.loc) || !length(get_path_to(user, target, max_distance = cling.sting_range, simulated_only = FALSE)))
return FALSE
if(!cling.chosen_sting)
to_chat(user, "We haven't prepared our sting yet!")
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index 8c03d7239da..b61758aa74b 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -193,6 +193,7 @@
data_hud.remove_hud_from(src)
GLOB.bots_list -= src
+ QDEL_NULL(path)
QDEL_NULL(Radio)
QDEL_NULL(access_card)
@@ -489,12 +490,12 @@ Movement proc for stepping a bot through a path generated through A-star.
Pass a positive integer as an argument to override a bot's default speed.
*/
/mob/living/simple_animal/bot/proc/bot_move(dest, move_speed)
- if(!dest || !path || path.len == 0) //A-star failed or a path/destination was not set.
+ if(!dest || !path || !length(path)) //A-star failed or a path/destination was not set.
set_path(null)
return FALSE
dest = get_turf(dest) //We must always compare turfs, so get the turf of the dest var if dest was originally something else.
- var/turf/last_node = get_turf(path[path.len]) //This is the turf at the end of the path, it should be equal to dest.
+ var/turf/last_node = get_turf(path[length(path)]) //This is the turf at the end of the path, it should be equal to dest.
if(get_turf(src) == dest) //We have arrived, no need to move again.
return TRUE
@@ -517,21 +518,12 @@ Pass a positive integer as an argument to override a bot's default speed.
if(!length(path))
return FALSE
- // Only one destination
- if(length(path) == 1)
- step_to(src, path[1])
- set_path(null)
-
- else
- // Move us slowly
- Move(path[1], get_dir(src, path[1]), BOT_STEP_DELAY)
- if(get_turf(src) == path[1]) //Successful move
- increment_path()
- tries = 0
- else
- tries++
- return FALSE
+ if(!step_towards(src, path[1]))
+ tries++
+ return FALSE
+ increment_path()
+ tries = 0
return TRUE
@@ -546,18 +538,18 @@ Pass a positive integer as an argument to override a bot's default speed.
var/datum/job/captain/All = new/datum/job/captain
access_card.access = All.get_access() // Give the bot temporary all access
- set_path(get_path_to(src, waypoint, /turf/proc/Distance_cardinal, 0, 200, id = access_card))
+ set_path(get_path_to(src, waypoint, 200, id = access_card))
calling_ai = caller //Link the AI to the bot!
ai_waypoint = waypoint
- if(path && path.len) //Ensures that a valid path is calculated!
+ if(path && length(path)) //Ensures that a valid path is calculated!
if(!on)
turn_on() //Saves the AI the hassle of having to activate a bot manually.
if(client)
reset_access_timer_id = addtimer(CALLBACK (src, .proc/bot_reset), 600, TIMER_OVERRIDE|TIMER_STOPPABLE) //if the bot is player controlled, they get the extra access for a limited time
- to_chat(src, "Priority waypoint set by [calling_ai] [caller]. Proceed to [end_area.name].
[path.len-1] meters to destination. You have been granted additional door access for 60 seconds.")
+ to_chat(src, "Priority waypoint set by [calling_ai] [caller]. Proceed to [end_area.name].
[length(path)-1] meters to destination. You have been granted additional door access for 60 seconds.")
if(message)
- to_chat(calling_ai, "[bicon(src)] [name] called to [end_area.name]. [path.len-1] meters to destination.")
+ to_chat(calling_ai, "[bicon(src)] [name] called to [end_area.name]. [length(path)-1] meters to destination.")
pathset = TRUE
mode = BOT_RESPONDING
tries = 0
@@ -640,27 +632,26 @@ Pass a positive integer as an argument to override a bot's default speed.
/mob/living/simple_animal/bot/proc/patrol_step()
- if(client) // In use by player, don't actually move.
+ if(client) // In use by player, don't actually move.
return
- if(loc == patrol_target) // reached target
+ if(loc == patrol_target) // reached target
//Find the next beacon matching the target.
if(!get_next_patrol_target())
find_patrol_target() //If it fails, look for the nearest one instead.
return
- else if(path.len > 0 && patrol_target) // valid path
- var/turf/next = path[1]
- if(next == loc)
+ else if(length(path) && patrol_target) // valid path
+ if(path[1] == loc)
increment_path()
return
- var/moved = bot_move(patrol_target)//step_towards(src, next) // attempt to move
+ var/moved = bot_move(patrol_target)//step_towards(src, next) // attempt to move
if(!moved) //Couldn't proceed the next step of the path BOT_STEP_MAX_RETRIES times
addtimer(CALLBACK(src, .proc/patrol_step_not_moved), 2)
- else // no path, so calculate new one
+ else // no path, so calculate new one
mode = BOT_START_PATROL
/mob/living/simple_animal/bot/proc/patrol_step_not_moved()
@@ -766,13 +757,13 @@ Pass a positive integer as an argument to override a bot's default speed.
// given an optional turf to avoid
/mob/living/simple_animal/bot/proc/calc_path(turf/avoid)
check_bot_access()
- set_path(get_path_to(src, patrol_target, /turf/proc/Distance_cardinal, 0, 120, id=access_card, exclude=avoid))
+ set_path(get_path_to(src, patrol_target, 120, id=access_card, exclude=avoid))
/mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid)
set waitfor = FALSE
check_bot_access()
- set_path(get_path_to(src, summon_target, /turf/proc/Distance_cardinal, 0, 150, id=access_card, exclude=avoid))
- if(!path.len) //Cannot reach target. Give up and announce the issue.
+ set_path(get_path_to(src, summon_target, 150, id=access_card, exclude=avoid))
+ if(!length(path)) //Cannot reach target. Give up and announce the issue.
speak("Summon command failed, destination unreachable.",radio_channel)
bot_reset()
@@ -785,7 +776,7 @@ Pass a positive integer as an argument to override a bot's default speed.
bot_reset()
return
- else if(path.len > 0 && summon_target) //Proper path acquired!
+ else if(length(path) && summon_target) //Proper path acquired!
var/turf/next = path[1]
if(next == loc)
increment_path()
@@ -1090,7 +1081,7 @@ Pass a positive integer as an argument to override a bot's default speed.
/mob/living/simple_animal/bot/proc/increment_path()
- if(!path || !path.len)
+ if(!path || !length(path))
return
var/image/I = path[path[1]]
if(I)
diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
index 7dbf8c5b228..2621042c9b8 100644
--- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
@@ -115,10 +115,15 @@
if(mode == BOT_PATROL)
bot_patrol()
+ if(target && loc == get_turf(target))
+ start_clean(target)
+ path = list()
+ target = null
+
if(target)
- if(!path || path.len == 0) //No path, need a new one
+ if(!path || !length(path)) //No path, need a new one
//Try to produce a path to the target, and ignore airlocks to which it has access.
- path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 0, 30, id=access_card)
+ path = get_path_to(src, target, 30, id=access_card)
if(!bot_move(target))
add_to_ignore(target)
target = null
@@ -130,11 +135,6 @@
mode = BOT_IDLE
return
- if(target && loc == target.loc)
- start_clean(target)
- path = list()
- target = null
-
oldloc = loc
/mob/living/simple_animal/bot/cleanbot/proc/get_targets()
diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm
index 93e1eb62a3a..f7224e87ca8 100644
--- a/code/modules/mob/living/simple_animal/bot/floorbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm
@@ -214,23 +214,6 @@
bot_patrol()
if(target)
- if(path.len == 0)
- if(!isturf(target))
- var/turf/TL = get_turf(target)
- path = get_path_to(src, TL, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = 0)
- else
- path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = 0)
-
- if(!bot_move(target))
- add_to_ignore(target)
- target = null
- mode = BOT_IDLE
- return
- else if( !bot_move(target) )
- target = null
- mode = BOT_IDLE
- return
-
if(loc == target || loc == target.loc)
if(istype(target, /obj/item/stack/tile/plasteel))
start_eattile(target)
@@ -251,6 +234,22 @@
path = list()
return
+ if(!length(path))
+ if(!isturf(target))
+ var/turf/TL = get_turf(target)
+ path = get_path_to(src, TL, 30, id=access_card,simulated_only = 0)
+ else
+ path = get_path_to(src, target, 30, id=access_card,simulated_only = 0)
+
+ if(!bot_move(target))
+ add_to_ignore(target)
+ target = null
+ mode = BOT_IDLE
+ return
+ else if(!bot_move(target))
+ target = null
+ mode = BOT_IDLE
+ return
oldloc = loc
diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm
index a608a1334e0..3aaab2c4a80 100644
--- a/code/modules/mob/living/simple_animal/bot/medbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/medbot.dm
@@ -322,7 +322,7 @@
return
//Patient has moved away from us!
- else if(patient && path.len && (get_dist(patient,path[path.len]) > 2))
+ else if(patient && length(path) && (get_dist(patient,path[length(path)]) > 2))
path = list()
mode = BOT_IDLE
last_found = world.time
@@ -331,21 +331,21 @@
soft_reset()
return
- if(patient && path.len == 0 && (get_dist(src,patient) > 1))
- path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,id=access_card)
+ if(patient && !length(path) && (get_dist(src,patient) > 1))
+ path = get_path_to(src, patient, 30,id=access_card)
mode = BOT_MOVING
- if(!path.len) //try to get closer if you can't reach the patient directly
- path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,1,id=access_card)
- if(!path.len) //Do not chase a patient we cannot reach.
+ if(!length(path)) //try to get closer if you can't reach the patient directly
+ path = get_path_to(src, patient, 30,1,id=access_card)
+ if(!length(path)) //Do not chase a patient we cannot reach.
soft_reset()
- if(path.len > 0 && patient)
- if(!bot_move(path[path.len]))
+ if(length(path) && patient)
+ if(!bot_move(path[length(path)]))
oldpatient = patient
soft_reset()
return
- if(path.len > 8 && patient)
+ if(length(path) > 8 && patient)
frustration++
if(auto_patrol && !stationary_mode && !patient)
diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm
index da50db1b2c7..eb917e0cb6f 100644
--- a/code/modules/mob/living/simple_animal/bot/mulebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm
@@ -80,6 +80,9 @@
QDEL_NULL(cell)
return ..()
+/mob/living/simple_animal/bot/mulebot/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id)
+ return FALSE
+
/mob/living/simple_animal/bot/mulebot/can_buckle()
return FALSE //no ma'am, you cannot buckle mulebots to chairs
@@ -465,7 +468,7 @@
/mob/living/simple_animal/bot/mulebot/call_bot()
..()
var/area/dest_area
- if(path && path.len)
+ if(path && length(path))
target = ai_waypoint //Target is the end point of the path, the waypoint set by the AI.
dest_area = get_area(target)
destination = format_text(dest_area.name)
@@ -514,6 +517,7 @@
var/turf/next = path[1]
reached_target = FALSE
if(next == loc)
+ increment_path()
path -= next
return
if(isturf(next))
@@ -521,6 +525,7 @@
var/moved = step_towards(src, next) // attempt to move
if(moved && oldloc!=loc) // successful move
blockcount = 0
+ increment_path()
path -= loc
if(destination == home_destination)
mode = BOT_GO_HOME
@@ -577,7 +582,7 @@
// given an optional turf to avoid
/mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null)
check_bot_access()
- set_path(get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 250, id=access_card, exclude=avoid))
+ set_path(get_path_to(src, target, 250, id=access_card, exclude=avoid))
// sets the current destination
// signals all beacons matching the delivery code
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index 8d642f54b45..4f7763ce742 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -414,7 +414,7 @@
parrot_state = PARROT_SWOOP|PARROT_RETURN
return
- var/list/path_to_take = get_path_to(src, get_turf(parrot_interest), /turf/proc/Distance_cardinal)
+ var/list/path_to_take = get_path_to(src, parrot_interest)
if(length(path_to_take) <= 1) // The target is below us
parrot_interest = null
parrot_state = PARROT_SWOOP|PARROT_RETURN
@@ -439,7 +439,7 @@
icon_state = "parrot_sit"
return
- var/list/path_to_take = get_path_to(src, get_turf(parrot_perch), /turf/proc/Distance_cardinal)
+ var/list/path_to_take = get_path_to(src, parrot_perch)
if(length(path_to_take) <= 1) // The target is below us
parrot_perch = null
parrot_state = PARROT_WANDER
@@ -544,7 +544,7 @@
var/turf/T = get_turf(O)
if(my_turf != T)
var/cache_id = "[my_turf.UID()]_[T.UID()]"
- computed_paths[cache_id] = computed_paths[cache_id] || get_path_to(src, T, /turf/proc/Distance_cardinal)
+ computed_paths[cache_id] = computed_paths[cache_id] || get_path_to(src, T)
if(!length(computed_paths[cache_id]))
continue
@@ -760,5 +760,5 @@
animate(held_item_icon, transform = m180)
underlays += held_item_icon
-/mob/living/simple_animal/parrot/CanAStarPassTo(ID, dir, obj/destination)
+/mob/living/simple_animal/parrot/CanPathfindPassTo(ID, dir, obj/destination)
return is_type_in_typecache(destination, desired_perches)
diff --git a/code/modules/surgery/surgery.dm b/code/modules/surgery/surgery.dm
index a456939c448..8b06870e2b7 100644
--- a/code/modules/surgery/surgery.dm
+++ b/code/modules/surgery/surgery.dm
@@ -474,12 +474,12 @@
var/germs = 0
for(var/mob/living/carbon/human/H in view(2, E.loc))//germs from people
- if(AStar(E.loc, H.loc, /turf/proc/Distance, 2, simulated_only = 0))
+ if(length(get_path_to(E.loc, H.loc, max_distance = 2, simulated_only = FALSE)))
if(!HAS_TRAIT(H, TRAIT_NOBREATH) && !H.wear_mask) //wearing a mask helps preventing people from breathing cooties into open incisions
germs += H.germ_level * 0.25
for(var/obj/effect/decal/cleanable/M in view(2, E.loc))//germs from messes
- if(AStar(E.loc, M.loc, /turf/proc/Distance, 2, simulated_only = 0))
+ if(length(get_path_to(E.loc, M.loc, 2, simulated_only = FALSE)))
germs++
if(tool && tool.blood_DNA && length(tool.blood_DNA)) //germs from blood-stained tools
diff --git a/paradise.dme b/paradise.dme
index 28f12e5b92e..8834b13034f 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -96,6 +96,7 @@
#include "code\__DEFINES\tgs.dm"
#include "code\__DEFINES\tgui.dm"
#include "code\__DEFINES\tools.dm"
+#include "code\__DEFINES\turfs.dm"
#include "code\__DEFINES\typeids.dm"
#include "code\__DEFINES\uplinks.dm"
#include "code\__DEFINES\vampire.dm"
@@ -126,6 +127,7 @@
#include "code\__HELPERS\matrices.dm"
#include "code\__HELPERS\mobs.dm"
#include "code\__HELPERS\names.dm"
+#include "code\__HELPERS\path.dm"
#include "code\__HELPERS\pronouns.dm"
#include "code\__HELPERS\qdel.dm"
#include "code\__HELPERS\radiation.dm"
@@ -265,6 +267,7 @@
#include "code\controllers\subsystem\npcpool.dm"
#include "code\controllers\subsystem\overlays.dm"
#include "code\controllers\subsystem\parallax.dm"
+#include "code\controllers\subsystem\pathfinder.dm"
#include "code\controllers\subsystem\persistent_data.dm"
#include "code\controllers\subsystem\profiler.dm"
#include "code\controllers\subsystem\radiation.dm"
@@ -533,7 +536,6 @@
#include "code\defines\vox_sounds.dm"
#include "code\defines\procs\admin.dm"
#include "code\defines\procs\announce.dm"
-#include "code\defines\procs\AStar.dm"
#include "code\defines\procs\radio.dm"
#include "code\defines\procs\records.dm"
#include "code\game\alternate_appearance.dm"