diff --git a/code/__DEFINES/path.dm b/code/__DEFINES/path.dm index 95713c5d36f..6a930699041 100644 --- a/code/__DEFINES/path.dm +++ b/code/__DEFINES/path.dm @@ -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) diff --git a/code/__HELPERS/path.dm b/code/__HELPERS/path.dm deleted file mode 100644 index fb6d9c27b2b..00000000000 --- a/code/__HELPERS/path.dm +++ /dev/null @@ -1,463 +0,0 @@ -/** - * 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. - * 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. - * * 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(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE) - var/list/path = 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 = CALLBACK(GLOBAL_PROC, /proc/pathfinding_finished, path) - if(!SSpathfinder.pathfind(caller, end, max_distance, mintargetdist, id, simulated_only, exclude, skip_first, diagonal_safety, await)) - return list() - - UNTIL(length(path)) - if(length(path) == 1 && path[1] == null || (QDELETED(caller) || QDELETED(end))) // It's trash, just hand back null to make it easy - return list() - return path - -/// Uses funny pass by reference bullshit to take the path 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, list/path) - // We use += here to ensure the list is still pointing at the same thing - return_list += 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 - /// 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 - /// Ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary - var/diagonal_safety = TRUE - /// The callback to invoke when we're done working, passing in the completed var/list/path - var/datum/callback/on_finish - -/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, skip_first, diagonal_safety, datum/callback/on_finish) - 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.skip_first = skip_first - src.diagonal_safety = diagonal_safety - src.on_finish = on_finish - -/datum/pathfind/Destroy(force, ...) - . = ..() - SSpathfinder.active_pathing -= src - SSpathfinder.currentrun -= src - if(on_finish) - on_finish.Invoke(null) - on_finish = null - avoid = null - id = null - caller = null - open = 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() - start = get_turf(caller) - if(!start || !get_turf(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 - - 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 - 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() - 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 - -/** - * early_exit() is called when something goes wrong in processing, and we need to halt the pathfinding NOW - */ -/datum/pathfind/proc/early_exit() - on_finish.Invoke(null) - on_finish = null - qdel(src) - -/** - * Cleanup pass for the pathfinder. This tidies up the path, and fufills the pathfind's obligations - */ -/datum/pathfind/proc/finished() - //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_NULL(open) - - if(diagonal_safety) - path = diagonal_movement_safety() - if(length(path) > 0 && skip_first) - path.Cut(1,2) - on_finish.Invoke(path) - on_finish = null - qdel(src) - -/// 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. - * 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 JPS, so it'd better be fast - * - * 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, atom/movable/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) || midstep_turf.LinkBlockedWithAccess(destination_turf, caller, 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)) - 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_astar_pass == CANASTARPASS_DENSITY) - continue - if(!border.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) - // This is an optimization because of the massive call count of this code - if(!iter_object.density && iter_object.can_astar_pass == CANASTARPASS_DENSITY) - continue - if(!iter_object.CanAStarPass(ID, reverse_dir, caller, no_id)) - return TRUE - return FALSE diff --git a/code/__HELPERS/paths/jps.dm b/code/__HELPERS/paths/jps.dm new file mode 100644 index 00000000000..6ef883c7d2b --- /dev/null +++ b/code/__HELPERS/paths/jps.dm @@ -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 diff --git a/code/__HELPERS/paths/path.dm b/code/__HELPERS/paths/path.dm new file mode 100644 index 00000000000..14241ef8e70 --- /dev/null +++ b/code/__HELPERS/paths/path.dm @@ -0,0 +1,379 @@ +/** + * 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 CanAstarPass 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.CanAStarPass(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_astar_pass == CANASTARPASS_DENSITY) + continue + if(!border.CanAStarPass(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_astar_pass == CANASTARPASS_DENSITY) + continue + if(!iter_object.CanAStarPass(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 CanAStarPass 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 + /// 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 + /// 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 + + /// 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 + + /// Weakref to the caller used to generate this info + /// Should not use this almost ever, it's for context and to allow for proc chains that + /// Require a movable + var/datum/weakref/caller_ref = null + +/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) + src.access = access.Copy() + src.no_id = no_id + + if(isnull(construct_from)) + return + + src.caller_ref = WEAKREF(construct_from) + src.pass_flags = construct_from.pass_flags + src.movement_type = construct_from.movement_type + src.thrown = !!construct_from.throwing + src.anchored = construct_from.anchored + src.has_gravity = construct_from.has_gravity() + if(ismob(construct_from)) + var/mob/living/mob_construct = construct_from + src.incapacitated = mob_construct.incapacitated() + 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 = HAS_TRAIT(living_construct, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(living_construct, TRAIT_VENTCRAWLER_NUDE) + src.mob_size = living_construct.mob_size + src.incorporeal_move = living_construct.incorporeal_move + 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_ref" + ASSERT("caller_ref" in lamb.vars, "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" + ASSERT("pulling_info" in lamb.vars, "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 diff --git a/code/__HELPERS/paths/sssp.dm b/code/__HELPERS/paths/sssp.dm new file mode 100644 index 00000000000..f735c664694 --- /dev/null +++ b/code/__HELPERS/paths/sssp.dm @@ -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 + 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 ..() diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index 6ff9d39d531..0b464a9a196 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -301,7 +301,7 @@ * repath_delay - How often we're allowed to recalculate our path * max_path_length - The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite) * miminum_distance - 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 + * access - A list representing what access we have and what doors we can open * simulated_only - Whether we consider turfs without atmos simulation (AKA do we want to ignore space) * avoid - 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 things @@ -318,7 +318,7 @@ repath_delay, max_path_length, minimum_distance, - obj/item/card/id/id, + list/access, simulated_only, turf/avoid, skip_first, @@ -339,7 +339,7 @@ repath_delay, max_path_length, minimum_distance, - id, + access, simulated_only, avoid, skip_first, @@ -352,8 +352,8 @@ var/max_path_length ///Minimum distance to the target before path returns var/minimum_distance - ///An ID card representing what access we have and what doors we can open. Kill me - var/obj/item/card/id/id + ///A list representing what access we have and what doors we can open. + var/list/access ///Whether we consider turfs without atmos simulation (AKA do we want to ignore space) var/simulated_only ///A perticular turf to avoid @@ -366,30 +366,28 @@ COOLDOWN_DECLARE(repath_cooldown) ///Bool used to determine if we're already making a path in JPS. this prevents us from re-pathing while we're already busy. var/is_pathing = FALSE - ///Callback to invoke once we make a path - var/datum/callback/on_finish_callback + ///Callbacks to invoke once we make a path + var/list/datum/callback/on_finish_callbacks /datum/move_loop/has_target/jps/New(datum/movement_packet/owner, datum/controller/subsystem/movement/controller, atom/moving, priority, flags, datum/extra_info) . = ..() - on_finish_callback = CALLBACK(src, PROC_REF(on_finish_pathing)) + on_finish_callbacks += CALLBACK(src, PROC_REF(on_finish_pathing)) -/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, obj/item/card/id/id, simulated_only, turf/avoid, skip_first, list/initial_path) +/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, list/initial_path) . = ..() if(!.) return src.repath_delay = repath_delay src.max_path_length = max_path_length src.minimum_distance = minimum_distance - src.id = id + src.access = access src.simulated_only = simulated_only src.avoid = avoid src.skip_first = skip_first movement_path = initial_path?.Copy() - if(isidcard(id)) - RegisterSignal(id, COMSIG_QDELETING, PROC_REF(handle_no_id)) //I prefer erroring to harddels. If this breaks anything consider making id info into a datum or something -/datum/move_loop/has_target/jps/compare_loops(datum/move_loop/loop_type, priority, flags, extra_info, delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, obj/item/card/id/id, simulated_only, turf/avoid, skip_first, initial_path) - if(..() && repath_delay == src.repath_delay && max_path_length == src.max_path_length && minimum_distance == src.minimum_distance && id == src.id && simulated_only == src.simulated_only && avoid == src.avoid) +/datum/move_loop/has_target/jps/compare_loops(datum/move_loop/loop_type, priority, flags, extra_info, delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, initial_path) + if(..() && repath_delay == src.repath_delay && max_path_length == src.max_path_length && minimum_distance == src.minimum_distance && access ~= src.access && simulated_only == src.simulated_only && avoid == src.avoid) return TRUE return FALSE @@ -403,21 +401,16 @@ movement_path = null /datum/move_loop/has_target/jps/Destroy() - id = null //Kill me avoid = null - on_finish_callback = null + on_finish_callbacks = null return ..() -/datum/move_loop/has_target/jps/proc/handle_no_id() - SIGNAL_HANDLER - id = null - ///Tries to calculate a new path for this moveloop. /datum/move_loop/has_target/jps/proc/recalculate_path() if(!COOLDOWN_FINISHED(src, repath_cooldown)) return COOLDOWN_START(src, repath_cooldown, repath_delay) - if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, id, simulated_only, avoid, skip_first, on_finish = on_finish_callback)) + if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, access, simulated_only, avoid, skip_first, on_finish = on_finish_callbacks)) is_pathing = TRUE SEND_SIGNAL(src, COMSIG_MOVELOOP_JPS_REPATH) diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm index c503826b9bd..fa1a7af5c85 100644 --- a/code/controllers/subsystem/pathfinder.dm +++ b/code/controllers/subsystem/pathfinder.dm @@ -8,6 +8,10 @@ SUBSYSTEM_DEF(pathfinder) 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() @@ -23,6 +27,7 @@ SUBSYSTEM_DEF(pathfinder) /datum/controller/subsystem/pathfinder/fire(resumed) if(!resumed) src.currentrun = active_pathing.Copy() + src.currentmaps = deep_copy_list(source_to_maps) // Dies of sonic speed from caching datum var reads var/list/currentrun = src.currentrun @@ -38,10 +43,165 @@ SUBSYSTEM_DEF(pathfinder) // Next please currentrun.len-- + // 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 + + 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, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE, datum/callback/on_finish) - var/datum/pathfind/path = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude, skip_first, diagonal_safety, on_finish) +/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 diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 9ce83becc17..f5af1409f9a 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -334,7 +334,7 @@ multiple modular subtrees with behaviors set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained)) -/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding, likely pointing to whatever ID slot is relevant +/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding. Return the access list you want to use /datum/ai_controller/proc/get_access() return diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm index 6883642b689..1ac19d01022 100644 --- a/code/datums/ai/dog/dog_controller.dm +++ b/code/datums/ai/dog/dog_controller.dm @@ -42,4 +42,4 @@ if(!istype(corgi_pawn)) return - return corgi_pawn.access_card + return corgi_pawn.access_card.GetAccess() diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm index da46735ec36..15083399133 100644 --- a/code/datums/ai/movement/ai_movement_jps.dm +++ b/code/datums/ai/movement/ai_movement_jps.dm @@ -15,7 +15,7 @@ repath_delay = 0.5 SECONDS, max_path_length = AI_MAX_PATH_LENGTH, minimum_distance = controller.get_minimum_distance(), - id = controller.get_access(), + access = controller.get_access(), subsystem = SSai_movement, extra_info = controller, ) @@ -28,5 +28,5 @@ SIGNAL_HANDLER var/datum/ai_controller/controller = source.extra_info - source.id = controller.get_access() + source.access = controller.get_access() source.minimum_distance = controller.get_minimum_distance() diff --git a/code/datums/ai/objects/mod.dm b/code/datums/ai/objects/mod.dm index ff3a8c6d169..2bb555d281b 100644 --- a/code/datums/ai/objects/mod.dm +++ b/code/datums/ai/objects/mod.dm @@ -28,7 +28,7 @@ queue_behavior(/datum/ai_behavior/mod_attach) /datum/ai_controller/mod/get_access() - return id_card + return id_card.GetAccess() /datum/ai_behavior/mod_attach behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT|AI_BEHAVIOR_MOVE_AND_PERFORM diff --git a/code/datums/ai/oldhostile/hostile_tameable.dm b/code/datums/ai/oldhostile/hostile_tameable.dm index d75eb308731..5c96eca17da 100644 --- a/code/datums/ai/oldhostile/hostile_tameable.dm +++ b/code/datums/ai/oldhostile/hostile_tameable.dm @@ -62,7 +62,7 @@ if(!istype(simple_pawn)) return - return simple_pawn.access_card + return simple_pawn.access_card.GetAccess() /datum/ai_controller/hostile_friend/proc/on_ridden_driver_move(atom/movable/movable_parent, mob/living/user, direction) SIGNAL_HANDLER diff --git a/code/datums/components/omen.dm b/code/datums/components/omen.dm index 56bf4cc9e20..3341a9c7e95 100644 --- a/code/datums/components/omen.dm +++ b/code/datums/components/omen.dm @@ -91,7 +91,7 @@ for(var/turf/the_turf as anything in get_adjacent_open_turfs(living_guy)) if(istype(the_turf, /turf/open/floor/glass/reinforced/tram)) // don't fall off the tram bridge, we want to hit you instead return - if(the_turf.zPassOut(living_guy, DOWN) && living_guy.can_z_move(DOWN, the_turf, z_move_flags = ZMOVE_FALL_FLAGS)) + if(living_guy.can_z_move(DOWN, the_turf, z_move_flags = ZMOVE_FALL_FLAGS)) to_chat(living_guy, span_warning("A malevolent force guides you towards the edge...")) living_guy.throw_at(the_turf, 1, 10, force = MOVE_FORCE_EXTREMELY_STRONG) if(!permanent) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index e590fb66019..9f3281bb348 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -2128,16 +2128,14 @@ * 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 CanAStarPass will always return true if density is FALSE * If this is NOT you, ensure you edit your can_astar_pass variable. Check __DEFINES/path.dm **/ -/atom/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) - if(caller && (caller.pass_flags & pass_flags_self)) +/atom/proc/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + if(pass_info.pass_flags & pass_flags_self) return TRUE . = !density diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 4f7326bcc86..9208ea7a9a2 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -432,7 +432,7 @@ else to_chat(src, span_warning("You are not Superman.")) return FALSE - if(!(z_move_flags & ZMOVE_IGNORE_OBSTACLES) && !(start.zPassOut(src, direction, destination, (z_move_flags & ZMOVE_ALLOW_ANCHORED)) && destination.zPassIn(src, direction, start))) + if((!(z_move_flags & ZMOVE_IGNORE_OBSTACLES) && !(start.zPassOut(direction) && destination.zPassIn(direction))) || (!(z_move_flags & ZMOVE_ALLOW_ANCHORED) && anchored)) if(z_move_flags & ZMOVE_FEEDBACK) to_chat(rider || src, span_warning("You couldn't move there!")) return FALSE diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index 1aba12fe6e2..7c903db123b 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -1406,9 +1406,9 @@ assemblytype = initial(airlock.assemblytype) update_appearance() -/obj/machinery/door/airlock/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) +/obj/machinery/door/airlock/CanAStarPass(to_dir, datum/can_pass_info/pass_info) //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() && !no_id) + return !density || (check_access_list(pass_info.access) && !locked && hasPower() && !pass_info.no_id) /obj/machinery/door/airlock/emag_act(mob/user, obj/item/card/emag/emag_card) if(!operating && density && hasPower() && !(obj_flags & EMAGGED)) diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm index 94b1d1270ae..b38ecb7c60f 100644 --- a/code/game/machinery/doors/firedoor.dm +++ b/code/game/machinery/doors/firedoor.dm @@ -725,7 +725,7 @@ if(!(border_dir == dir)) //Make sure looking at appropriate border return TRUE -/obj/machinery/door/firedoor/border_only/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) +/obj/machinery/door/firedoor/border_only/CanAStarPass(to_dir, datum/can_pass_info/pass_info) return !density || (dir != to_dir) /obj/machinery/door/firedoor/border_only/proc/on_exit(datum/source, atom/movable/leaving, direction) diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm index 30b4e04c4cc..c70d7a751ed 100644 --- a/code/game/machinery/doors/windowdoor.dm +++ b/code/game/machinery/doors/windowdoor.dm @@ -186,8 +186,8 @@ return TRUE //used in the AStar algorithm to determinate if the turf the door is on is passable -/obj/machinery/door/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) - return !density || (dir != to_dir) || (check_access(ID) && hasPower() && !no_id) +/obj/machinery/door/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + return !density || (dir != to_dir) || (check_access_list(pass_info.access) && hasPower() && !pass_info.no_id) /obj/machinery/door/window/proc/on_exit(datum/source, atom/movable/leaving, direction) SIGNAL_HANDLER diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm index 787ce602a3a..73e95ab48cd 100644 --- a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm +++ b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm @@ -137,9 +137,10 @@ if(!istype(location)) return FALSE + var/datum/can_pass_info/info = new(no_id = TRUE) for(var/iter_dir in GLOB.cardinals) var/turf/spread_turf = get_step(src, iter_dir) - if(spread_turf?.density || spread_turf.LinkBlockedWithAccess(spread_turf, no_id = TRUE)) + if(spread_turf?.density || spread_turf.LinkBlockedWithAccess(spread_turf, info)) continue var/obj/effect/particle_effect/fluid/foam/foundfoam = locate() in spread_turf //Don't spread foam where there's already foam! diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 63a040d6dfe..7b697368578 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -376,10 +376,12 @@ if((mover.pass_flags & PASSGRILLE) || isprojectile(mover)) return prob(girderpasschance) -/obj/structure/girder/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) - . = !density - if(caller) - . = . || (caller.pass_flags & PASSGRILLE) +/obj/structure/girder/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + if(!density) + return TRUE + if(pass_info.pass_flags & PASSGRILLE) + return TRUE + return FALSE /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 bfe7f0b4a88..3d68af42dc7 100644 --- a/code/game/objects/structures/grille.dm +++ b/code/game/objects/structures/grille.dm @@ -175,10 +175,12 @@ if(!. && isprojectile(mover)) return prob(30) -/obj/structure/grille/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) - . = !density - if(caller) - . = . || (caller.pass_flags & PASSGRILLE) +/obj/structure/grille/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + if(!density) + return TRUE + if(pass_info.pass_flags & PASSGRILLE) + return TRUE + return FALSE /obj/structure/grille/wirecutter_act(mob/living/user, obj/item/tool) add_fingerprint(user) diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm index 7bb79de7f67..b594be80cf6 100644 --- a/code/game/objects/structures/plasticflaps.dm +++ b/code/game/objects/structures/plasticflaps.dm @@ -83,18 +83,15 @@ return FALSE return TRUE -/obj/structure/plasticflaps/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) - if(isliving(caller)) - if(isbot(caller)) +/obj/structure/plasticflaps/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + if(pass_info.is_living) + if(pass_info.is_bot) return TRUE - - 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) + if(pass_info.can_ventcrawl && pass_info.mob_size != MOB_SIZE_TINY) return FALSE - if(caller?.pulling) - return CanAStarPass(ID, to_dir, caller.pulling, no_id = no_id) + if(pass_info.pulling_info) + return CanAStarPass(to_dir, pass_info.pulling_info) return TRUE //diseases, stings, etc can pass diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm index f88ba15ecc5..682b691863c 100644 --- a/code/game/objects/structures/railings.dm +++ b/code/game/objects/structures/railings.dm @@ -117,7 +117,7 @@ return . || mover.throwing || mover.movement_type & (FLYING | FLOATING) return TRUE -/obj/structure/railing/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) +/obj/structure/railing/CanAStarPass(to_dir, datum/can_pass_info/pass_info) if(!(to_dir & dir)) return TRUE return ..() diff --git a/code/game/objects/structures/safe.dm b/code/game/objects/structures/safe.dm index 5d42331b5df..9c1a8f1c4f3 100644 --- a/code/game/objects/structures/safe.dm +++ b/code/game/objects/structures/safe.dm @@ -121,9 +121,9 @@ FLOOR SAFES if(open) var/list/contents_names = list() data["contents"] = contents_names - for(var/obj/O in contents) - contents_names[++contents_names.len] = list("name" = O.name, "sprite" = O.icon_state) - user << browse_rsc(icon(O.icon, O.icon_state), "[O.icon_state].png") + for(var/obj/jewel in contents) + contents_names[++contents_names.len] = list("name" = jewel.name, "sprite" = jewel.icon_state) + user << browse_rsc(icon(jewel.icon, jewel.icon_state), "[jewel.icon_state].png") return data diff --git a/code/game/objects/structures/stairs.dm b/code/game/objects/structures/stairs.dm index 5e4078b5afa..169f76a4b57 100644 --- a/code/game/objects/structures/stairs.dm +++ b/code/game/objects/structures/stairs.dm @@ -94,7 +94,8 @@ var/turf/checking = get_step_multiz(get_turf(src), UP) if(!istype(checking)) return - if(!checking.zPassIn(climber, UP, get_turf(src))) + // I'm only interested in if the pass is unobstructed, not if the mob will actually make it + if(!climber.can_z_move(UP, get_turf(src), checking, z_move_flags = ZMOVE_ALLOW_BUCKLED)) return var/turf/target = get_step_multiz(get_turf(src), (dir|UP)) if(istype(target) && !climber.can_z_move(DOWN, target, z_move_flags = ZMOVE_FALL_FLAGS)) //Don't throw them into a tile that will just dump them back down. diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 9bc6814c9d5..7219bb69c2e 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -149,10 +149,12 @@ if(locate(/obj/structure/table) in get_turf(mover)) return TRUE -/obj/structure/table/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) - . = !density - if(caller) - . = . || (caller.pass_flags & PASSTABLE) +/obj/structure/table/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + if(!density) + return TRUE + if(pass_info.pass_flags & PASSTABLE) + return TRUE + return FALSE /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 2b1bab756d7..6884888e405 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -431,7 +431,7 @@ /obj/structure/window/get_dumping_location() return null -/obj/structure/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) +/obj/structure/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info) if(!density) return TRUE if(fulltile || (dir == to_dir)) diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm index 7abd965e4e0..61cd57c6a52 100644 --- a/code/game/turfs/open/_open.dm +++ b/code/game/turfs/open/_open.dm @@ -52,22 +52,22 @@ . += mutable_appearance(damaged_dmi, pick(broken_states())) //direction is direction of travel of A -/turf/open/zPassIn(atom/movable/A, direction, turf/source) - if(direction == DOWN) - for(var/obj/O in contents) - if(O.obj_flags & BLOCK_Z_IN_DOWN) - return FALSE - return TRUE - return FALSE +/turf/open/zPassIn(direction) + if(direction != DOWN) + return FALSE + for(var/obj/on_us in contents) + if(on_us.obj_flags & BLOCK_Z_IN_DOWN) + return FALSE + return TRUE -//direction is direction of travel of A -/turf/open/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement) - if(direction == UP) - for(var/obj/O in contents) - if(O.obj_flags & BLOCK_Z_OUT_UP) - return FALSE - return TRUE - return FALSE +//direction is direction of travel of an atom +/turf/open/zPassOut(direction) + if(direction != UP) + return FALSE + for(var/obj/on_us in contents) + if(on_us.obj_flags & BLOCK_Z_OUT_UP) + return FALSE + return TRUE //direction is direction of travel of air /turf/open/zAirIn(direction, turf/source) diff --git a/code/game/turfs/open/openspace.dm b/code/game/turfs/open/openspace.dm index 309bda83c0e..8b8b7ca00c6 100644 --- a/code/game/turfs/open/openspace.dm +++ b/code/game/turfs/open/openspace.dm @@ -80,7 +80,7 @@ /turf/open/openspace/zAirOut() return TRUE -/turf/open/openspace/zPassIn(atom/movable/A, direction, turf/source) +/turf/open/openspace/zPassIn(direction) if(direction == DOWN) for(var/obj/contained_object in contents) if(contained_object.obj_flags & BLOCK_Z_IN_DOWN) @@ -93,9 +93,7 @@ return TRUE return FALSE -/turf/open/openspace/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement) - if(A.anchored && !allow_anchored_movement) - return FALSE +/turf/open/openspace/zPassOut(direction) if(direction == DOWN) for(var/obj/contained_object in contents) if(contained_object.obj_flags & BLOCK_Z_OUT_DOWN) @@ -154,8 +152,9 @@ /turf/open/openspace/rust_heretic_act() return FALSE -/turf/open/openspace/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) - if(caller && !caller.can_z_move(DOWN, src, null , ZMOVE_FALL_FLAGS)) //If we can't fall here (flying/lattice), it's fine to path through +/turf/open/openspace/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + var/atom/movable/our_movable = pass_info.caller_ref.resolve() + if(our_movable && !our_movable.can_z_move(DOWN, src, null, ZMOVE_FALL_FLAGS)) //If we can't fall here (flying/lattice), it's fine to path through return TRUE return FALSE diff --git a/code/game/turfs/open/space/space.dm b/code/game/turfs/open/space/space.dm index e1bab6bfe00..63a8f3449dd 100644 --- a/code/game/turfs/open/space/space.dm +++ b/code/game/turfs/open/space/space.dm @@ -285,7 +285,7 @@ GLOBAL_LIST_EMPTY(starlight) /turf/open/space/openspace/zAirOut() return TRUE -/turf/open/space/openspace/zPassIn(atom/movable/A, direction, turf/source) +/turf/open/space/openspace/zPassIn(direction) if(direction == DOWN) for(var/obj/contained_object in contents) if(contained_object.obj_flags & BLOCK_Z_IN_DOWN) @@ -298,9 +298,7 @@ GLOBAL_LIST_EMPTY(starlight) return TRUE return FALSE -/turf/open/space/openspace/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement) - if(A.anchored && !allow_anchored_movement) - return FALSE +/turf/open/space/openspace/zPassOut(direction) if(direction == DOWN) for(var/obj/contained_object in contents) if(contained_object.obj_flags & BLOCK_Z_OUT_DOWN) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 5895c7fd393..d2fd05460b8 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -295,20 +295,21 @@ GLOBAL_LIST_EMPTY(station_turfs) return TRUE return FALSE -//zPassIn doesn't necessarily pass an atom! -//direction is direction of travel of air -/turf/proc/zPassIn(atom/movable/A, direction, turf/source) +//The zpass procs exist to be overriden, not directly called +//use can_z_pass for that +///If we'd allow anything to travel into us +/turf/proc/zPassIn(direction) return FALSE -//direction is direction of travel of air -/turf/proc/zPassOut(atom/movable/A, direction, turf/destination, allow_anchored_movement) +///If we'd allow anything to travel out of us +/turf/proc/zPassOut(direction) return FALSE //direction is direction of travel of air /turf/proc/zAirIn(direction, turf/source) return FALSE -//direction is direction of travel of air +//direction is direction of travel /turf/proc/zAirOut(direction, turf/source) return FALSE @@ -520,9 +521,9 @@ GLOBAL_LIST_EMPTY(station_turfs) /turf/singularity_act() if(underfloor_accessibility < UNDERFLOOR_INTERACTABLE) - for(var/obj/O in contents) //this is for deleting things like wires contained in the turf - if(HAS_TRAIT(O, TRAIT_T_RAY_VISIBLE)) - O.singularity_act() + for(var/obj/on_top in contents) //this is for deleting things like wires contained in the turf + if(HAS_TRAIT(on_top, TRAIT_T_RAY_VISIBLE)) + on_top.singularity_act() ScrapeAway(flags = CHANGETURF_INHERIT_AIR) return(2) @@ -723,19 +724,20 @@ GLOBAL_LIST_EMPTY(station_turfs) * * 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 + * * access: A list that decides if we can gain access to doors that would otherwise block a turf * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? * * no_id: When true, doors with public access will count as impassible */ -/turf/proc/reachableAdjacentTurfs(atom/movable/caller, ID, simulated_only, no_id = FALSE) +/turf/proc/reachableAdjacentTurfs(atom/movable/caller, list/access, simulated_only, no_id = FALSE) var/static/space_type_cache = typecacheof(/turf/open/space) . = list() + var/datum/can_pass_info/pass_info = new(caller, access, no_id) 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, no_id = no_id)) + if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, pass_info)) continue . += turf_to_check diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index d29c24021ff..eb06d37c21e 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -567,6 +567,15 @@ if(held_item) . = held_item.GetID() +/** + * Returns the access list for this mob + */ +/mob/living/proc/get_access() + var/obj/item/card/id/id = get_idcard() + if(isnull(id)) + return list() + return id.GetAccess() + /mob/living/proc/get_id_in_hand() var/obj/item/held_item = get_active_held_item() if(!held_item) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index ed9c83a7836..4f56c1403bc 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -439,8 +439,8 @@ . = ..() if(. & EMP_PROTECT_CONTENTS) return - for(var/obj/O in contents) - O.emp_act(severity) + for(var/obj/inside in contents) + inside.emp_act(severity) ///Logs, gibs and returns point values of whatever mob is unfortunate enough to get eaten. /mob/living/singularity_act() diff --git a/code/modules/mob/living/navigation.dm b/code/modules/mob/living/navigation.dm index 44847a239c1..98f1080c791 100644 --- a/code/modules/mob/living/navigation.dm +++ b/code/modules/mob/living/navigation.dm @@ -63,7 +63,7 @@ stack_trace("Navigate target ([navigate_target]) is not an atom, somehow.") return - var/list/path = get_path_to(src, navigate_target, MAX_NAVIGATE_RANGE, mintargetdist = 1, id = get_idcard(), skip_first = FALSE) + var/list/path = get_path_to(src, navigate_target, MAX_NAVIGATE_RANGE, mintargetdist = 1, access = get_access(), skip_first = FALSE) if(!length(path)) balloon_alert(src, "no valid path with current access!") return diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 7bd48ab62fb..0ae9b56d5b4 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -708,9 +708,9 @@ Pass a positive integer as an argument to override a bot's default speed. bot_reset() //Reset a bot before setting it to call mode. //For giving the bot temporary all-access. This method is bad and makes me feel bad. Refactoring access to a component is for another PR. - var/obj/item/card/id/all_access = new /obj/item/card/id/advanced/gold/captains_spare() - set_path(get_path_to(src, waypoint, max_distance=200, id = all_access)) - qdel(all_access) + //Easier then building the list ourselves. I'm sorry. + var/static/obj/item/card/id/all_access = new /obj/item/card/id/advanced/gold/captains_spare() + set_path(get_path_to(src, waypoint, max_distance=200, access = all_access.GetAccess())) calling_ai = caller //Link the AI to the bot! ai_waypoint = waypoint @@ -924,12 +924,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, max_distance=120, id=access_card, exclude=avoid)) + set_path(get_path_to(src, patrol_target, max_distance=120, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL)) /mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid) check_bot_access() var/datum/callback/path_complete = CALLBACK(src, PROC_REF(on_summon_path_finish)) - SSpathfinder.pathfind(src, summon_target, max_distance=150, id=access_card, exclude=avoid, on_finish = path_complete) + SSpathfinder.pathfind(src, summon_target, max_distance=150, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL, on_finish=list(path_complete)) /mob/living/simple_animal/bot/proc/on_summon_path_finish(list/path) set_path(path) diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm index 71008588465..21186be8bb0 100644 --- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm +++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm @@ -291,7 +291,7 @@ return if(target && path.len == 0 && (get_dist(src,target) > 1)) - path = get_path_to(src, target, max_distance=30, mintargetdist=1, id=access_card) + path = get_path_to(src, target, max_distance=30, mintargetdist=1, access=access_card.GetAccess()) mode = BOT_MOVING if(length(path) == 0) add_to_ignore(target) diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm index 29006613e42..59d08fe7339 100644 --- a/code/modules/mob/living/simple_animal/bot/firebot.dm +++ b/code/modules/mob/living/simple_animal/bot/firebot.dm @@ -239,7 +239,7 @@ if(target_fire && (get_dist(src, target_fire) > 2)) - path = get_path_to(src, target_fire, max_distance=30, mintargetdist=1, id=access_card) + path = get_path_to(src, target_fire, max_distance=30, mintargetdist=1, access=access_card.GetAccess()) 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 b2f7f418589..17e918f0795 100644 --- a/code/modules/mob/living/simple_animal/bot/floorbot.dm +++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm @@ -249,9 +249,9 @@ if(!length(path)) if(!isturf(target)) var/turf/TL = get_turf(target) - path = get_path_to(src, TL, max_distance=30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, TL, max_distance=30, access=access_card.GetAccess(), simulated_only = FALSE) else - path = get_path_to(src, target, max_distance=30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, target, max_distance=30, access=access_card.GetAccess(), 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 9c06465b8a5..acdf21c9fc8 100644 --- a/code/modules/mob/living/simple_animal/bot/medbot.dm +++ b/code/modules/mob/living/simple_animal/bot/medbot.dm @@ -493,10 +493,10 @@ return if(patient && path.len == 0 && (get_dist(src,patient) > 1)) - path = get_path_to(src, patient, max_distance=30, id=access_card) + path = get_path_to(src, patient, max_distance=30, access=access_card.GetAccess()) mode = BOT_MOVING if(!path.len) //try to get closer if you can't reach the patient directly - path = get_path_to(src, patient, max_distance=30, mintargetdist=1, id=access_card) + path = get_path_to(src, patient, max_distance=30, mintargetdist=1, access=access_card.GetAccess()) 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 c6ae677136a..df180631b1e 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -588,7 +588,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, max_distance=250, id=access_card, exclude=avoid) + path = get_path_to(src, target, max_distance=250, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL) // sets the current destination // signals all beacons matching the delivery code diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index e76af741a18..dd1bf3d6017 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -212,8 +212,8 @@ /obj/item/gun/emp_act(severity) . = ..() if(!(. & EMP_PROTECT_CONTENTS)) - for(var/obj/O in contents) - O.emp_act(severity) + for(var/obj/inside in contents) + inside.emp_act(severity) /obj/item/gun/attack_self_secondary(mob/user, modifiers) . = ..() diff --git a/code/modules/wiremod/components/action/pathfind.dm b/code/modules/wiremod/components/action/pathfind.dm index e7dcb160207..9820d9b81e0 100644 --- a/code/modules/wiremod/components/action/pathfind.dm +++ b/code/modules/wiremod/components/action/pathfind.dm @@ -56,9 +56,11 @@ if(isnull(target_Y)) return - var/atom/path_id = id_card.value - if(path_id && !isidcard(path_id)) - path_id = null + var/list/access = list() + if(isidcard(id_card.value)) + var/obj/item/card/id/id = id_card.value + access = id.GetAccess() + else if (id_card.value) failed.set_output(COMPONENT_SIGNAL) reason_failed.set_output("Object marked is not an ID! Using no ID instead.") @@ -98,7 +100,7 @@ TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME) old_dest = destination - path = get_path_to(src, destination, max_range, id=path_id) + path = get_path_to(src, destination, max_range, access=access) if(length(path) == 0 || !path)// Check if we can even path there next_turf = null failed.set_output(COMPONENT_SIGNAL) diff --git a/tgstation.dme b/tgstation.dme index b817774cef8..c59839bced3 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -512,7 +512,6 @@ #include "code\__HELPERS\mouse_control.dm" #include "code\__HELPERS\nameof.dm" #include "code\__HELPERS\names.dm" -#include "code\__HELPERS\path.dm" #include "code\__HELPERS\piping_colors_lists.dm" #include "code\__HELPERS\priority_announce.dm" #include "code\__HELPERS\pronouns.dm" @@ -570,6 +569,9 @@ #include "code\__HELPERS\logging\transport.dm" #include "code\__HELPERS\logging\ui.dm" #include "code\__HELPERS\logging\virus.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"