Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TG Mirror] Adds pathmaps, refactors pathfinding a bit [MDB IGNORE] #133

Merged
merged 1 commit into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions code/__DEFINES/path.dm
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,23 @@
#define CANASTARPASS_DENSITY 0
/// If this is set, we bypass density checks and always call the proc
#define CANASTARPASS_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)
463 changes: 0 additions & 463 deletions code/__HELPERS/path.dm

This file was deleted.

306 changes: 306 additions & 0 deletions code/__HELPERS/paths/jps.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
/**
* 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

/// TODO: Macro this to reduce proc overhead
/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))
stack_trace("Invalid JPS destination")
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.is_empty() && !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
Loading
Loading