Retrofit TG pathfinder with upstream improvements (#25583)

* Retrofit TG pathfinder with upstream improvements

* i know how things work

* don't use HAS_TRAIT for things that aren't traits

* unique filenames

* i'm just a goofy lil moron

* use new block syntax

* apparently values don't need clamping with new block syntax?

* silence invalid JPS dest runtime

* fix runtime passing ID instead of access list
This commit is contained in:
warriorstar-orion
2024-07-08 11:48:43 -04:00
committed by GitHub
parent 62dc3d57dd
commit ca93f6bc77
34 changed files with 1324 additions and 580 deletions
+25
View File
@@ -0,0 +1,25 @@
// Define set that decides how an atom will be scanned for astar things
/// If set, we make the assumption that CanPathfindPass() will NEVER return FALSE unless density is true
#define CANPATHFINDPASS_DENSITY 0
/// If this is set, we bypass density checks and always call the proc
#define CANPATHFINDPASS_ALWAYS_PROC 1
/**
* 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.
* 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, simulated_only, pass_info, avoid) (next && !next.density && !(simulated_only && SSpathfinder.space_type_cache[next.type]) && !cur_turf.LinkBlockedWithAccess(next, pass_info) && (next != avoid))
#define DIAGONAL_DO_NOTHING NONE
#define DIAGONAL_REMOVE_ALL 1
#define DIAGONAL_REMOVE_CLUNKY 2
// Set of delays for path_map reuse
// The longer you go, the higher the risk of invalid paths
#define MAP_REUSE_INSTANT (0)
#define MAP_REUSE_SNAPPY (0.5 SECONDS)
#define MAP_REUSE_FAST (2 SECONDS)
#define MAP_REUSE_SLOW (20 SECONDS)
// Longest delay, so any maps older then this will be discarded from the subsystem cache
#define MAP_REUSE_SLOWEST (60 SECONDS)
+1
View File
@@ -101,6 +101,7 @@
#define FIRE_PRIORITY_AIR 20
#define FIRE_PRIORITY_NPC 20
#define FIRE_PRIORITY_CAMERA 20
#define FIRE_PRIORITY_PATHFINDING 23
#define FIRE_PRIORITY_PROCESS 25
#define FIRE_PRIORITY_THROWING 25
#define FIRE_PRIORITY_SPACEDRIFT 30
+6
View File
@@ -4,3 +4,9 @@
#define TURF_PATHING_PASS_PROC 1
/// Turf is never passable
#define TURF_PATHING_PASS_NO 2
/// Returns a list of turfs similar to CORNER_BLOCK but with offsets
#define CORNER_BLOCK_OFFSET(corner, width, height, offset_x, offset_y) (block(corner.x + offset_x, corner.y + offset_y, corner.z, corner.x + (width - 1) + offset_x, corner.y + (height - 1) + offset_y))
/// Returns a list of around us
#define TURF_NEIGHBORS(turf) (CORNER_BLOCK_OFFSET(turf, 3, 3, -1, -1) - turf)
+13
View File
@@ -50,6 +50,12 @@
} while(FALSE)
// Generic listoflist safe add and removal macros:
///If value is a list, wrap it in a list so it can be used with list add/remove operations
#define LIST_VALUE_WRAP_LISTS(value) (islist(value) ? list(value) : value)
///Add an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun
#define UNTYPED_LIST_ADD(list, item) (list += LIST_VALUE_WRAP_LISTS(item))
//Returns a list in plain english as a string
/proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" )
var/total = length(input)
@@ -870,3 +876,10 @@
else
used_key_list[input_key] = 1
return input_key
/// Turns an associative list into a flat list of keys
/proc/assoc_to_keys(list/input)
var/list/keys = list()
for(var/key in input)
UNTYPED_LIST_ADD(keys, key)
return keys
-445
View File
@@ -1,445 +0,0 @@
#define GET_DIST_REAL(turf_a, turf_b) sqrt((turf_a.x - turf_b.x) ** 2 + (turf_a.y - turf_b.y) ** 2)
/**
* 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
/// Multiplier for making diagonals more expensive
var/diagonal_move_mult = 1
/datum/jps_node/New(turf/our_tile, datum/jps_node/incoming_previous_node, jumps_taken, turf/incoming_goal, is_diagonal)
tile = our_tile
jumps = jumps_taken
diagonal_move_mult = (is_diagonal ? SQRT_2 : 1)
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_REAL(tile, node_goal)
f_value = heuristic + previous_node.number_tiles + (jumps * diagonal_move_mult)
// 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_REAL(tile, previous_node.tile)
number_tiles = previous_node.number_tiles + jumps
heuristic = GET_DIST_REAL(tile, node_goal)
f_value = heuristic + previous_node.number_tiles + (jumps * diagonal_move_mult)
/// 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(GLOBAL_PROC_REF(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_REAL(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_REAL(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_REAL(current_turf, end) <= mintargetdist)))
var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken, is_diagonal = TRUE)
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, is_diagonal = TRUE)
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_REAL(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
#undef GET_DIST_REAL
+305
View File
@@ -0,0 +1,305 @@
/**
* 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.
*/
/// A helper macro for JPS, for telling when a node has forced neighbors that need expanding
/// Only usable in the context of the jps datum because of the datum vars it relies on
#define STEP_NOT_HERE_BUT_THERE(cur_turf, dirA, dirB) ((!CAN_STEP(cur_turf, get_step(cur_turf, dirA), simulated_only, pass_info, avoid) && CAN_STEP(cur_turf, get_step(cur_turf, dirB), simulated_only, pass_info, avoid)))
/// 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(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
/proc/HeapPathWeightCompare(datum/jps_node/a, datum/jps_node/b)
return b.f_value - a.f_value
/datum/pathfind/jps
/// The movable we are pathing
var/atom/movable/caller
/// 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
/// The list we compile at the end if successful to pass back
var/list/path
///An assoc list that serves as the closed list. Key is the turf, points to true if we've seen it before
var/list/found_turfs
/// How far away we have to get to the end target before we can call it quits
var/mintargetdist = 0
/// If we should delete the first step in the path or not. Used often because it is just the starting tile
var/skip_first = FALSE
///Defines how we handle diagonal moves. See __DEFINES/path.dm
var/diagonal_handling = DIAGONAL_REMOVE_CLUNKY
/datum/pathfind/jps/proc/setup(atom/movable/caller, list/access, max_distance, simulated_only, avoid, list/datum/callback/on_finish, atom/goal, mintargetdist, skip_first, diagonal_handling)
src.caller = caller
src.pass_info = new(caller, access)
src.max_distance = max_distance
src.simulated_only = simulated_only
src.avoid = avoid
src.on_finish = on_finish
src.mintargetdist = mintargetdist
src.skip_first = skip_first
src.diagonal_handling = diagonal_handling
end = get_turf(goal)
open = new /datum/heap(/proc/HeapPathWeightCompare)
found_turfs = list()
/datum/pathfind/jps/Destroy(force)
. = ..()
caller = null
end = null
open = null
/datum/pathfind/jps/start()
start = start || get_turf(caller)
. = ..()
if(!.)
return .
if(!get_turf(end))
// Something has asynchronously removed our original target
return FALSE
if(start.z != end.z || start == end) //no pathfinding between z levels
return FALSE
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 FALSE
var/datum/jps_node/current_processed_node = new (start, -1, 0, end)
open.Insert(current_processed_node)
found_turfs[start] = TRUE // i'm sure this is fine
return TRUE
/datum/pathfind/jps/search_step()
. = ..()
if(!.)
return .
if(QDELETED(caller))
return FALSE
while(!open.IsEmpty() && !path)
var/datum/jps_node/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)
// Stable, we'll just be back later
if(TICK_CHECK)
return TRUE
return TRUE
/datum/pathfind/jps/finished()
//we're done! turn our reversed path (end to start) into a path (start to end)
found_turfs = null
QDEL_NULL(open)
var/list/path = src.path || list()
path = reverselist(path)
switch(diagonal_handling)
if(DIAGONAL_REMOVE_CLUNKY)
path = remove_clunky_diagonals(path, pass_info, simulated_only, avoid)
if(DIAGONAL_REMOVE_ALL)
path = remove_diagonals(path, pass_info, simulated_only, avoid)
if(skip_first && length(path) > 0)
path.Cut(1,2)
hand_back(path)
return ..()
/// 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/jps/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 in 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/jps/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
var/datum/can_pass_info/pass_info = src.pass_info
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, simulated_only, pass_info, avoid))
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)
found_turfs[current_turf] = TRUE
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(found_turfs[current_turf]) // already visited, essentially in the closed list
return
else
found_turfs[current_turf] = TRUE
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/jps/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
var/datum/can_pass_info/pass_info = src.pass_info
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, simulated_only, pass_info, avoid))
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)
found_turfs[current_turf] = TRUE
unwind_path(final_node)
return
else if(found_turfs[current_turf]) // already visited, essentially in the closed list
return
else
found_turfs[current_turf] = TRUE
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
+395
View File
@@ -0,0 +1,395 @@
/**
* 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.
* It will yield until a path is returned, using magic
*
* 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.
* * access: A list representing what access we have and what doors we can open.
* * simulated_only: Whether we consider tur fs 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_handling: defines how we handle diagonal moves. see __DEFINES/path.dm
*/
/proc/get_path_to(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, access=list(), simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_handling=DIAGONAL_REMOVE_CLUNKY)
var/list/hand_around = list()
// We're guarenteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
var/datum/callback/await = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around))
if(!SSpathfinder.pathfind(caller, end, max_distance, mintargetdist, access, simulated_only, exclude, skip_first, diagonal_handling, await))
return list()
UNTIL(length(hand_around))
var/list/return_val = hand_around[1]
if(!islist(return_val) || (QDELETED(caller) || QDELETED(end))) // It's trash, just hand back empty to make it easy
return list()
return return_val
/**
* POTENTIALLY cheaper version of get_path_to
* This proc generates a path map for the end atom's turf, which allows us to cheaply do pathing operations "at" it
* Generation is significantly SLOWER then get_path_to, but if many things are/might be pathing at something then it is much faster
* Runs the risk of returning an suboptimal or INVALID PATH if the delay between map creation and use is too long
*
* If no path was found, returns an empty list, which is important for bots like medibots who expect an empty list rather than nothing.
* It will yield until a path is returned, using magic
*
* 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.
* * age: How old a path map can be before we'll avoid reusing it. Use the defines found in [code/__DEFINES/path.dm], values larger then MAP_REUSE_SLOWEST will be discarded
* * access: A list representing what access we have and what doors we can open.
* * simulated_only: Whether we consider tur fs 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_swarm_path_to(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, age = MAP_REUSE_INSTANT, access = list(), simulated_only = TRUE, turf/exclude, skip_first=TRUE)
var/list/hand_around = list()
// We're guarenteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
var/datum/callback/await = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around))
if(!SSpathfinder.swarmed_pathfind(caller, end, max_distance, mintargetdist, age, access, simulated_only, exclude, skip_first, await))
return list()
UNTIL(length(hand_around))
var/list/return_val = hand_around[1]
if(!islist(return_val) || (QDELETED(caller) || QDELETED(end))) // It's trash, just hand back empty to make it easy
return list()
return return_val
/proc/get_sssp(atom/movable/caller, max_distance = 30, access = list(), simulated_only = TRUE, turf/exclude)
var/list/hand_around = list()
// We're guarenteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
var/datum/callback/await = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around))
if(!SSpathfinder.build_map(caller, get_turf(caller), max_distance, access, simulated_only, exclude, await))
return null
UNTIL(length(hand_around))
var/datum/path_map/return_val = hand_around[1]
if(!istype(return_val, /datum/path_map) || (QDELETED(caller))) // It's trash, just hand back null to make it easy
return null
return return_val
/// Uses funny pass by reference bullshit to take the output created by pathfinding, and insert it into a return list
/// We'll be able to use this return list to tell a sleeping proc to continue execution
/proc/pathfinding_finished(list/return_list, hand_back)
// We use += here to behave nicely with lists
return_list += LIST_VALUE_WRAP_LISTS(hand_back)
/// The datum used to handle the JPS pathfinding, completely self-contained
/datum/pathfind
/// The turf we started at
var/turf/start
// general pathfinding vars/args
/// Limits 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
/// The callbacks to invoke when we're done working, passing in the completed product
/// Invoked in order
var/list/datum/callback/on_finish
/// Datum that holds the canpass info of this pathing attempt. This is what CanPathfindPass sees
var/datum/can_pass_info/pass_info
/datum/pathfind/Destroy(force)
. = ..()
SSpathfinder.active_pathing -= src
SSpathfinder.currentrun -= src
hand_back(null)
avoid = null
/**
* "starts" off the pathfinding, by storing the values this datum will need to work later on
* returns FALSE if it fails to setup properly, TRUE otherwise
*/
/datum/pathfind/proc/start()
if(!start)
stack_trace("Invalid pathfinding start")
return FALSE
return TRUE
/**
* search_step() is the workhorse of pathfinding. It'll do the searching logic, and will slowly build up a path
* returns TRUE if everything is stable, FALSE if the pathfinding logic has failed, and we need to abort
*/
/datum/pathfind/proc/search_step()
return TRUE
/**
* early_exit() is called when something goes wrong in processing, and we need to halt the pathfinding NOW
*/
/datum/pathfind/proc/early_exit()
hand_back(null)
qdel(src)
/**
* Cleanup pass for the pathfinder. This tidies up the path, and fufills the pathfind's obligations
*/
/datum/pathfind/proc/finished()
qdel(src)
/**
* Call to return a value to whoever spawned this pathfinding work
* Will fail if it's already been called
*/
/datum/pathfind/proc/hand_back(value)
for(var/datum/callback/finished as anything in on_finish)
finished.Invoke(value)
on_finish = null
/**
* Processes a path (list of turfs), removes any diagonal moves that would lead to a weird bump
*
* path - The path to process down
* pass_info - Holds all the info about what this path attempt can go through
* simulated_only - If we are not allowed to pass space turfs
* avoid - A turf to be avoided
*/
/proc/remove_clunky_diagonals(list/path, datum/can_pass_info/pass_info, simulated_only, turf/avoid)
if(length(path) < 2)
return path
var/list/modified_path = list()
for(var/i in 1 to length(path) - 1)
var/turf/current_turf = path[i]
modified_path += current_turf
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
continue
//If the first diagonal movement step is invalid (north/south), replace with a sidestep first, with an implied vertical step in next_turf
var/vertical_only = movement_dir & (NORTH|SOUTH)
if(!CAN_STEP(current_turf,get_step(current_turf, vertical_only), simulated_only, pass_info, avoid))
modified_path += get_step(current_turf, movement_dir & ~vertical_only)
modified_path += path[length(path)]
return modified_path
/**
* Processes a path (list of turfs), removes any diagonal moves
*
* path - The path to process down
* pass_info - Holds all the info about what this path attempt can go through
* simulated_only - If we are not allowed to pass space turfs
* avoid - A turf to be avoided
*/
/proc/remove_diagonals(list/path, datum/can_pass_info/pass_info, simulated_only, turf/avoid)
if(length(path) < 2)
return path
var/list/modified_path = list()
for(var/i in 1 to length(path) - 1)
var/turf/current_turf = path[i]
modified_path += current_turf
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
continue
var/vertical_only = movement_dir & (NORTH|SOUTH)
// If we can't go directly north/south, we will first go to the side,
if(!CAN_STEP(current_turf,get_step(current_turf, vertical_only), simulated_only, pass_info, avoid))
modified_path += get_step(current_turf, movement_dir & ~vertical_only)
else // Otherwise, we'll first go north/south, then to the side
modified_path += get_step(current_turf, vertical_only)
modified_path += path[length(path)]
return modified_path
/**
* 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.
* Makes some other assumptions, such as assuming that unless declared, non dense objects will not block movement.
* It's fragile, but this is VERY much the most expensive part of pathing, so it'd better be fast
*
* Arguments:
* * destination_turf - Where are we going from where we are?
* * pass_info - Holds all the info about what this path attempt can go through
*/
/turf/proc/LinkBlockedWithAccess(turf/destination_turf, datum/can_pass_info/pass_info)
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, pass_info) || midstep_turf.LinkBlockedWithAccess(destination_turf, pass_info)
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)
// This is already assumed to be true
//if(TURF_PATHING_PASS_DENSITY)
// if(destination_turf.density)
// return TRUE
if(TURF_PATHING_PASS_PROC)
if(!destination_turf.CanPathfindPass(actual_dir, pass_info))
return TRUE
if(TURF_PATHING_PASS_NO)
return TRUE
var/static/list/directional_blocker_cache = typecacheof(list(/obj/structure/window, /obj/machinery/door/window, /obj/structure/railing, /obj/machinery/door/firedoor/border_only))
// Source border object checks
for(var/obj/border in src)
if(!directional_blocker_cache[border.type])
continue
if(!border.density && border.can_pathfind_pass == CANPATHFINDPASS_DENSITY)
continue
if(!border.CanPathfindPass(actual_dir, pass_info))
return TRUE
// Destination blockers check
var/reverse_dir = get_dir(destination_turf, src)
for(var/obj/iter_object in destination_turf)
// This is an optimization because of the massive call count of this code
if(!iter_object.density && iter_object.can_pathfind_pass == CANPATHFINDPASS_DENSITY)
continue
if(!iter_object.CanPathfindPass(reverse_dir, pass_info))
return TRUE
return FALSE
// Could easily be a struct if/when we get that
/**
* Holds all information about what an atom can move through
* Passed into CanPathfindPass to provide context for a pathing attempt
*
* Also used to check if using a cached path_map is safe
* There are some vars here that are unused. They exist to cover cases where caller_ref is used
* They're the properties of caller_ref used in those cases.
* It's kinda annoying, but there's some proc chains we can't convert to this datum
*/
/datum/can_pass_info
/// If we have no id, public airlocks are walls
var/no_id = FALSE
/// What we can pass through. Mirrors /atom/movable/pass_flags
var/pass_flags = NONE
/// What access we have, airlocks, windoors, etc
var/list/access = null
/// What sort of movement do we have. Mirrors /atom/movable/movement_type
var/movement_type = NONE
/// Are we being thrown?
var/thrown = FALSE
/// Are we anchored
var/anchored = FLASH_LIGHT_POWER
/// Are we a ghost? (they have effectively unique pathfinding)
var/is_observer = FALSE
/// Are we a living mob?
var/is_living = FALSE
/// Are we a bot?
var/is_bot = FALSE
/// Are we a drone?
var/is_drone = FALSE
/// Are we a movable? This seems weird but some of our pathfind checks do check for this
var/is_movable = FALSE
/// Can we ventcrawl?
var/can_ventcrawl = FALSE
/// What is the size of our mob
var/mob_size = null
/// Is our mob incapacitated
var/incapacitated = FALSE
/// Is our mob incorporeal
var/incorporeal_move = FALSE
/// Are we flying?
var/is_flying = FALSE
/// Are we megafauna?
var/is_megafauna = FALSE
/// If our mob has a rider, what does it look like
var/datum/can_pass_info/rider_info = null
/// If our mob is buckled to something, what's it like
var/datum/can_pass_info/buckled_info = null
var/list/factions = list()
/// Do we have gravity
var/has_gravity = TRUE
/// Pass information for the object we are pulling, if any
var/datum/can_pass_info/pulling_info = null
/// Cameras have a lot of BS can_z_move overrides
/// Let's avoid this
var/camera_type
var/caller_uid
/datum/can_pass_info/New(atom/movable/construct_from, list/access_, no_id_ = FALSE, call_depth = 0)
// No infiniloops
if(call_depth > 10)
return
if(access_)
access = access_.Copy()
no_id = no_id_
if(isnull(construct_from))
return
caller_uid = construct_from.UID()
pass_flags = construct_from.pass_flags
// TG uses movement type flags
// movement_type = construct_from.movement_type
thrown = !!construct_from.throwing
anchored = construct_from.anchored
has_gravity = has_gravity(construct_from)
is_movable = ismovable(construct_from)
is_drone = isdrone(construct_from)
is_megafauna = ismegafauna(construct_from)
if(ismob(construct_from))
var/mob/living/mob_construct = construct_from
src.incapacitated = mob_construct.incapacitated()
factions = mob_construct.faction
if(mob_construct.buckled)
src.buckled_info = new(mob_construct.buckled, access, no_id, call_depth + 1)
if(isobserver(construct_from))
src.is_observer = TRUE
if(isliving(construct_from))
var/mob/living/living_construct = construct_from
src.is_living = TRUE
src.can_ventcrawl = living_construct.ventcrawler == VENTCRAWLER_ALWAYS || living_construct.ventcrawler == VENTCRAWLER_NUDE
src.mob_size = living_construct.mob_size
src.incorporeal_move = living_construct.incorporeal_move
is_flying = living_construct.flying
if(iscameramob(construct_from))
src.camera_type = construct_from.type
src.is_bot = isbot(construct_from)
if(construct_from.pulling)
src.pulling_info = new(construct_from.pulling, access, no_id, call_depth + 1)
/// List of vars on /datum/can_pass_info to use when checking two instances for equality
GLOBAL_LIST_INIT(can_pass_info_vars, GLOBAL_PROC_REF(can_pass_check_vars))
/proc/can_pass_check_vars()
var/datum/can_pass_info/lamb = new()
var/datum/isaac = new()
var/list/altar = assoc_to_keys(lamb.vars - isaac.vars)
// Don't compare against calling atom, it's not relevant here
altar -= "caller_uid"
if(!("caller_uid" in lamb.vars))
CRASH("caller_ref var was not found in /datum/can_pass_info, why are we filtering for it?")
// We will bespoke handle pulling_info
altar -= "pulling_info"
if(!("pulling_info" in lamb.vars))
CRASH("pulling_info var was not found in /datum/can_pass_info, why are we filtering for it?")
return altar
/datum/can_pass_info/proc/compare_against(datum/can_pass_info/check_against)
for(var/comparable_var in GLOB.can_pass_info_vars)
if(!(vars[comparable_var] ~= check_against[comparable_var]))
return FALSE
if(!pulling_info != !check_against.pulling_info)
return FALSE
if(pulling_info && !pulling_info.compare_against(check_against.pulling_info))
return FALSE
return TRUE
+300
View File
@@ -0,0 +1,300 @@
#define FLOW_PATH_END 1
/// Datum that describes the shortest path between a source turf and any turfs within a distance
/datum/path_map
/// Assoc list of turf -> the turf one step closer on the path
/// Arranged in discovery order, so the last turf here will be the furthest from the start
var/list/next_closest = list()
/// List of distances from the starting turf, each index lines up with the next_closest list
var/list/distances = list()
/// Our starting turf, the location this map feeds into
var/turf/start
/// The tick we were completed on, in case you want to hold onto this for a bit
var/creation_time
/// The pass info datum used to create us
var/datum/can_pass_info/pass_info
/// Were we allowed to path over space?
var/pass_space = TRUE
/// Were we avoiding a turf? If so, which one?
var/turf/avoid
/// Are we currently being expanded?
var/expanding = FALSE
/// Are we currently being built
var/building = FALSE
/// Gets a list of turfs reachable by this path_map from the distance first to the distance second, both inclusive
/// first > second or first < second are both respected, and the return order will reflect the arg order
/// We return a list of turf -> distance, or null if we error
/datum/path_map/proc/turfs_in_range(first, second)
var/list/hand_back = list()
var/list/distances = src.distances
var/smaller = min(first, second)
var/larger = max(first, second)
var/largest_dist = distances[length(distances)]
if(smaller < 0 || larger < 0 || largest_dist < larger || largest_dist < smaller)
return null
if(first == smaller)
for(var/i in 1 to length(distances))
if(i > larger)
break
if(i >= smaller)
hand_back[next_closest[i]] = distances[i]
else
for(var/i in length(distances) to 1 step -1)
if(i < smaller)
break
if(i <= larger)
hand_back[next_closest[i]] = distances[i]
return hand_back
/**
* Takes a turf to path to, returns the shortest path to it at the time of this datum's creation
*
* skip_first - If we should drop the first step in the path. Used to avoid stepping where we already are
* min_target_dist - How many, if any, turfs off the end of the path should we drop?
*/
/datum/path_map/proc/get_path_to(turf/path_to, skip_first = FALSE, min_target_dist = 0)
return generate_path(path_to, skip_first, min_target_dist)
/**
* Takes a turf to start from, returns a path to the source turf of this datum
*
* skip_first - If we should drop the first step in the path. Used to avoid stepping where we already are
* min_target_dist - How many, if any, turfs off the end of the path should we drop?
*/
/datum/path_map/proc/get_path_from(turf/path_from, skip_first = FALSE, min_target_dist = 0)
return generate_path(path_from, skip_first, min_target_dist, reverse = TRUE)
/**
* Takes a turf to use as the other end, returns the path between the source node and it
*
* skip_first - If we should drop the first step in the path. Used to avoid stepping where we already are
* min_target_dist - How many, if any, turfs off the end of the path should we drop?
* reverse - If true, "reverses" the path generated. You'd want to use this for generating a path to the source node itself
*/
/datum/path_map/proc/generate_path(turf/other_end, skip_first = FALSE, min_target_dist = 0, reverse = FALSE)
var/list/path = list()
var/turf/next_turf = other_end
// Cache for sonic speed
var/next_closest = src.next_closest
while(next_turf != FLOW_PATH_END || next_turf == null)
path += next_turf
next_turf = next_closest[next_turf] // We take the first entry cause that's the turf
// This makes sense from a consumer level, I hate double negatives too I promise
if(!reverse)
path = reverselist(path)
if(skip_first && length(path) > 0)
path.Cut(1,2)
if(min_target_dist)
path.Cut(length(path) + 1 - min_target_dist, length(path) + 1)
return path
/datum/path_map/proc/display(delay = 10 SECONDS)
for(var/index in 1 to length(distances))
var/turf/next_turf = next_closest[index]
next_turf.maptext = "[distances[index]]"
next_turf.color = COLOR_NAVY_BLUE
animate(next_turf, color = null, delay)
animate(maptext = "", world.tick_lag)
/// Copies the passed in path_map into this datum
/// Saves some headache with updating refs if we want to modify a path_map
/datum/path_map/proc/copy_from(datum/path_map/read_from)
// Copy all the relevant vars over. NOT any of the timer stuff, we want them to still count
src.next_closest = read_from.next_closest
src.distances = read_from.distances
src.start = read_from.start
src.pass_info = read_from.pass_info
src.pass_space = read_from.pass_space
src.avoid = read_from.avoid
/// Returns true if the passed in pass_map's pass logic matches ours
/// False otherwise
/datum/path_map/proc/compare_against(datum/path_map/map)
return compare_against_args(map.pass_info, map.start, map.pass_space, map.avoid)
/// Returns true if the passed in pass_info and start/pass_space/avoid match ours
/// False otherwise
/datum/path_map/proc/compare_against_args(datum/can_pass_info/pass_info, turf/start, pass_space, turf/avoid)
if(src.start != start)
return FALSE
if(src.pass_space != pass_space)
return FALSE
if(src.avoid != avoid)
return FALSE
return pass_info.compare_against(pass_info)
/// Returns a new /datum/pathfind/sssp based off our settings
/// Will have an invalid source mob, no max distance, and no ending callback
/datum/path_map/proc/settings_to_path()
// Default creation to not set any vars incidentially
var/static/mob/jeremy = new()
var/datum/pathfind/sssp/based_on_what = new()
based_on_what.setup(pass_info, null, INFINITY, pass_space, avoid)
return based_on_what
/// Expands this pathmap to cover a new range, assuming the arg is greater then the current range
/// Returns true if this succeeded or was not required, false otherwise
/datum/path_map/proc/expand(new_range)
var/list/working_distances = distances
var/working_index = working_distances.len
var/max_dist = working_distances[working_distances.len]
if(new_range <= max_dist)
return TRUE
UNTIL(expanding == FALSE)
// In case max_dist has changed ya feel
if(new_range <= max_dist)
return TRUE
// Walk the start point backwards until we're at the first turf at the max distance
while(working_distances[working_index] == max_dist)
working_index -= 1
var/list/hand_around = list()
// We're guarenteed that hand_around will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
var/datum/callback/await = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around)
// We're gonna build a pathfind datum from our settings and set it running
var/datum/pathfind/sssp/based_off_us = new()
based_off_us.setup_from_canpass(pass_info, start, new_range, pass_space, avoid, list(await))
based_off_us.working_queue = next_closest.Copy()
based_off_us.working_distances = working_distances.Copy()
based_off_us.working_index = working_index
if(!SSpathfinder.run_pathfind(based_off_us))
return FALSE
expanding = TRUE
UNTIL(length(hand_around))
var/datum/path_map/return_val = hand_around[1]
if(!istype(return_val, /datum/path_map)) // It's trash, we've failed and need to clear away
return FALSE
copy_from(return_val)
expanding = FALSE
return TRUE
/datum/path_map/proc/sanity_check()
for(var/index in 1 to length(distances))
var/turf/next_turf = next_closest[index]
var/list/path = get_path_from(next_turf)
if(length(path) != distances[index] + 1)
stack_trace("[next_turf] had a distance of [length(path)] instead of the expected [distances[index]]")
if(path.Find(next_turf) != 1)
stack_trace("Starting turf [next_turf] was not the first entry in its list (instead it's at [path.Find(next_turf)])")
path = get_path_to(next_turf)
if(length(path) != distances[index] + 1)
stack_trace("[next_turf] had a distance of [length(path)] instead of the expected [distances[index]]")
if(path.Find(next_turf) != length(path))
stack_trace("Starting turf [next_turf] was not the last entry in its list (instead it's at [path.Find(next_turf)])")
/// Single source shortest path
/// Generates a flow map of a reachable turf -> the turf next closest to the map's center
/datum/pathfind/sssp
/// Ever expanding list of turfs to visit/visited, associated with the turf that's next closest to them
var/list/working_queue
/// List of distances, each entry mirrors an entry in the working_queue
var/list/working_distances
/// Our current position in the working queue
var/working_index
/datum/pathfind/sssp/proc/setup(atom/movable/caller, list/access, turf/center, max_distance, simulated_only, turf/avoid, list/datum/callback/on_finish)
src.pass_info = new(caller, access)
src.start = center
src.max_distance = max_distance
src.simulated_only = simulated_only
src.avoid = avoid
src.on_finish = on_finish
/datum/pathfind/sssp/proc/setup_from_canpass(datum/can_pass_info/info, turf/center, max_distance, simulated_only, turf/avoid, list/datum/callback/on_finish)
src.pass_info = info
src.start = center
src.max_distance = max_distance
src.simulated_only = simulated_only
src.avoid = avoid
src.on_finish = on_finish
/datum/pathfind/sssp/start()
. = ..()
if(!.)
return .
working_queue = list()
working_distances = list()
working_queue[start] = FLOW_PATH_END
working_distances += 0
working_index = 0
return TRUE
/datum/pathfind/sssp/search_step()
. = ..()
if(!.)
return .
var/datum/can_pass_info/pass_info = src.pass_info
while(working_index < length(working_queue))
working_index += 1
var/turf/next_turf = working_queue[working_index]
var/distance = working_distances[working_index] + 1
if(distance > max_distance)
if(TICK_CHECK)
return TRUE
continue
for(var/turf/adjacent in TURF_NEIGHBORS(next_turf))
// Already have a path? then we're gooood baby
if(working_queue[adjacent])
continue
// If it's blocked, go home
if(!CAN_STEP(next_turf, adjacent, simulated_only, pass_info, avoid))
continue
// I want to prevent diagonal moves around corners
// We do this first because blocked diagonals are more common then non blocked ones.
if(next_turf.x != adjacent.x && next_turf.y != adjacent.y)
var/movement_dir = get_dir(next_turf, adjacent)
// If either of the move components would bump into something, replace it with an explicit move around
var/turf/vertical_move = get_step(next_turf, movement_dir & (NORTH|SOUTH))
var/turf/horizontal_move = get_step(next_turf, movement_dir & (EAST|WEST))
if(!working_queue[vertical_move])
if(CAN_STEP(next_turf, vertical_move, simulated_only, pass_info, avoid))
working_queue[vertical_move] = next_turf
working_distances += distance
else
// Can't do a vertical move? let's do a horizontal move first
if(!working_queue[horizontal_move])
working_queue[horizontal_move] = next_turf
working_distances += distance
continue
if(!working_queue[horizontal_move])
if(CAN_STEP(next_turf, horizontal_move, simulated_only, pass_info, avoid))
working_queue[horizontal_move] = next_turf
working_distances += distance
else
if(!working_queue[vertical_move])
working_queue[vertical_move] = next_turf
working_distances += distance
continue
// Otherwise, this new turf's next closest turf is our source, so we'll mark as such and continue
// This is a breadth first search, we're essentially moving out in layers from the start position
working_queue[adjacent] = next_turf
working_distances += distance
if(TICK_CHECK)
return TRUE
return TRUE
/datum/pathfind/sssp/finished()
var/datum/path_map/flow_map = new()
flow_map.start = start
flow_map.pass_info = pass_info
flow_map.pass_space = simulated_only
flow_map.avoid = avoid
flow_map.next_closest = working_queue
flow_map.distances = working_distances
flow_map.creation_time = world.time
hand_back(flow_map)
return ..()
@@ -1,45 +1,206 @@
/// Queues and manages JPS pathfinding steps
SUBSYSTEM_DEF(pathfinder)
name = "Pathfinder"
init_order = INIT_ORDER_PATH
flags = SS_NO_FIRE
var/datum/flowcache/mobs
priority = FIRE_PRIORITY_PATHFINDING
wait = 0.5
/// List of pathfind datums we are currently trying to process
var/list/datum/pathfind/active_pathing = list()
/// List of pathfind datums being ACTIVELY processed. exists to make subsystem stats readable
var/list/datum/pathfind/currentrun = list()
/// List of uncheccked source_to_map entries
var/list/currentmaps = list()
/// Assoc list of target turf -> list(/datum/path_map) centered on the turf
var/list/source_to_maps = list()
var/static/space_type_cache
/datum/controller/subsystem/pathfinder/Initialize()
space_type_cache = typecacheof(/turf/space)
mobs = new(10)
/datum/flowcache
var/lcount
var/run
var/free
var/list/flow
/datum/controller/subsystem/pathfinder/stat_entry(msg)
msg = "P:[length(active_pathing)]"
return ..()
/datum/flowcache/New(n)
. = ..()
lcount = n
run = 0
free = 1
flow = new/list(lcount)
// This is another one of those subsystems (hey lighting) in which one "Run" means fully processing a queue
// We'll use a copy for this just to be nice to people reading the mc panel
/datum/controller/subsystem/pathfinder/fire(resumed)
if(!resumed)
src.currentrun = active_pathing.Copy()
src.currentmaps = deepCopyList(source_to_maps)
/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, TYPE_PROC_REF(/datum/flowcache, toolong), free), 150, TIMER_STOPPABLE)
flow[free] = t
flow[t] = M
return free
else
return 0
// Dies of sonic speed from caching datum var reads
var/list/currentrun = src.currentrun
while(length(currentrun))
var/datum/pathfind/path = currentrun[length(currentrun)]
if(!path.search_step()) // Something's wrong
path.early_exit()
currentrun.len--
continue
if(MC_TICK_CHECK)
return
path.finished()
// Next please
currentrun.len--
/datum/flowcache/proc/toolong(l)
log_game("Pathfinder route took longer than 150 ticks, src bot [flow[flow[l]]]")
found(l)
// Go over our existing pathmaps, clear out the ones we aren't using
var/list/currentmaps = src.currentmaps
var/oldest_time = world.time - MAP_REUSE_SLOWEST
while(length(currentmaps))
var/turf/source = currentmaps[length(currentmaps)]
var/list/datum/path_map/owned_maps = currentmaps[source]
for(var/datum/path_map/map as anything in owned_maps)
if(map.creation_time < oldest_time && !map.building)
source_to_maps[source] -= map
owned_maps.len--
if(MC_TICK_CHECK)
return
if(!length(source_to_maps[source]))
source_to_maps -= source
/datum/flowcache/proc/found(l)
deltimer(flow[l])
flow[l] = null
run -= 1
currentmaps.len--
/// Initiates a pathfind. Returns true if we're good, FALSE if something's failed
/datum/controller/subsystem/pathfinder/proc/pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, access = list(), simulated_only = TRUE, turf/exclude, skip_first = TRUE, diagonal_handling = DIAGONAL_REMOVE_CLUNKY, list/datum/callback/on_finish)
var/datum/pathfind/jps/path = new()
path.setup(caller, access, max_distance, simulated_only, exclude, on_finish, end, mintargetdist, skip_first, diagonal_handling)
if(path.start())
active_pathing += path
return TRUE
return FALSE
/// Initiates a swarmed pathfind. Returns TRUE if we're good, FALSE if something's failed
/// If a valid pathmap exists for the TARGET turf we'll use that, otherwise we have to build a new one
/datum/controller/subsystem/pathfinder/proc/swarmed_pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist = 0, age = MAP_REUSE_INSTANT, access = list(), simulated_only = TRUE, turf/exclude, skip_first = TRUE, list/datum/callback/on_finish)
var/turf/target = get_turf(end)
var/datum/can_pass_info/pass_info = new(caller, access)
// If there's a map we can use already, use it
var/datum/path_map/valid_map = get_valid_map(pass_info, target, simulated_only, exclude, age, include_building = TRUE)
if(valid_map && valid_map.expand(max_distance))
path_map_passalong(on_finish, get_turf(caller), mintargetdist, skip_first, valid_map)
return TRUE
// Otherwise we're gonna make a new one, and turn it into a path for the callbacks passed into us
var/list/datum/callback/pass_in = list()
pass_in += CALLBACK(GLOBAL_PROC, /proc/path_map_passalong, on_finish, get_turf(caller), mintargetdist, skip_first)
// And to allow subsequent calls to reuse the same map, we'll put a placeholder in the cache, and fill it up when the pathing finishes
var/datum/path_map/empty = new()
empty.pass_info = new(caller, access)
empty.start = target
empty.pass_space = simulated_only
empty.avoid = exclude
empty.building = TRUE
path_map_cache(target, empty)
pass_in += CALLBACK(src, PROC_REF(path_map_fill), target, empty)
if(!SSpathfinder.can_pass_build_map(pass_info, target, max_distance, simulated_only, exclude, pass_in))
return FALSE
return TRUE
/// We generate a path for the passed in callbacks, and then pipe it over
/proc/path_map_passalong(list/datum/callback/return_callbacks, turf/target, mintargetdist = 0, skip_first = TRUE, datum/path_map/hand_back)
var/list/requested_path
if(istype(hand_back, /datum/path_map))
requested_path = hand_back.get_path_from(target, skip_first, mintargetdist)
for(var/datum/callback/return_callback as anything in return_callbacks)
return_callback.Invoke(requested_path)
/// Caches the passed in path_map, allowing for reuse in future
/datum/controller/subsystem/pathfinder/proc/path_map_cache(turf/target, datum/path_map/hand_back)
// Cache our path_map
if(!target || !hand_back)
return
source_to_maps[target] += list(hand_back)
/datum/controller/subsystem/pathfinder/proc/path_map_fill(turf/target, datum/path_map/fill_into, datum/path_map/hand_back)
fill_into.building = FALSE
if(!fill_into.compare_against(hand_back))
source_to_maps[target] -= fill_into
return
fill_into.copy_from(hand_back)
fill_into.creation_time = hand_back.creation_time
// If we aren't in the source list anymore don't go trying to clear it out yeah?
if(!source_to_maps[target] || !(fill_into in source_to_maps[target]))
return
// Let's remove anything we're better than
for(var/datum/path_map/same_target as anything in source_to_maps[target])
if(fill_into == same_target || !same_target.compare_against(hand_back))
continue
// If it's still being made it'll be fresher then us
if(same_target.building)
continue
// We assume that we are fresher, and that's all we care about
// If it's being expanded it'll get updated when that finishes, then clear when all the refs drop
source_to_maps[target] -= same_target
/// Initiates a SSSP run. Returns true if we're good, FALSE if something's failed
/datum/controller/subsystem/pathfinder/proc/build_map(atom/movable/caller, turf/source, max_distance = 30, access = list(), simulated_only = TRUE, turf/exclude, list/datum/callback/on_finish)
var/datum/pathfind/sssp/path = new()
path.setup(caller, access, source, max_distance, simulated_only, exclude, on_finish)
if(path.start())
active_pathing += path
return TRUE
return FALSE
/// Initiates a SSSP run from a pass_info datum. Returns true if we're good, FALSE if something's failed
/datum/controller/subsystem/pathfinder/proc/can_pass_build_map(datum/can_pass_info/pass_info, turf/source, max_distance = 30, simulated_only = TRUE, turf/exclude, list/datum/callback/on_finish)
var/datum/pathfind/sssp/path = new()
path.setup_from_canpass(pass_info, source, max_distance, simulated_only, exclude, on_finish)
if(path.start())
active_pathing += path
return TRUE
return FALSE
/// Begins to handle a pathfinding run based off the input /datum/pathfind datum
/// You should not use this, it exists to allow for shenanigans. You do not know how to do shenanigans
/datum/controller/subsystem/pathfinder/proc/run_pathfind(datum/pathfind/run)
active_pathing += run
return TRUE
/// Takes a set of pathfind info, returns the first valid pathmap that would work if one exists
/// Optionally takes a max age to accept (defaults to 0 seconds) and a minimum acceptable range
/// If include_building is true and we can only find a building path, ew'll use that instead. tho we will wait for it to finish first
/datum/controller/subsystem/pathfinder/proc/get_valid_map(datum/can_pass_info/pass_info, turf/target, simulated_only = TRUE, turf/exclude, age = MAP_REUSE_INSTANT, min_range = -INFINITY, include_building = FALSE)
// Walk all the maps that match our caller's turf OR our target's
// Then hold onto em. If their cache time is short we can reuse/expand them, if not we'll have to make a new one
var/oldest_time = world.time - age
/// Backup return value used if no finished pathmaps are found
var/datum/path_map/constructing
for(var/datum/path_map/shared_source as anything in source_to_maps[target])
if(!shared_source.compare_against_args(pass_info, target, simulated_only, exclude))
continue
var/max_dist = 0
if(shared_source.distances.len)
max_dist = shared_source.distances[shared_source.distances.len]
if(max_dist < min_range)
continue
if(oldest_time > shared_source.creation_time && !shared_source.building)
continue
if(shared_source.building)
if(include_building)
constructing = constructing || shared_source
continue
return shared_source
if(constructing)
UNTIL(constructing.building == FALSE)
return constructing
return null
/// Takes a set of pathfind info, returns all valid pathmaps that would work
/// Takes an optional minimum range arg
/datum/controller/subsystem/pathfinder/proc/get_valid_maps(datum/can_pass_info/pass_info, turf/target, simulated_only = TRUE, turf/exclude, age = MAP_REUSE_INSTANT, min_range = -INFINITY, include_building = FALSE)
// Walk all the maps that match our caller's turf OR our target's
// Then hold onto em. If their cache time is short we can reuse/expand them, if not we'll have to make a new one
var/list/valid_maps = list()
var/oldest_time = world.time - age
for(var/datum/path_map/shared_source as anything in source_to_maps[target])
if(shared_source.compare_against_args(pass_info, target, simulated_only, exclude))
continue
var/max_dist = shared_source.distances[shared_source.distances.len]
if(max_dist < min_range)
continue
if(oldest_time > shared_source.creation_time)
continue
if(!include_building && shared_source.building)
continue
valid_maps += shared_source
return valid_maps
+10 -14
View File
@@ -14,6 +14,8 @@
/// 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
/// How this atom should react to having its astar blocking checked
var/can_pathfind_pass = CANPATHFINDPASS_DENSITY
var/list/blood_DNA
var/blood_color
@@ -1254,14 +1256,6 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
color = C
return
/*
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/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/CanPathfindPassTo(ID, dir, obj/destination)
return FALSE
/** Call this when you want to present a renaming prompt to the user.
It's a simple proc, but handles annoying edge cases such as forgetting to add a "cancel" button,
@@ -1365,16 +1359,18 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
* 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
* * to_dir - What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions
* * pass_info - Datum that stores info about the thing that's trying to pass us
*
* IMPORTANT NOTE: /turf/proc/LinkBlockedWithAccess assumes that overrides of CanPathfindPass will always return true if density is FALSE
* If this is NOT you, ensure you edit your can_pathfind_pass variable. Check __DEFINES/path.dm
**/
/atom/proc/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
if(caller && (caller.pass_flags & pass_flags_self))
/atom/proc/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(pass_info.pass_flags & pass_flags_self)
return TRUE
. = !density
/atom/proc/atom_prehit(obj/item/projectile/P)
return SEND_SIGNAL(src, COMSIG_ATOM_PREHIT, P)
+3 -3
View File
@@ -1375,13 +1375,13 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
update_icon()
return 1
/obj/machinery/door/airlock/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
/obj/machinery/door/airlock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(!density)
return TRUE
if(caller?.checkpass(PASSDOOR) && !locked)
if(pass_info.pass_flags & PASSDOOR && !locked)
return TRUE
//Airlock is passable if it is open (!density), bot has access, and is not bolted or welded shut)
return check_access(ID) && !locked && !welded && arePowerSystemsOn() && !no_id
return check_access_list(pass_info.access) && !locked && !welded && arePowerSystemsOn() && !pass_info.no_id
/obj/machinery/door/airlock/emag_act(mob/user)
if(!operating && density && arePowerSystemsOn() && !emagged)
+4 -4
View File
@@ -509,12 +509,12 @@
zap_flags &= ~ZAP_OBJ_DAMAGE
. = ..()
/obj/machinery/door/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id)
if(QDELETED(caller))
/obj/machinery/door/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(!locateUID(pass_info.caller_uid))
return ..()
if(caller.checkpass(PASSDOOR) && !locked)
if(pass_info.pass_flags & PASSDOOR && !locked)
return TRUE
if(caller.checkpass(PASSGLASS))
if(pass_info.pass_flags & PASSGLASS)
return !opacity
return ..()
+2 -2
View File
@@ -169,8 +169,8 @@
return TRUE
//used in the AStar algorithm to determinate if the turf the door is on is passable
/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/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
return !density || (dir != to_dir) || (check_access_list(pass_info.access) && hasPower())
/obj/machinery/door/window/CheckExit(atom/movable/mover, turf/target)
if(istype(mover) && mover.checkpass(PASSGLASS))
+4 -4
View File
@@ -1085,8 +1085,8 @@ GLOBAL_LIST_EMPTY(turret_icons)
/obj/machinery/porta_turret/syndicate/CanPass(atom/A)
return ((stat & BROKEN) || !isliving(A))
/obj/machinery/porta_turret/syndicate/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
return ((stat & BROKEN) || !isliving(caller))
/obj/machinery/porta_turret/syndicate/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
return ((stat & BROKEN) || !pass_info.is_living)
/obj/machinery/porta_turret/syndicate/die()
. = ..()
@@ -1208,5 +1208,5 @@ GLOBAL_LIST_EMPTY(turret_icons)
/obj/machinery/porta_turret/inflatable_turret/CanPass(atom/A)
return ((stat & BROKEN) || !isliving(A))
/obj/machinery/porta_turret/inflatable_turret/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
return ((stat & BROKEN) || !isliving(caller))
/obj/machinery/porta_turret/inflatable_turret/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
return ((stat & BROKEN) || !pass_info.is_living)
+4 -6
View File
@@ -573,12 +573,10 @@
return FALSE
return ..(mover, target, height)
/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)
return TRUE
return ..(ID, to_dir, caller)
/obj/machinery/shieldwall/syndicate/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(pass_info.is_living && ("syndicate" in pass_info.factions))
return TRUE
return ..(to_dir, pass_info)
/obj/machinery/shieldwall/syndicate/proc/phaseout()
// If you're bumping into an invisible shield, make it fully visible, then fade out over a couple of seconds.
+3 -4
View File
@@ -419,11 +419,10 @@
else
return 0
/obj/structure/girder/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
/obj/structure/girder/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
. = . || mover.checkpass(PASSGRILLE)
if(pass_info.is_movable)
. = . || pass_info.pass_flags & PASSGRILLE
/obj/structure/girder/deconstruct(disassembled = TRUE)
if(!(flags & NODECONSTRUCT))
+3 -4
View File
@@ -126,11 +126,10 @@
if(isprojectile(mover))
return (prob(30) || !density)
/obj/structure/grille/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
/obj/structure/grille/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
. = . || mover.checkpass(PASSGRILLE)
if(pass_info.is_movable)
. = . || pass_info.pass_flags & PASSGRILLE
/obj/structure/grille/attackby(obj/item/I, mob/user, params)
user.changeNext_move(CLICK_CD_MELEE)
+3 -4
View File
@@ -308,11 +308,10 @@
return FALSE
/obj/structure/m_tray/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
/obj/structure/m_tray/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
. = . || mover.checkpass(PASSTABLE)
if(pass_info.is_movable)
. = . || pass_info.pass_flags & PASSTABLE
/obj/structure/m_tray/Process_Spacemove(movement_dir)
return TRUE
+9 -10
View File
@@ -76,17 +76,16 @@
return ..()
/obj/structure/plasticflaps/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
if(isliving(caller))
if(isbot(caller) || isdrone(caller))
return TRUE
/obj/structure/plasticflaps/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(pass_info.is_bot || pass_info.is_drone)
return TRUE
if(!pass_info.can_ventcrawl && pass_info.mob_size != MOB_SIZE_TINY)
return FALSE
if(pass_info.pulling_info)
return CanPathfindPass(to_dir, pass_info.pulling_info)
var/mob/living/M = caller
if(!M.ventcrawler && M.mob_size != MOB_SIZE_TINY)
return FALSE
var/atom/movable/M = caller
if(M && M.pulling)
return CanPathfindPass(ID, to_dir, M.pulling)
return TRUE //diseases, stings, etc can pass
/obj/structure/plasticflaps/deconstruct(disassembled = TRUE)
+3 -3
View File
@@ -84,7 +84,7 @@
/obj/structure/railing/corner/CanPass()
return TRUE
/obj/structure/railing/corner/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
/obj/structure/railing/corner/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
return TRUE
/obj/structure/railing/corner/CheckExit()
@@ -93,7 +93,7 @@
/obj/structure/railing/cap/CanPass()
return TRUE
/obj/structure/railing/cap/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
/obj/structure/railing/cap/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
return TRUE
/obj/structure/railing/cap/CheckExit()
@@ -118,7 +118,7 @@
return density
return FALSE
/obj/structure/railing/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
/obj/structure/railing/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(to_dir == dir)
return FALSE
if(ordinal_direction_check(to_dir))
+6 -8
View File
@@ -150,11 +150,10 @@
return TRUE
return FALSE
/obj/structure/table/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
/obj/structure/table/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
. = . || mover.checkpass(PASSTABLE)
if(pass_info.is_movable)
. = . || pass_info.pass_flags & PASSTABLE
/**
* Determines whether a projectile crossing our turf should be stopped.
@@ -887,11 +886,10 @@
else
return 0
/obj/structure/rack/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
/obj/structure/rack/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
. = . || mover.checkpass(PASSTABLE)
if(pass_info.is_movable)
. = . || pass_info.pass_flags & PASSTABLE
/obj/structure/rack/MouseDrop_T(obj/O, mob/user)
if((!isitem(O) || user.get_active_hand() != O))
+4 -4
View File
@@ -137,13 +137,13 @@
return FALSE
return TRUE
/obj/structure/window/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
/obj/structure/window/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(!density)
return 1
return TRUE
if((dir == FULLTILE_WINDOW_DIR) || (dir & to_dir) || fulltile)
return 0
return FALSE
return 1
return TRUE
/obj/structure/window/attack_tk(mob/user)
user.changeNext_move(CLICK_CD_MELEE)
+4 -4
View File
@@ -50,11 +50,11 @@
if(!drop_stuff())
STOP_PROCESSING(SSprocessing, src)
/turf/simulated/floor/chasm/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE)
if(!isliving(caller))
/turf/simulated/floor/chasm/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
if(!pass_info.is_living)
return TRUE
var/mob/living/L = caller
return (L.flying || ismegafauna(caller))
return pass_info.is_flying || pass_info.is_megafauna
/turf/simulated/floor/chasm/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir)
underlay_appearance.icon = 'icons/turf/floors.dmi'
+4 -5
View File
@@ -57,11 +57,10 @@ GLOBAL_LIST_EMPTY(blob_minions)
/obj/structure/blob/CanAtmosPass(direction)
return !atmosblock
/obj/structure/blob/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = 0
if(ismovable(caller))
var/atom/movable/mover = caller
. = . || mover.checkpass(PASSBLOB)
/obj/structure/blob/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
. = FALSE
if(pass_info.is_movable)
. = . || pass_info.pass_flags & PASSBLOB
/obj/structure/blob/process()
Life()
+1 -1
View File
@@ -212,7 +212,7 @@
AM.setDir(current_dir)
now_pushing = FALSE
/mob/living/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
/mob/living/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
return TRUE // Unless you're a mule, something's trying to run you over.
/mob/living/proc/can_track(mob/living/user)
@@ -442,12 +442,8 @@
var/datum/pathfinding_mover/pathfind = new(src, target)
// I originally only wanted to make it use an ID if it couldnt pathfind otherwise, but that means it could take multiple minutes if both searches failed
var/obj/item/card/id/temp_id = new(src)
temp_id.access = get_all_accesses()
set_pathfinding(pathfind)
var/found_path = pathfind.generate_path(150, null, temp_id)
qdel(temp_id)
var/found_path = pathfind.generate_path(150, null, get_all_accesses())
if(!found_path)
set_pathfinding(null)
return FALSE
@@ -178,7 +178,7 @@
return
if(!bot_move(last_target_location, move_speed = 6))
var/last_target_pos_path = get_path_to(src, last_target_location, id = access_card, skip_first = TRUE)
var/last_target_pos_path = get_path_to(src, last_target_location, access = access_card.access, skip_first = TRUE)
if(length(last_target_pos_path) == 0)
frustration = 10
return
@@ -656,7 +656,7 @@ Pass a positive integer as an argument to override a bot's default speed.
access_card.access = get_all_accesses() // Give the bot temporary all access
set_path(get_path_to(src, waypoint, 200, id = access_card))
set_path(get_path_to(src, waypoint, 200, access = access_card.access))
calling_ai = caller // Link the AI to the bot!
ai_waypoint = waypoint
@@ -880,12 +880,12 @@ 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, 120, id=access_card, exclude=avoid))
set_path(get_path_to(src, patrol_target, 120, access = access_card.access, 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, 150, id=access_card, exclude=avoid))
set_path(get_path_to(src, summon_target, 150, access = access_card.access, exclude=avoid))
if(!length(path)) // Cannot reach target. Give up and announce the issue.
speak("Summon command failed, destination unreachable.",radio_channel)
bot_reset()
@@ -157,7 +157,7 @@
if(target)
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, 30, id=access_card)
path = get_path_to(src, target, 30, access = access_card.access)
if(!bot_move(target))
ignore_job -= target.UID()
add_to_ignore(target)
@@ -235,9 +235,9 @@
if(!length(path)) // No path, need a new one
if(!isturf(target))
var/turf/TL = get_turf(target)
path = get_path_to(src, TL, 30, id = access_card, simulated_only = 0)
path = get_path_to(src, TL, 30, access = access_card.access, simulated_only = 0)
else
path = get_path_to(src, target, 30, id = access_card, simulated_only = 0)
path = get_path_to(src, target, 30, access = access_card.access, simulated_only = 0)
if(!bot_move(target))
add_to_ignore(target)
@@ -335,10 +335,10 @@
return
if(patient && !length(path) && (get_dist(src,patient) > 1))
path = get_path_to(src, patient, 30,id=access_card)
path = get_path_to(src, patient, 30, access = access_card.access)
mode = BOT_MOVING
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)
path = get_path_to(src, patient, 30, 1, access = access_card.access)
if(!length(path)) //Do not chase a patient we cannot reach.
soft_reset()
@@ -79,7 +79,7 @@
QDEL_NULL(cell)
return ..()
/mob/living/simple_animal/bot/mulebot/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id)
/mob/living/simple_animal/bot/mulebot/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
return FALSE
/mob/living/simple_animal/bot/mulebot/can_buckle()
@@ -606,7 +606,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, 250, id=access_card, exclude=avoid))
set_path(get_path_to(src, target, 250, access = access_card.access, exclude = avoid))
// sets the current destination
// signals all beacons matching the delivery code
@@ -712,9 +712,6 @@
animate(held_item_icon, transform = m180)
underlays += held_item_icon
/mob/living/simple_animal/parrot/CanPathfindPassTo(ID, dir, obj/destination)
return is_type_in_typecache(destination, desired_perches)
#undef PARROT_PERCH
#undef PARROT_SWOOP
#undef PARROT_WANDER
@@ -115,7 +115,7 @@
to_chat(imp_in, "<span class='warning'>The implant does not recognize you as a known species!</span>")
return FALSE
var/mob/living/carbon/human/H = imp_in
set_path(get_path_to(module.mod, target, 150, id = H.wear_id, simulated_only = FALSE)) //Yes, science proves jetpacks work in space. More at 11.
set_path(get_path_to(module.mod, target, 150, access = H?.wear_id.GetAccess(), simulated_only = FALSE)) //Yes, science proves jetpacks work in space. More at 11.
if(!length(path)) //Cannot reach target. Give up and announce the issue.
to_chat(H, "<span class='warning'>No viable path found!</span>")
return FALSE
@@ -179,7 +179,7 @@
set_path(null)
var/target = get_turf(imp_in)
var/mob/living/carbon/human/H = imp_in
set_path(get_path_to(module.mod, target, 150, id = H.wear_id, simulated_only = FALSE)) //Yes, science proves jetpacks work in space. More at 11.
set_path(get_path_to(module.mod, target, 150, access = H?.wear_id.GetAccess(), simulated_only = FALSE)) //Yes, science proves jetpacks work in space. More at 11.
addtimer(CALLBACK(src, PROC_REF(mod_move), target), 6) //I'll value this properly soon
return TRUE
+4 -1
View File
@@ -95,6 +95,7 @@
#include "code\__DEFINES\muzzle_flash.dm"
#include "code\__DEFINES\newscaster_defines.dm"
#include "code\__DEFINES\particle_defines.dm"
#include "code\__DEFINES\path_defines.dm"
#include "code\__DEFINES\pda.dm"
#include "code\__DEFINES\pipes.dm"
#include "code\__DEFINES\power_defines.dm"
@@ -164,7 +165,6 @@
#include "code\__HELPERS\matrices.dm"
#include "code\__HELPERS\mob_helpers.dm"
#include "code\__HELPERS\name_helpers.dm"
#include "code\__HELPERS\path.dm"
#include "code\__HELPERS\pronouns.dm"
#include "code\__HELPERS\qdel.dm"
#include "code\__HELPERS\radiation_helpers.dm"
@@ -181,6 +181,9 @@
#include "code\__HELPERS\unique_ids.dm"
#include "code\__HELPERS\unsorted.dm"
#include "code\__HELPERS\verb_helpers.dm"
#include "code\__HELPERS\paths\jps.dm"
#include "code\__HELPERS\paths\path.dm"
#include "code\__HELPERS\paths\sssp.dm"
#include "code\__HELPERS\sorts\__main.dm"
#include "code\__HELPERS\sorts\InsertSort.dm"
#include "code\__HELPERS\sorts\MergeSort.dm"