mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-09 16:05:07 +00:00
* Fixes foam spreading through public airlocks and windoors (#67101) * Fixes foam spreading through public airlocks and windoors * nova * Fixes foam spreading through public airlocks and windoors Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
435 lines
19 KiB
Plaintext
435 lines
19 KiB
Plaintext
/**
|
|
* This file contains the stuff you need for using JPS (Jump Point Search) pathing, an alternative to A* that skips
|
|
* over large numbers of uninteresting tiles resulting in much quicker pathfinding solutions. Mind that diagonals
|
|
* cost the same as cardinal moves currently, so paths may look a bit strange, but should still be optimal.
|
|
*/
|
|
|
|
/**
|
|
* This is the proc you use whenever you want to have pathfinding more complex than "try stepping towards the thing".
|
|
* If no path was found, returns an empty list, which is important for bots like medibots who expect an empty list rather than nothing.
|
|
*
|
|
* Arguments:
|
|
* * caller: The movable atom that's trying to find the path
|
|
* * end: What we're trying to path to. It doesn't matter if this is a turf or some other atom, we're gonna just path to the turf it's on anyway
|
|
* * max_distance: The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
|
|
* * mintargetdistance: Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example.
|
|
* * id: An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
|
|
* * simulated_only: Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
|
|
* * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
|
|
* * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures.
|
|
* * 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 (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
|
|
|
|
/// TODO: Macro this to reduce proc overhead
|
|
/proc/HeapPathWeightCompare(datum/jps_node/a, datum/jps_node/b)
|
|
return b.f_value - a.f_value
|
|
|
|
/// The datum used to handle the JPS pathfinding, completely self-contained
|
|
/datum/pathfind
|
|
/// The thing that we're actually trying to path for
|
|
var/atom/movable/caller
|
|
/// The turf where we started at
|
|
var/turf/start
|
|
/// The turf we're trying to path to (note that this won't track a moving target)
|
|
var/turf/end
|
|
/// The open list/stack we pop nodes out from (TODO: make this a normal list and macro-ize the heap operations to reduce proc overhead)
|
|
var/datum/heap/open
|
|
///An assoc list that serves as the closed list & tracks what turfs came from where. Key is the turf, and the value is what turf it came from
|
|
var/list/sources
|
|
/// The list we compile at the end if successful to pass back
|
|
var/list/path
|
|
|
|
// general pathfinding vars/args
|
|
/// An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
|
|
var/obj/item/card/id/id
|
|
/// How far away we have to get to the end target before we can call it quits
|
|
var/mintargetdist = 0
|
|
/// I don't know what this does vs , but they limit how far we can search before giving up on a path
|
|
var/max_distance = 30
|
|
/// Space is big and empty, if this is TRUE then we ignore pathing through unsimulated tiles
|
|
var/simulated_only
|
|
/// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through
|
|
var/turf/avoid
|
|
/// Ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary
|
|
var/diagonal_safety = TRUE
|
|
|
|
/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, diagonal_safety)
|
|
src.caller = caller
|
|
end = get_turf(goal)
|
|
open = new /datum/heap(/proc/HeapPathWeightCompare)
|
|
sources = new()
|
|
src.id = id
|
|
src.max_distance = max_distance
|
|
src.mintargetdist = mintargetdist
|
|
src.simulated_only = simulated_only
|
|
src.avoid = avoid
|
|
src.diagonal_safety = diagonal_safety
|
|
|
|
/**
|
|
* search() is the proc you call to kick off and handle the actual pathfinding, and kills the pathfind datum instance when it's done.
|
|
*
|
|
* If a valid path was found, it's returned as a list. If invalid or cross-z-level params are entered, or if there's no valid path found, we
|
|
* return null, which [/proc/get_path_to] translates to an empty list (notable for simple bots, who need empty lists)
|
|
*/
|
|
/datum/pathfind/proc/search()
|
|
start = get_turf(caller)
|
|
if(!start || !end)
|
|
stack_trace("Invalid A* start or destination")
|
|
return
|
|
if(start.z != end.z || start == end ) //no pathfinding between z levels
|
|
return
|
|
if(max_distance && (max_distance < get_dist(start, end))) //if start turf is farther than max_distance from end turf, no need to do anything
|
|
return
|
|
|
|
//initialization
|
|
var/datum/jps_node/current_processed_node = new (start, -1, 0, end)
|
|
open.insert(current_processed_node)
|
|
sources[start] = start // i'm sure this is fine
|
|
|
|
//then run the main loop
|
|
while(!open.is_empty() && !path)
|
|
if(!caller)
|
|
return
|
|
current_processed_node = open.pop() //get the lower f_value turf in the open list
|
|
if(max_distance && (current_processed_node.number_tiles > max_distance))//if too many steps, don't process that path
|
|
continue
|
|
|
|
var/turf/current_turf = current_processed_node.tile
|
|
for(var/scan_direction in list(EAST, WEST, NORTH, SOUTH))
|
|
lateral_scan_spec(current_turf, scan_direction, current_processed_node)
|
|
|
|
for(var/scan_direction in list(NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST))
|
|
diag_scan_spec(current_turf, scan_direction, current_processed_node)
|
|
|
|
CHECK_TICK
|
|
|
|
//we're done! reverse the path to get it from start to finish
|
|
if(path)
|
|
for(var/i = 1 to round(0.5 * length(path)))
|
|
path.Swap(i, length(path) - i + 1)
|
|
|
|
sources = null
|
|
qdel(open)
|
|
|
|
if(diagonal_safety)
|
|
path = diagonal_movement_safety()
|
|
|
|
return path
|
|
|
|
/// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search]
|
|
/datum/pathfind/proc/unwind_path(datum/jps_node/unwind_node)
|
|
path = new()
|
|
var/turf/iter_turf = unwind_node.tile
|
|
path.Add(iter_turf)
|
|
|
|
while(unwind_node.previous_node)
|
|
var/dir_goal = get_dir(iter_turf, unwind_node.previous_node.tile)
|
|
for(var/i = 1 to unwind_node.jumps)
|
|
iter_turf = get_step(iter_turf,dir_goal)
|
|
path.Add(iter_turf)
|
|
unwind_node = unwind_node.previous_node
|
|
|
|
/datum/pathfind/proc/diagonal_movement_safety()
|
|
if(length(path) < 2)
|
|
return
|
|
var/list/modified_path = list()
|
|
|
|
for(var/i in 1 to length(path) - 1)
|
|
var/turf/current_turf = path[i]
|
|
var/turf/next_turf = path[i+1]
|
|
var/movement_dir = get_dir(current_turf, next_turf)
|
|
if(!(movement_dir & (movement_dir - 1))) //cardinal movement, no need to verify
|
|
modified_path += current_turf
|
|
continue
|
|
//If default diagonal movement step is invalid, replace with alternative two steps
|
|
if(movement_dir & NORTH)
|
|
if(!CAN_STEP(current_turf,get_step(current_turf,NORTH)))
|
|
modified_path += current_turf
|
|
modified_path += get_step(current_turf, movement_dir & ~NORTH)
|
|
else
|
|
modified_path += current_turf
|
|
else
|
|
if(!CAN_STEP(current_turf,get_step(current_turf,SOUTH)))
|
|
modified_path += current_turf
|
|
modified_path += get_step(current_turf, movement_dir & ~SOUTH)
|
|
else
|
|
modified_path += current_turf
|
|
modified_path += path[length(path)]
|
|
|
|
return modified_path
|
|
|
|
/**
|
|
* For performing lateral scans from a given starting turf.
|
|
*
|
|
* These scans are called from both the main search loop, as well as subscans for diagonal scans, and they treat finding interesting turfs slightly differently.
|
|
* If we're doing a normal lateral scan, we already have a parent node supplied, so we just create the new node and immediately insert it into the heap, ezpz.
|
|
* If we're part of a subscan, we still need for the diagonal scan to generate a parent node, so we return a node datum with just the turf and let the diag scan
|
|
* proc handle transferring the values and inserting them into the heap.
|
|
*
|
|
* Arguments:
|
|
* * original_turf: What turf did we start this scan at?
|
|
* * heading: What direction are we going in? Obviously, should be cardinal
|
|
* * parent_node: Only given for normal lateral scans, if we don't have one, we're a diagonal subscan.
|
|
*/
|
|
/datum/pathfind/proc/lateral_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node)
|
|
var/steps_taken = 0
|
|
|
|
var/turf/current_turf = original_turf
|
|
var/turf/lag_turf = original_turf
|
|
|
|
while(TRUE)
|
|
if(path)
|
|
return
|
|
lag_turf = current_turf
|
|
current_turf = get_step(current_turf, heading)
|
|
steps_taken++
|
|
if(!CAN_STEP(lag_turf, current_turf))
|
|
return
|
|
|
|
if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist)))
|
|
var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken)
|
|
sources[current_turf] = original_turf
|
|
if(parent_node) // if this is a direct lateral scan we can wrap up, if it's a subscan from a diag, we need to let the diag make their node first, then finish
|
|
unwind_path(final_node)
|
|
return final_node
|
|
else if(sources[current_turf]) // already visited, essentially in the closed list
|
|
return
|
|
else
|
|
sources[current_turf] = original_turf
|
|
|
|
if(parent_node && parent_node.number_tiles + steps_taken > max_distance)
|
|
return
|
|
|
|
var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list?
|
|
|
|
switch(heading)
|
|
if(NORTH)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST))
|
|
interesting = TRUE
|
|
if(SOUTH)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST))
|
|
interesting = TRUE
|
|
if(EAST)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST))
|
|
interesting = TRUE
|
|
if(WEST)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST))
|
|
interesting = TRUE
|
|
|
|
if(interesting)
|
|
var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken)
|
|
if(parent_node) // if we're a diagonal subscan, we'll handle adding ourselves to the heap in the diag
|
|
open.insert(newnode)
|
|
return newnode
|
|
|
|
/**
|
|
* For performing diagonal scans from a given starting turf.
|
|
*
|
|
* Unlike lateral scans, these only are called from the main search loop, so we don't need to worry about returning anything,
|
|
* though we do need to handle the return values of our lateral subscans of course.
|
|
*
|
|
* Arguments:
|
|
* * original_turf: What turf did we start this scan at?
|
|
* * heading: What direction are we going in? Obviously, should be diagonal
|
|
* * parent_node: We should always have a parent node for diagonals
|
|
*/
|
|
/datum/pathfind/proc/diag_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node)
|
|
var/steps_taken = 0
|
|
var/turf/current_turf = original_turf
|
|
var/turf/lag_turf = original_turf
|
|
|
|
while(TRUE)
|
|
if(path)
|
|
return
|
|
lag_turf = current_turf
|
|
current_turf = get_step(current_turf, heading)
|
|
steps_taken++
|
|
if(!CAN_STEP(lag_turf, current_turf))
|
|
return
|
|
|
|
if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist)))
|
|
var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken)
|
|
sources[current_turf] = original_turf
|
|
unwind_path(final_node)
|
|
return
|
|
else if(sources[current_turf]) // already visited, essentially in the closed list
|
|
return
|
|
else
|
|
sources[current_turf] = original_turf
|
|
|
|
if(parent_node.number_tiles + steps_taken > max_distance)
|
|
return
|
|
|
|
var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list?
|
|
var/datum/jps_node/possible_child_node // otherwise, did one of our lateral subscans turn up something?
|
|
|
|
switch(heading)
|
|
if(NORTHWEST)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST))
|
|
interesting = TRUE
|
|
else
|
|
possible_child_node = (lateral_scan_spec(current_turf, WEST) || lateral_scan_spec(current_turf, NORTH))
|
|
if(NORTHEAST)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST))
|
|
interesting = TRUE
|
|
else
|
|
possible_child_node = (lateral_scan_spec(current_turf, EAST) || lateral_scan_spec(current_turf, NORTH))
|
|
if(SOUTHWEST)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST))
|
|
interesting = TRUE
|
|
else
|
|
possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, WEST))
|
|
if(SOUTHEAST)
|
|
if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST))
|
|
interesting = TRUE
|
|
else
|
|
possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, EAST))
|
|
|
|
if(interesting || possible_child_node)
|
|
var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken)
|
|
open.insert(newnode)
|
|
if(possible_child_node)
|
|
possible_child_node.update_parent(newnode)
|
|
open.insert(possible_child_node)
|
|
if(possible_child_node.tile == end || (mintargetdist && (get_dist(possible_child_node.tile, end) <= mintargetdist)))
|
|
unwind_path(possible_child_node)
|
|
return
|
|
|
|
/**
|
|
* For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags
|
|
*
|
|
* Assumes destinantion turf is non-dense - check and shortcircuit in code invoking this proc to avoid overhead.
|
|
*
|
|
* Arguments:
|
|
* * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach
|
|
* * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf
|
|
* * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space?
|
|
* * no_id: When true, doors with public access will count as impassible
|
|
*/
|
|
/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID, no_id = FALSE)
|
|
if(destination_turf.x != x && destination_turf.y != y) //diagonal
|
|
var/in_dir = get_dir(destination_turf,src) // eg. northwest (1+8) = 9 (00001001)
|
|
var/first_step_direction_a = in_dir & 3 // eg. north (1+8)&3 (0000 0011) = 1 (0000 0001)
|
|
var/first_step_direction_b = in_dir & 12 // eg. west (1+8)&12 (0000 1100) = 8 (0000 1000)
|
|
|
|
for(var/first_step_direction in list(first_step_direction_a,first_step_direction_b))
|
|
var/turf/midstep_turf = get_step(destination_turf,first_step_direction)
|
|
var/way_blocked = midstep_turf.density || LinkBlockedWithAccess(midstep_turf,caller,ID, no_id = no_id) || midstep_turf.LinkBlockedWithAccess(destination_turf,caller,ID, no_id = no_id)
|
|
if(!way_blocked)
|
|
return FALSE
|
|
return TRUE
|
|
|
|
var/actual_dir = get_dir(src, destination_turf)
|
|
|
|
/// These are generally cheaper than looping contents so they go first
|
|
switch(destination_turf.pathing_pass_method)
|
|
// 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.CanAStarPass(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.CanAStarPass(ID, actual_dir, no_id = no_id))
|
|
return TRUE
|
|
|
|
for(var/obj/machinery/door/window/iter_windoor in src)
|
|
if(!iter_windoor.CanAStarPass(ID, actual_dir, no_id = no_id))
|
|
return TRUE
|
|
|
|
for(var/obj/structure/railing/iter_rail in src)
|
|
if(!iter_rail.CanAStarPass(ID, actual_dir, no_id = no_id))
|
|
return TRUE
|
|
|
|
for(var/obj/machinery/door/firedoor/border_only/firedoor in src)
|
|
if(!firedoor.CanAStarPass(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.CanAStarPass(ID, reverse_dir, caller, no_id = no_id))
|
|
return TRUE
|
|
|
|
return FALSE
|
|
|
|
#undef CAN_STEP
|
|
#undef STEP_NOT_HERE_BUT_THERE
|