From adea9a644932602040655aeaf96afa434d0df142 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Mon, 22 Feb 2021 03:24:42 +0100 Subject: [PATCH] [MIRROR] Implements JPS (Jump Point Search) Pathfinding (#3581) * Implements JPS (Jump Point Search) Pathfinding (#56780) So a month or so ago I wanted to make it so dogs in my dog AI PR could path through doors if they had access, and was told I'd need to improve our pathfinding efficiency if I wanted to use full pathfinding for them. Thus, enter JPS, a pathfinding algorithm that allows for massive timesavings in systems with uniform cost grids like ours. This code is still fairly rough and needs polishing, but it's fully functional and already shows massive savings over traditional A*! I plan for this to replace A* as our default pathing method, but I'll leave the A* code in place in case someone ever needs it for whatever reason, like if a specific case needs variable cost pathing. Note that this allows for diagonal pathing instead of the cardinal pathing our A* uses right now, and the current version of the code costs the same to move diagonally as it does to move laterally, which may change later. There's also a lot of dummy/test code in right now in general, but you should still be able to test it out for yourself by spawning a bot like a medibot and using your PDA to summon it. Preliminary Profile Results A preliminary profile is available here. Using one medibot by itself on Metastation, I generated a list of 500 random blob spawn points around the station, gave the medibot all access, then let each algorithm tackle the list. The old A* algorithm took a total of 86 seconds to complete the list and processed 978065 nodes, while JPS took a total of 46 seconds and processed only 100062 nodes, for a 47% decrease in total time and an almost 90% decrease in nodes processed! Why It's Good For The Game Significantly cheaper pathing, which will very much come in handy for the AI datums I'm looking to dig into, what's not to like? * Implements JPS (Jump Point Search) Pathfinding Co-authored-by: Ryll Ryll <3589655+Ryll-Ryll@users.noreply.github.com> --- code/__HELPERS/AStar.dm | 209 ----------- code/__HELPERS/heap.dm | 50 +-- code/__HELPERS/path.dm | 349 ++++++++++++++++++ code/controllers/subsystem/pathfinder.dm | 2 - code/game/machinery/doors/airlock.dm | 4 +- code/game/objects/objs.dm | 19 +- code/game/objects/structures/girders.dm | 7 +- code/game/objects/structures/grille.dm | 7 +- code/game/objects/structures/plasticflaps.dm | 14 +- code/game/objects/structures/tables_racks.dm | 7 +- code/game/objects/structures/window.dm | 2 +- code/game/turfs/turf.dm | 20 + .../changeling/powers/tiny_prick.dm | 2 +- .../mob/living/simple_animal/bot/bot.dm | 7 +- .../mob/living/simple_animal/bot/cleanbot.dm | 2 +- .../mob/living/simple_animal/bot/firebot.dm | 2 +- .../mob/living/simple_animal/bot/floorbot.dm | 4 +- .../mob/living/simple_animal/bot/medbot.dm | 4 +- .../mob/living/simple_animal/bot/mulebot.dm | 2 +- .../mob/living/simple_animal/parrot.dm | 2 +- tgstation.dme | 2 +- 21 files changed, 443 insertions(+), 274 deletions(-) delete mode 100644 code/__HELPERS/AStar.dm create mode 100644 code/__HELPERS/path.dm diff --git a/code/__HELPERS/AStar.dm b/code/__HELPERS/AStar.dm deleted file mode 100644 index 606fa4cef14..00000000000 --- a/code/__HELPERS/AStar.dm +++ /dev/null @@ -1,209 +0,0 @@ -/* -A Star pathfinding algorithm -Returns a list of tiles forming a path from A to B, taking dense objects as well as walls, and the orientation of -windows along the route into account. -Use: -your_list = AStar(start location, end location, moving atom, distance proc, max nodes, maximum node depth, minimum distance to target, adjacent proc, atom id, turfs to exclude, check only simulated) - -Optional extras to add on (in order): -Distance proc : the distance used in every A* calculation (length of path and heuristic) -MaxNodes: The maximum number of nodes the returned path can be (0 = infinite) -Maxnodedepth: The maximum number of nodes to search (default: 30, 0 = infinite) -Mintargetdist: Minimum distance to the target before path returns, could be used to get -near a target, but not right to it - for an AI mob with a gun, for example. -Adjacent proc : returns the turfs to consider around the actually processed node -Simulated only : whether to consider unsimulated turfs or not (used by some Adjacent proc) - -Also added 'exclude' turf to avoid travelling over; defaults to null - -Actual Adjacent procs : - - /turf/proc/reachableAdjacentTurfs : returns reachable turfs in cardinal directions (uses simulated_only) - - /turf/proc/reachableAdjacentAtmosTurfs : returns turfs in cardinal directions reachable via atmos - -*/ -#define PF_TIEBREAKER 0.005 -//tiebreker weight.To help to choose between equal paths -////////////////////// -//datum/pathnode object -////////////////////// -#define MASK_ODD 85 -#define MASK_EVEN 170 - - -//A* nodes variables -/datum/pathnode - var/turf/source //turf associated with the PathNode - var/datum/pathnode/prevNode //link to the parent PathNode - var/f //A* Node weight (f = g + h) - var/g //A* movement cost variable - var/h //A* heuristic variable - var/nt //count the number of Nodes traversed - var/bf //bitflag for dir to expand.Some sufficiently advanced motherfuckery - -/datum/pathnode/New(s,p,pg,ph,pnt,_bf) - source = s - prevNode = p - g = pg - h = ph - f = g + h*(1+ PF_TIEBREAKER) - nt = pnt - bf = _bf - -/datum/pathnode/proc/setp(p,pg,ph,pnt) - prevNode = p - g = pg - h = ph - f = g + h*(1+ PF_TIEBREAKER) - nt = pnt - -/datum/pathnode/proc/calc_f() - f = g + h - -////////////////////// -//A* procs -////////////////////// - -//the weighting function, used in the A* algorithm -/proc/PathWeightCompare(datum/pathnode/a, datum/pathnode/b) - return a.f - b.f - -//reversed so that the Heap is a MinHeap rather than a MaxHeap -/proc/HeapPathWeightCompare(datum/pathnode/a, datum/pathnode/b) - return b.f - a.f - -//wrapper that returns an empty list if A* failed to find a path -/proc/get_path_to(caller, end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableTurftest, id=null, turf/exclude=null, simulated_only = TRUE) - var/l = SSpathfinder.mobs.getfree(caller) - while(!l) - stoplag(3) - l = SSpathfinder.mobs.getfree(caller) - var/list/path = AStar(caller, end, dist, maxnodes, maxnodedepth, mintargetdist, adjacent,id, exclude, simulated_only) - - SSpathfinder.mobs.found(l) - if(!path) - path = list() - return path - -/proc/cir_get_path_to(caller, end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableTurftest, id=null, turf/exclude=null, simulated_only = TRUE) - var/l = SSpathfinder.circuits.getfree(caller) - while(!l) - stoplag(3) - l = SSpathfinder.circuits.getfree(caller) - var/list/path = AStar(caller, end, dist, maxnodes, maxnodedepth, mintargetdist, adjacent,id, exclude, simulated_only) - SSpathfinder.circuits.found(l) - if(!path) - path = list() - return path - -/proc/AStar(caller, _end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableTurftest, id=null, turf/exclude=null, simulated_only = TRUE) - //sanitation - var/turf/end = get_turf(_end) - var/turf/start = get_turf(caller) - if(!start || !end) - stack_trace("Invalid A* start or destination") - return FALSE - if( start.z != end.z || start == end ) //no pathfinding between z levels - return FALSE - if(maxnodes) - //if start turf is farther than maxnodes from end turf, no need to do anything - if(call(start, dist)(end) > maxnodes) - return FALSE - maxnodedepth = maxnodes //no need to consider path longer than maxnodes - var/datum/heap/open = new /datum/heap(/proc/HeapPathWeightCompare) //the open list - var/list/openc = new() //open list for node check - var/list/path = null //the returned path, if any - //initialization - var/datum/pathnode/cur = new /datum/pathnode(start,null,0,call(start,dist)(end),0,15,1)//current processed turf - open.Insert(cur) - openc[start] = cur - //then run the main loop - while(!open.IsEmpty() && !path) - cur = open.Pop() //get the lower f turf in the open list - //get the lower f node on the open list - //if we only want to get near the target, check if we're close enough - var/closeenough - if(mintargetdist) - closeenough = call(cur.source,dist)(end) <= mintargetdist - - - //found the target turf (or close enough), let's create the path to it - if(cur.source == end || closeenough) - path = new() - path.Add(cur.source) - while(cur.prevNode) - cur = cur.prevNode - path.Add(cur.source) - break - //get adjacents turfs using the adjacent proc, checking for access with id - if((!maxnodedepth)||(cur.nt <= maxnodedepth))//if too many steps, don't process that path - for(var/i = 0 to 3) - var/f= 1<>1) //getting reverse direction throught swapping even and odd bits.((f & 01010101)<<1)|((f & 10101010)>>1) - var/newg = cur.g + call(cur.source,dist)(T) - if(CN) - //is already in open list, check if it's a better way from the current turf - CN.bf &= 15^r //we have no closed, so just cut off exceed dir.00001111 ^ reverse_dir.We don't need to expand to checked turf. - if((newg < CN.g) ) - if(call(cur.source,adjacent)(caller, T, id, simulated_only)) - CN.setp(cur,newg,CN.h,cur.nt+1) - open.ReSort(CN)//reorder the changed element in the list - else - //is not already in open list, so add it - if(call(cur.source,adjacent)(caller, T, id, simulated_only)) - CN = new(T,cur,newg,call(T,dist)(end),cur.nt+1,15^r) - open.Insert(CN) - openc[T] = CN - cur.bf = 0 - CHECK_TICK - //reverse the path to get it from start to finish - if(path) - for(var/i = 1 to round(0.5*path.len)) - path.Swap(i,path.len-i+1) - openc = null - //cleaning after us - return path - -//Returns adjacent turfs in cardinal directions that are reachable -//simulated_only controls whether only simulated turfs are considered or not - -/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only) - var/list/L = new() - var/turf/T - var/static/space_type_cache = typecacheof(/turf/open/space) - - for(var/k in 1 to GLOB.cardinals.len) - T = get_step(src,GLOB.cardinals[k]) - if(!T || (simulated_only && space_type_cache[T.type])) - continue - if(!T.density && !LinkBlockedWithAccess(T,caller, ID)) - L.Add(T) - return L - -/turf/proc/reachableTurftest(caller, turf/T, ID, simulated_only) - if(T && !T.density && !(simulated_only && SSpathfinder.space_type_cache[T.type]) && !LinkBlockedWithAccess(T,caller, ID)) - return TRUE - -//Returns adjacent turfs in cardinal directions that are reachable via atmos -/turf/proc/reachableAdjacentAtmosTurfs() - return atmos_adjacent_turfs - -/turf/proc/LinkBlockedWithAccess(turf/T, caller, ID) - var/adir = get_dir(src, T) - var/rdir = ((adir & MASK_ODD)<<1)|((adir & MASK_EVEN)>>1) - for(var/obj/structure/window/W in src) - if(!W.CanAStarPass(ID, adir)) - return TRUE - for(var/obj/machinery/door/window/W in src) - if(!W.CanAStarPass(ID, adir)) - return TRUE - for(var/obj/O in T) - if(!O.CanAStarPass(ID, rdir, caller)) - return TRUE - - return FALSE diff --git a/code/__HELPERS/heap.dm b/code/__HELPERS/heap.dm index 2da417107e3..c860b2013ca 100644 --- a/code/__HELPERS/heap.dm +++ b/code/__HELPERS/heap.dm @@ -1,4 +1,3 @@ - ////////////////////// //datum/heap object ////////////////////// @@ -11,29 +10,35 @@ L = new() cmp = compare -/datum/heap/proc/IsEmpty() - return !L.len +/datum/heap/Destroy(force, ...) + for(var/i in L) // because this is before the list helpers are loaded + qdel(i) + L = null + return ..() -//Insert and place at its position a new node in the heap -/datum/heap/proc/Insert(atom/A) +/datum/heap/proc/is_empty() + return !length(L) + +//insert and place at its position a new node in the heap +/datum/heap/proc/insert(atom/A) L.Add(A) - Swim(L.len) + swim(length(L)) //removes and returns the first element of the heap //(i.e the max or the min dependant on the comparison function) -/datum/heap/proc/Pop() - if(!L.len) +/datum/heap/proc/pop() + if(!length(L)) return 0 . = L[1] - L[1] = L[L.len] - L.Cut(L.len) - if(L.len) - Sink(1) + L[1] = L[length(L)] + L.Cut(length(L)) + if(length(L)) + sink(1) //Get a node up to its right position in the heap -/datum/heap/proc/Swim(index) +/datum/heap/proc/swim(index) var/parent = round(index * 0.5) while(parent > 0 && (call(cmp)(L[index],L[parent]) > 0)) @@ -42,21 +47,21 @@ parent = round(index * 0.5) //Get a node down to its right position in the heap -/datum/heap/proc/Sink(index) - var/g_child = GetGreaterChild(index) +/datum/heap/proc/sink(index) + var/g_child = get_greater_child(index) while(g_child > 0 && (call(cmp)(L[index],L[g_child]) < 0)) L.Swap(index,g_child) index = g_child - g_child = GetGreaterChild(index) + g_child = get_greater_child(index) //Returns the greater (relative to the comparison proc) of a node children //or 0 if there's no child -/datum/heap/proc/GetGreaterChild(index) - if(index * 2 > L.len) +/datum/heap/proc/get_greater_child(index) + if(index * 2 > length(L)) return 0 - if(index * 2 + 1 > L.len) + if(index * 2 + 1 > length(L)) return index * 2 if(call(cmp)(L[index * 2],L[index * 2 + 1]) < 0) @@ -65,12 +70,11 @@ return index * 2 //Replaces a given node so it verify the heap condition -/datum/heap/proc/ReSort(atom/A) +/datum/heap/proc/resort(atom/A) var/index = L.Find(A) - Swim(index) - Sink(index) + swim(index) + sink(index) /datum/heap/proc/List() . = L.Copy() - diff --git a/code/__HELPERS/path.dm b/code/__HELPERS/path.dm new file mode 100644 index 00000000000..96becc1a590 --- /dev/null +++ b/code/__HELPERS/path.dm @@ -0,0 +1,349 @@ +/** + * 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" + * + * 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 + */ +/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude) + if(!caller || !get_turf(end)) + return + + var/l = SSpathfinder.mobs.getfree(caller) + while(!l) + stoplag(3) + l = SSpathfinder.mobs.getfree(caller) + + var/list/path + var/datum/pathfind/pathfind_datum = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude) + path = pathfind_datum.search() + qdel(pathfind_datum) + + SSpathfinder.mobs.found(l) + if(!path) + path = list() + return path + +/** + * A helper macro to see if it's possible to step from the first turf into the second one, minding things like door access and directional windows. + * Note that this can only be used inside the [datum/pathfind][pathfind datum] since it uses variables from said datum + * If you really want to optimize things, optimize this, cuz this gets called a lot + */ +#define CAN_STEP(cur_turf, next) (next && !next.density && cur_turf.Adjacent(next) && !(simulated_only && SSpathfinder.space_type_cache[next.type]) && !cur_turf.LinkBlockedWithAccess(next,caller, id) && (next != avoid)) +/// Another helper macro for JPS, for telling when a node has forced neighbors that need expanding +#define STEP_NOT_HERE_BUT_THERE(cur_turf, dirA, dirB) ((!CAN_STEP(cur_turf, get_step(cur_turf, dirA)) && CAN_STEP(cur_turf, get_step(cur_turf, dirB)))) + +/// The JPS Node datum represents a turf that we find interesting enough to add to the open list and possibly search for new tiles from +/datum/jps_node + /// The turf associated with this node + var/turf/tile + /// The node we just came from + var/datum/jps_node/previous_node + /// The A* node weight (f_value = number_of_tiles + heuristic) + var/f_value + /// The A* node heuristic (a rough estimate of how far we are from the goal) + var/heuristic + /// How many steps it's taken to get here from the start (currently pulling double duty as steps taken & cost to get here, since all moves incl diagonals cost 1 rn) + var/number_tiles + /// How many steps it took to get here from the last node + var/jumps + /// Nodes store the endgoal so they can process their heuristic without a reference to the pathfind datum + var/turf/node_goal + +/datum/jps_node/New(turf/our_tile, datum/jps_node/incoming_previous_node, jumps_taken, turf/incoming_goal) + tile = our_tile + jumps = jumps_taken + if(incoming_goal) // if we have the goal argument, this must be the first/starting node + node_goal = incoming_goal + else if(incoming_previous_node) // if we have the parent, this is from a direct lateral/diagonal scan, we can fill it all out now + previous_node = incoming_previous_node + number_tiles = previous_node.number_tiles + jumps + node_goal = previous_node.node_goal + heuristic = get_dist(tile, node_goal) + f_value = number_tiles + heuristic + // otherwise, no parent node means this is from a subscan lateral scan, so we just need the tile for now until we call [datum/jps/proc/update_parent] on it + +/datum/jps_node/Destroy(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 + +/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid) + src.caller = caller + end = get_turf(goal) + open = new /datum/heap(/proc/HeapPathWeightCompare) + sources = new() + src.id = id + src.max_distance = max_distance + src.mintargetdist = mintargetdist + src.simulated_only = simulated_only + src.avoid = avoid + +/// The proc you use to run the search, returns FALSE if it's invalid, an empty list if no path could be found, or a valid path to the target +/datum/pathfind/proc/search() + start = get_turf(caller) + if(!start || !end) + stack_trace("Invalid A* start or 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 + + //initialization + var/datum/jps_node/current_processed_node = new (start, -1, 0, end) + open.insert(current_processed_node) + sources[start] = start // i'm sure this is fine + + //then run the main loop + while(!open.is_empty() && !path) + if(!caller) + return + current_processed_node = open.pop() //get the lower f_value turf in the open list + if(max_distance && (current_processed_node.number_tiles > max_distance))//if too many steps, don't process that path + continue + + var/turf/current_turf = current_processed_node.tile + for(var/scan_direction in list(EAST, WEST, NORTH, SOUTH)) + lateral_scan_spec(current_turf, scan_direction, current_processed_node) + + for(var/scan_direction in list(NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST)) + diag_scan_spec(current_turf, scan_direction, current_processed_node) + + CHECK_TICK + + //we're done! reverse the path to get it from start to finish + if(path) + for(var/i = 1 to round(0.5 * length(path))) + path.Swap(i, length(path) - i + 1) + sources = null + qdel(open) + return path + +/// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search] +/datum/pathfind/proc/unwind_path(datum/jps_node/unwind_node) + path = new() + var/turf/iter_turf = unwind_node.tile + path.Add(iter_turf) + + while(unwind_node.previous_node) + var/dir_goal = get_dir(iter_turf, unwind_node.previous_node.tile) + for(var/i = 1 to unwind_node.jumps) + iter_turf = get_step(iter_turf,dir_goal) + path.Add(iter_turf) + unwind_node = unwind_node.previous_node + +/** + * For performing lateral scans from a given starting turf. + * + * These scans are called from both the main search loop, as well as subscans for diagonal scans, and they treat finding interesting turfs slightly differently. + * If we're doing a normal lateral scan, we already have a parent node supplied, so we just create the new node and immediately insert it into the heap, ezpz. + * If we're part of a subscan, we still need for the diagonal scan to generate a parent node, so we return a node datum with just the turf and let the diag scan + * proc handle transferring the values and inserting them into the heap. + * + * Arguments: + * * original_turf: What turf did we start this scan at? + * * heading: What direction are we going in? Obviously, should be cardinal + * * parent_node: Only given for normal lateral scans, if we don't have one, we're a diagonal subscan. +*/ +/datum/pathfind/proc/lateral_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node) + var/steps_taken = 0 + + var/turf/current_turf = original_turf + var/turf/lag_turf = original_turf + + while(TRUE) + if(path) + return + lag_turf = current_turf + current_turf = get_step(current_turf, heading) + steps_taken++ + if(!CAN_STEP(lag_turf, current_turf)) + return + + if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist))) + var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken) + sources[current_turf] = original_turf + if(parent_node) // if this is a direct lateral scan we can wrap up, if it's a subscan from a diag, we need to let the diag make their node first, then finish + unwind_path(final_node) + return final_node + else if(sources[current_turf]) // already visited, essentially in the closed list + return + else + sources[current_turf] = original_turf + + if(parent_node && parent_node.number_tiles + steps_taken > max_distance) + return + + var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list? + + switch(heading) + if(NORTH) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST)) + interesting = TRUE + if(SOUTH) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST)) + interesting = TRUE + if(EAST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST)) + interesting = TRUE + if(WEST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST)) + interesting = TRUE + + if(interesting) + var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken) + if(parent_node) // if we're a diagonal subscan, we'll handle adding ourselves to the heap in the diag + open.insert(newnode) + return newnode + +/** + * For performing diagonal scans from a given starting turf. + * + * Unlike lateral scans, these only are called from the main search loop, so we don't need to worry about returning anything, + * though we do need to handle the return values of our lateral subscans of course. + * + * Arguments: + * * original_turf: What turf did we start this scan at? + * * heading: What direction are we going in? Obviously, should be diagonal + * * parent_node: We should always have a parent node for diagonals +*/ +/datum/pathfind/proc/diag_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node) + var/steps_taken = 0 + var/turf/current_turf = original_turf + var/turf/lag_turf = original_turf + + while(TRUE) + if(path) + return + lag_turf = current_turf + current_turf = get_step(current_turf, heading) + steps_taken++ + if(!CAN_STEP(lag_turf, current_turf)) + return + + if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist))) + var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken) + sources[current_turf] = original_turf + unwind_path(final_node) + return + else if(sources[current_turf]) // already visited, essentially in the closed list + return + else + sources[current_turf] = original_turf + + if(parent_node.number_tiles + steps_taken > max_distance) + return + + var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list? + var/datum/jps_node/possible_child_node // otherwise, did one of our lateral subscans turn up something? + + switch(heading) + if(NORTHWEST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, WEST) || lateral_scan_spec(current_turf, NORTH)) + if(NORTHEAST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, EAST) || lateral_scan_spec(current_turf, NORTH)) + if(SOUTHWEST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, WEST)) + if(SOUTHEAST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, EAST)) + + if(interesting || possible_child_node) + var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken) + open.insert(newnode) + if(possible_child_node) + possible_child_node.update_parent(newnode) + open.insert(possible_child_node) + if(possible_child_node.tile == end || (mintargetdist && (get_dist(possible_child_node.tile, end) <= mintargetdist))) + unwind_path(possible_child_node) + return + +/** + * For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags + * + * Arguments: + * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach + * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf + * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? +*/ +/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID) + var/actual_dir = get_dir(src, destination_turf) + + for(var/obj/structure/window/iter_window in src) + if(!iter_window.CanAStarPass(ID, actual_dir)) + return TRUE + + for(var/obj/machinery/door/window/iter_windoor in src) + if(!iter_windoor.CanAStarPass(ID, actual_dir)) + return TRUE + + var/reverse_dir = get_dir(destination_turf, src) + for(var/obj/iter_object in destination_turf) + if(!iter_object.CanAStarPass(ID, reverse_dir, caller)) + return TRUE + + return FALSE + +#undef CAN_STEP +#undef STEP_NOT_HERE_BUT_THERE diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm index ccbea793066..26f54245732 100644 --- a/code/controllers/subsystem/pathfinder.dm +++ b/code/controllers/subsystem/pathfinder.dm @@ -3,13 +3,11 @@ SUBSYSTEM_DEF(pathfinder) init_order = INIT_ORDER_PATH flags = SS_NO_FIRE var/datum/flowcache/mobs - var/datum/flowcache/circuits var/static/space_type_cache /datum/controller/subsystem/pathfinder/Initialize() space_type_cache = typecacheof(/turf/open/space) mobs = new(10) - circuits = new(3) return ..() /datum/flowcache diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index d46590b453e..c3a0ef1273e 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -1275,8 +1275,8 @@ assemblytype = initial(airlock.assemblytype) update_appearance() -/obj/machinery/door/airlock/CanAStarPass(obj/item/card/id/ID) -//Airlock is passable if it is open (!density), bot has access, and is not bolted shut or powered off) +/obj/machinery/door/airlock/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) + //Airlock is passable if it is open (!density), bot has access, and is not bolted shut or powered off) return !density || (check_access(ID) && !locked && hasPower()) /obj/machinery/door/airlock/emag_act(mob/user, obj/item/card/emag/doorjack/D) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 38247b0f2e2..7c03dfab1e6 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -219,11 +219,20 @@ /obj/get_dumping_location(datum/component/storage/source,mob/user) return get_turf(src) -/obj/proc/CanAStarPass(ID, dir, caller) - if(ismovable(caller)) - var/atom/movable/AM = caller - if(AM.pass_flags & pass_flags_self) - return TRUE +/** + * This proc is used for telling whether something can pass by this object in a given direction, for use by the pathfinding system. + * + * Trying to generate one long path across the station will call this proc on every single object on every single tile that we're seeing if we can move through, likely + * multiple times per tile since we're likely checking if we can access said tile from multiple directions, so keep these as lightweight as possible. + * + * Arguments: + * * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference + * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions + * * caller- The movable we're checking pass flags for, if we're making any such checks + **/ +/obj/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) + if(istype(caller) && (caller.pass_flags & pass_flags_self)) + return TRUE . = !density /obj/proc/check_uplink_validity() diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 826b318b8d3..19dd9253979 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -297,11 +297,10 @@ if((mover.pass_flags & PASSGRILLE) || istype(mover, /obj/projectile)) return prob(girderpasschance) -/obj/structure/girder/CanAStarPass(ID, dir, caller) +/obj/structure/girder/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) . = !density - if(ismovable(caller)) - var/atom/movable/mover = caller - . = . || (mover.pass_flags & PASSGRILLE) + if(istype(caller)) + . = . || (caller.pass_flags & PASSGRILLE) /obj/structure/girder/deconstruct(disassembled = TRUE) if(!(flags_1 & NODECONSTRUCT_1)) diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm index 3f5d3eeeb6d..7b0cd9e9c1a 100644 --- a/code/game/objects/structures/grille.dm +++ b/code/game/objects/structures/grille.dm @@ -127,11 +127,10 @@ if(!. && istype(mover, /obj/projectile)) return prob(30) -/obj/structure/grille/CanAStarPass(ID, dir, caller) +/obj/structure/grille/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) . = !density - if(ismovable(caller)) - var/atom/movable/mover = caller - . = . || (mover.pass_flags & PASSGRILLE) + if(istype(caller)) + . = . || (caller.pass_flags & PASSGRILLE) /obj/structure/grille/attackby(obj/item/W, mob/user, params) user.changeNext_move(CLICK_CD_MELEE) diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm index 72bd452dfef..cf801cee0f9 100644 --- a/code/game/objects/structures/plasticflaps.dm +++ b/code/game/objects/structures/plasticflaps.dm @@ -56,18 +56,18 @@ return FALSE return TRUE -/obj/structure/plasticflaps/CanAStarPass(ID, to_dir, caller) +/obj/structure/plasticflaps/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) if(isliving(caller)) if(isbot(caller)) return TRUE - var/mob/living/M = caller - var/ventcrawler = HAS_TRAIT(M, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(M, TRAIT_VENTCRAWLER_NUDE) - if(!ventcrawler && M.mob_size != MOB_SIZE_TINY) + var/mob/living/living_caller = caller + var/ventcrawler = HAS_TRAIT(living_caller, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(living_caller, TRAIT_VENTCRAWLER_NUDE) + if(!ventcrawler && living_caller.mob_size != MOB_SIZE_TINY) return FALSE - var/atom/movable/M = caller - if(M?.pulling) - return CanAStarPass(ID, to_dir, M.pulling) + + if(caller?.pulling) + return CanAStarPass(ID, to_dir, caller.pulling) return TRUE //diseases, stings, etc can pass /obj/structure/plasticflaps/CanAllowThrough(atom/movable/A, turf/T) diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 17f9733e2d9..09795451801 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -109,11 +109,10 @@ if(locate(/obj/structure/table) in get_turf(mover)) return TRUE -/obj/structure/table/CanAStarPass(ID, dir, caller) +/obj/structure/table/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) . = !density - if(ismovable(caller)) - var/atom/movable/mover = caller - . = . || (mover.pass_flags & PASSTABLE) + if(istype(caller)) + . = . || (caller.pass_flags & PASSTABLE) /obj/structure/table/proc/tableplace(mob/living/user, mob/living/pushed_mob) pushed_mob.forceMove(loc) diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index c1113b50bc1..d678ab5d60f 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -342,7 +342,7 @@ /obj/structure/window/get_dumping_location(obj/item/storage/source,mob/user) return null -/obj/structure/window/CanAStarPass(ID, to_dir) +/obj/structure/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) if(!density) return TRUE if(fulltile || (dir == to_dir)) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 9cb66d364d1..b5dd3ab82e1 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -632,3 +632,23 @@ GLOBAL_LIST_EMPTY(station_turfs) if(!ismopable(movable_content)) continue movable_content.wash(clean_types) + +/** + * Returns adjacent turfs to this turf that are reachable, in all cardinal directions + * + * Arguments: + * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach + * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf + * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? +*/ +/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only) + var/static/space_type_cache = typecacheof(/turf/open/space) + . = list() + + for(var/iter_dir in GLOB.cardinals) + var/turf/turf_to_check = get_step(src,iter_dir) + if(!turf_to_check || (simulated_only && space_type_cache[turf_to_check.type])) + continue + if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, caller, ID)) + continue + . += turf_to_check diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm index 069af603899..9b0e8fab156 100644 --- a/code/modules/antagonists/changeling/powers/tiny_prick.dm +++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm @@ -47,7 +47,7 @@ return if(!isturf(user.loc)) return - if(!AStar(user, target.loc, /turf/proc/Distance, changeling.sting_range, simulated_only = FALSE)) + if(!get_path_to(user, target, max_distance = changeling.sting_range, simulated_only = FALSE)) return if(target.mind && target.mind.has_antag_datum(/datum/antagonist/changeling)) sting_feedback(user, target) diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 163db238a09..ce13bf6bba3 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -562,7 +562,7 @@ Pass a positive integer as an argument to override a bot's default speed. var/datum/job/captain/All = new/datum/job/captain all_access.access = All.get_access() - set_path(get_path_to(src, waypoint, /turf/proc/Distance_cardinal, 0, 200, id=all_access)) + set_path(get_path_to(src, waypoint, 200, id=all_access)) calling_ai = caller //Link the AI to the bot! ai_waypoint = waypoint @@ -745,6 +745,7 @@ Pass a positive integer as an argument to override a bot's default speed. access_card.access = user_access + prev_access //Adds the user's access, if any. mode = BOT_SUMMON speak("Responding.", radio_channel) + calc_summon_path() if("ejectpai") @@ -780,14 +781,14 @@ Pass a positive integer as an argument to override a bot's default speed. // given an optional turf to avoid /mob/living/simple_animal/bot/proc/calc_path(turf/avoid) check_bot_access() - set_path(get_path_to(src, patrol_target, /turf/proc/Distance_cardinal, 0, 120, id=access_card, exclude=avoid)) + set_path(get_path_to(src, patrol_target, 120, id=access_card, exclude=avoid)) /mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid) check_bot_access() INVOKE_ASYNC(src, .proc/do_calc_summon_path, avoid) /mob/living/simple_animal/bot/proc/do_calc_summon_path(turf/avoid) - set_path(get_path_to(src, summon_target, /turf/proc/Distance_cardinal, 0, 150, id=access_card, exclude=avoid)) + set_path(get_path_to(src, summon_target, 150, id=access_card, exclude=avoid)) if(!length(path)) //Cannot reach target. Give up and announce the issue. speak("Summon command failed, destination unreachable.",radio_channel) bot_reset() diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm index f3184bfefa2..127f5d83154 100644 --- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm +++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm @@ -265,7 +265,7 @@ if(!path || path.len == 0) //No path, need a new one //Try to produce a path to the target, and ignore airlocks to which it has access. - path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 0, 30, id=access_card) + path = get_path_to(src, target, 30, id=access_card) if(!bot_move(target)) add_to_ignore(target) target = null diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm index bfcee595c03..2ad3ba0fe99 100644 --- a/code/modules/mob/living/simple_animal/bot/firebot.dm +++ b/code/modules/mob/living/simple_animal/bot/firebot.dm @@ -238,7 +238,7 @@ if(target_fire && (get_dist(src, target_fire) > 2)) - path = get_path_to(src, get_turf(target_fire), /turf/proc/Distance_cardinal, 0, 30, 1, id=access_card) + path = get_path_to(src, target_fire, 30, 1, id=access_card) mode = BOT_MOVING if(!path.len) soft_reset() diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm index 426ef97ce18..c0ba903a853 100644 --- a/code/modules/mob/living/simple_animal/bot/floorbot.dm +++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm @@ -265,9 +265,9 @@ if(path.len == 0) if(!isturf(target)) var/turf/TL = get_turf(target) - path = get_path_to(src, TL, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, TL, 30, id=access_card,simulated_only = FALSE) else - path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, target, 30, id=access_card,simulated_only = FALSE) if(!bot_move(target)) add_to_ignore(target) diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm index 646abc094a9..8a0281af41c 100644 --- a/code/modules/mob/living/simple_animal/bot/medbot.dm +++ b/code/modules/mob/living/simple_animal/bot/medbot.dm @@ -375,10 +375,10 @@ return if(patient && path.len == 0 && (get_dist(src,patient) > 1)) - path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,id=access_card) + path = get_path_to(src, patient, 30,id=access_card) mode = BOT_MOVING if(!path.len) //try to get closer if you can't reach the patient directly - path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,1,id=access_card) + path = get_path_to(src, patient, 30,1,id=access_card) if(!path.len) //Do not chase a patient we cannot reach. soft_reset() diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index f67cf507def..6e3045641db 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -638,7 +638,7 @@ // calculates a path to the current destination // given an optional turf to avoid /mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null) - path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 250, id=access_card, exclude=avoid) + path = get_path_to(src, target, 250, id=access_card, exclude=avoid) // sets the current destination // signals all beacons matching the delivery code diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index d40ce093e68..72c26d57390 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -644,7 +644,7 @@ item = I break if(item) - if(!AStar(src, get_turf(item), /turf/proc/Distance_cardinal)) + if(!get_path_to(src, item)) item = null continue return item diff --git a/tgstation.dme b/tgstation.dme index bda0c28930f..49badea26c9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -168,7 +168,6 @@ #include "code\__HELPERS\_logging.dm" #include "code\__HELPERS\_string_lists.dm" #include "code\__HELPERS\areas.dm" -#include "code\__HELPERS\AStar.dm" #include "code\__HELPERS\bitflag_lists.dm" #include "code\__HELPERS\chat.dm" #include "code\__HELPERS\cmp.dm" @@ -189,6 +188,7 @@ #include "code\__HELPERS\mobs.dm" #include "code\__HELPERS\mouse_control.dm" #include "code\__HELPERS\names.dm" +#include "code\__HELPERS\path.dm" #include "code\__HELPERS\priority_announce.dm" #include "code\__HELPERS\pronouns.dm" #include "code\__HELPERS\qdel.dm"