diff --git a/code/__defines/_flags/turf_flags_ch.dm b/code/__defines/_flags/turf_flags_ch.dm new file mode 100644 index 0000000000..0ae1499177 --- /dev/null +++ b/code/__defines/_flags/turf_flags_ch.dm @@ -0,0 +1,52 @@ +//* /turf_flags var on /turf +/// This is used in literally one place, turf.dm, to block ethwereal jaunt. +#define NO_JAUNT (1<<0) +/// Unused reservation turf +#define UNUSED_RESERVATION_TURF (1<<2) +/// queued for planet turf addition +#define TURF_PLANET_QUEUED (1<<3) +/// registered to a planet +#define TURF_PLANET_REGISTERED (1<<4) +/// queued for ZAS rebuild +#define TURF_ZONE_REBUILD_QUEUED (1<<5) + +///CITMAIN TURF FLAGS - Completely unused +/* +/// If a turf can be made dirty at roundstart. This is also used in areas. +#define CAN_BE_DIRTY (1<<3) +/// Should this tile be cleaned up and reinserted into an excited group? +#define EXCITED_CLEANUP (1<<4) +/// Blocks lava rivers being generated on the turf +#define NO_LAVA_GEN (1<<5) +/// Blocks ruins spawning on the turf +#define NO_RUINS (1<<6) +*/ +/* +DEFINE_BITFIELD(turf_flags, list( + BITFIELD(NO_JAUNT), + BITFIELD(UNUSED_RESERVATION_TURF), + BITFIELD(TURF_PLANET_QUEUED), + BITFIELD(TURF_PLANET_REGISTERED), + BITFIELD(TURF_ZONE_REBUILD_QUEUED), +)) +*/ +//* /turf_path_danger var on /turf +/// lava, fire, etc +#define TURF_PATH_DANGER_BURN (1<<0) +/// openspace, chasms, etc +#define TURF_PATH_DANGER_FALL (1<<1) +/// will just fucking obliterate you +#define TURF_PATH_DANGER_ANNIHILATION (1<<2) +/// this, is literally space. +#define TURF_PATH_DANGER_SPACE (1<<3) +/* +DEFINE_SHARED_BITFIELD(turf_path_danger, list( + "turf_path_danger", + "turf_path_danger_ignore", +), list( + BITFIELD(TURF_PATH_DANGER_BURN), + BITFIELD(TURF_PATH_DANGER_FALL), + BITFIELD(TURF_PATH_DANGER_ANNIHILATION), + BITFIELD(TURF_PATH_DANGER_SPACE), +)) +*/ diff --git a/code/__defines/dcs/signals_ch.dm b/code/__defines/dcs/signals_ch.dm index 2876a0acc5..eb8b2cbbfa 100644 --- a/code/__defines/dcs/signals_ch.dm +++ b/code/__defines/dcs/signals_ch.dm @@ -4,3 +4,5 @@ #define COMSIG_BELLY_UPDATE_VORE_FX "update_vore_fx" ///from /obj/belly/process() #define COMSIG_BELLY_UPDATE_PREY_LOOP "update_prey_loop" +/// COMSIG used to get messages where they need to go +#define COMSIG_VISIBLE_MESSAGE "visible_message" diff --git a/code/_helpers/distance_ch.dm b/code/_helpers/distance_ch.dm new file mode 100644 index 0000000000..34841d447a --- /dev/null +++ b/code/_helpers/distance_ch.dm @@ -0,0 +1,37 @@ +/** + * checks distance from one thing to another but automatically resolving for turf / nesting + */ +/proc/in_range_of(atom/A, atom/B, dist = 1) + return game_range_to(A, B) <= dist + +/** + * gets real dist from A to B, including resolving for turf. if not the same Z, returns infinity. + */ +/proc/game_range_to(atom/A, atom/B) + A = get_turf(A) + B = get_turf(B) + return A.z == B.z? get_dist(A, B) : INFINITY + +/** + * real dist because byond dist doesn't go above 127 :/ + * + * accepts **TURFS** + */ +/proc/get_chebyshev_dist(turf/A, turf/B) + return max(abs(A.x - B.x), abs(A.y - B.y)) + +/** + * real euclidean dist + * + * accepts **TURFS** + */ +/proc/get_euclidean_dist(turf/A, turf/B) + return sqrt((A.x - B.x) ** 2 + (A.y - B.y) ** 2) + +/** + * real taxicab dist + * + * accepts **TURFS** + */ +/proc/get_manhattan_dist(turf/A, turf/B) + return abs(A.x - B.x) + abs(A.y - B.y) diff --git a/code/_helpers/game.dm b/code/_helpers/game.dm index b43b4abaa1..2fec4540e8 100644 --- a/code/_helpers/game.dm +++ b/code/_helpers/game.dm @@ -68,6 +68,7 @@ return heard + /proc/isStationLevel(var/level) return level in using_map.station_levels diff --git a/code/_helpers/graphs/astar_ch.dm b/code/_helpers/graphs/astar_ch.dm new file mode 100644 index 0000000000..1a0079c465 --- /dev/null +++ b/code/_helpers/graphs/astar_ch.dm @@ -0,0 +1,128 @@ +// todo: DO NOT FUCKING USE THIS +// it is *EXTREMELY* inefficient, and scales up quadratically in time complexity +// DO NOT USE THIS UNTIL IT IS REWRITTEN +// notably that "bad node trimming" is actually horrifying. + +/** + * A Star pathfinding algorithm + * + * This file's AStar should not be used generally; it's the generic graph search algorithm, as opposed + * to the optimized turf-grid-only search algorithm. + * + * Returns a list of tiles forming a path from A to B, taking dense objects as well as walls, and the orientation of + * windows along the route into account. + * + * + * Use: + * your_list = AStar(start location, end location, adjacent turf proc, distance proc) + * For the adjacent turf proc i wrote: + * /turf/proc/AdjacentTurfs + * And for the distance one i wrote: + * /turf/proc/Distance + * + * So an example use might be: + * + * src.path_list = AStar(src.loc, target.loc, TYPE_PROC_REF(/turf, AdjacentTurfs), TYPE_PROC_REF(/turf, Distance)) + * + * Note: The path is returned starting at the END node, so i wrote reverselist to reverse it for ease of use. + * + * src.path_list = reverselist(src.pathlist) + * + * Then to start on the path, all you need to do it: + * Step_to(src, src.path_list[1]) + * src.path_list -= src.path_list[1] or equivilent to remove that node from the list. + * + * Optional extras to add on (in order): + * MaxNodes: The maximum number of nodes the returned path can be (0 = infinite) + * Maxnodedepth: The maximum number of nodes to search (default: 30, 0 = infinite) + * Mintargetdist: Minimum distance to the target before path returns, could be used to get + * near a target, but not right to it - for an AI mob with a gun, for example. + * Minnodedist: Minimum number of nodes to return in the path, could be used to give a path a minimum + * length to avoid portals or something i guess?? Not that they're counted right now but w/e. + */ + +// Modified to provide ID argument - supplied to 'adjacent' proc, defaults to null +// Used for checking if route exists through a door which can be opened + +// Also added 'exclude' turf to avoid travelling over; defaults to null + +/datum/graph_astar_node + var/datum/position + var/datum/graph_astar_node/previous_node + + var/best_estimated_cost + var/estimated_cost + var/known_cost + var/cost + var/nodes_traversed + +/datum/graph_astar_node/New(_position, _previous_node, _known_cost, _cost, _nodes_traversed) + position = _position + previous_node = _previous_node + + known_cost = _known_cost + cost = _cost + estimated_cost = cost + known_cost + + best_estimated_cost = estimated_cost + nodes_traversed = _nodes_traversed + +/proc/cmp_graph_astar_node(datum/graph_astar_node/a, datum/graph_astar_node/b) + return a.estimated_cost - b.estimated_cost + +/proc/graph_astar(start, end, adjacent, dist, max_nodes, max_node_depth = 30, min_target_dist = 0, min_node_dist, id, datum/exclude) + var/datum/priority_queue/open = new /datum/priority_queue(/proc/cmp_graph_astar_node) + var/list/closed = list() + var/list/path + var/list/path_node_by_position = list() + start = get_turf(start) + if(!start) + return 0 + + open.enqueue(new /datum/graph_astar_node(start, null, 0, call(start, dist)(end), 0)) + + while(!open.is_empty() && !path) + var/datum/graph_astar_node/current = open.dequeue() + closed.Add(current.position) + + if(current.position == end || call(current.position, dist)(end) <= min_target_dist) + path = new /list(current.nodes_traversed + 1) + path[path.len] = current.position + var/index = path.len - 1 + + while(current.previous_node) + current = current.previous_node + path[index--] = current.position + break + + if(min_node_dist && max_node_depth) + if(call(current.position, min_node_dist)(end) + current.nodes_traversed >= max_node_depth) + continue + + if(max_node_depth) + if(current.nodes_traversed >= max_node_depth) + continue + + for(var/datum/datum in call(current.position, adjacent)(id)) + if(datum == exclude) + continue + + var/best_estimated_cost = current.estimated_cost + call(current.position, dist)(datum) + + //handle removal of sub-par positions + if(datum in path_node_by_position) + var/datum/graph_astar_node/target = path_node_by_position[datum] + if(target.best_estimated_cost) + if(best_estimated_cost + call(datum, dist)(end) < target.best_estimated_cost) + open.remove_entry(target) + else + continue + + var/datum/graph_astar_node/next_node = new (datum, current, best_estimated_cost, call(datum, dist)(end), current.nodes_traversed + 1) + path_node_by_position[datum] = next_node + open.enqueue(next_node) + + if(max_nodes && length(open.array) > max_nodes) + open.remove_index(length(open.array)) + + return path diff --git a/code/_helpers/legacy_tg_path_ch.dm b/code/_helpers/legacy_tg_path_ch.dm new file mode 100644 index 0000000000..e5685a3899 --- /dev/null +++ b/code/_helpers/legacy_tg_path_ch.dm @@ -0,0 +1,397 @@ +/** + * 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. + */ +////////////////////// +//datum/tg_heap object +////////////////////// + +/datum/tg_heap + var/list/L + var/cmp + +/datum/tg_heap/New(compare) + L = new() + cmp = compare + +/datum/tg_heap/Destroy(force, ...) + for(var/i in L) // because this is before the list helpers are loaded + qdel(i) + L = null + return ..() + +/datum/tg_heap/proc/is_empty() + return !length(L) + +//insert and place at its position a new node in the heap +/datum/tg_heap/proc/insert(A) + + L.Add(A) + swim(length(L)) + +//removes and returns the first element of the heap +//(i.e the max or the min dependant on the comparison function) +/datum/tg_heap/proc/pop() + if(!length(L)) + return 0 + . = L[1] + + L[1] = L[length(L)] + L.Cut(length(L)) + if(length(L)) + sink(1) + +//Get a node up to its right position in the heap +/datum/tg_heap/proc/swim(index) + var/parent = round(index * 0.5) + + while(parent > 0 && (call(cmp)(L[index],L[parent]) > 0)) + L.Swap(index,parent) + index = parent + parent = round(index * 0.5) + +//Get a node down to its right position in the heap +/datum/tg_heap/proc/sink(index) + var/g_child = get_greater_child(index) + + while(g_child > 0 && (call(cmp)(L[index],L[g_child]) < 0)) + L.Swap(index,g_child) + index = g_child + g_child = get_greater_child(index) + +//Returns the greater (relative to the comparison proc) of a node children +//or 0 if there's no child +/datum/tg_heap/proc/get_greater_child(index) + if(index * 2 > length(L)) + return 0 + + if(index * 2 + 1 > length(L)) + return index * 2 + + if(call(cmp)(L[index * 2],L[index * 2 + 1]) < 0) + return index * 2 + 1 + else + return index * 2 + +//Replaces a given node so it verify the heap condition +/datum/tg_heap/proc/resort(A) + var/index = L.Find(A) + + swim(index) + sink(index) + +/datum/tg_heap/proc/List() + . = L.Copy() + +GLOBAL_LIST_INIT(legacy_tg_space_type_cache, typecacheof(/turf/space)) + +/** + * 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/tg_jps_pathfind][pathfind datum] since it uses variables from said datum. + * If you really want to optimize things, optimize this, cuz this gets called a lot. + */ +#define CAN_STEP(cur_turf, next) (next && !next.density && !(simulated_only && GLOB.legacy_tg_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/tg_jps_node + /// The turf associated with this node + var/turf/tile + /// The node we just came from + var/datum/tg_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/tg_jps_node/New(turf/our_tile, datum/tg_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/tg_jps_node/Destroy(force, ...) + previous_node = null + return ..() + +/datum/tg_jps_node/proc/update_parent(datum/tg_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/TGHeapPathWeightCompare(datum/tg_jps_node/a, datum/tg_jps_node/b) + return b.f_value - a.f_value + +/** + * The datum used to handle the JPS pathfinding, completely self-contained. + */ +/datum/tg_jps_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/tg_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/weapon/card/id/id + /// How far away we have to get to the end target before we can call it quits + var/mintargetdist = 0 + /// I don't know what this does vs , but they limit how far we can search before giving up on a path + var/max_distance = 30 + /// Space is big and empty, if this is TRUE then we ignore pathing through unsimulated tiles + var/simulated_only + /// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through + var/turf/avoid + +/datum/tg_jps_pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid) + src.caller = caller + end = get_turf(goal) + open = new /datum/tg_heap(GLOBAL_PROC_REF(TGHeapPathWeightCompare)) + sources = new() + src.id = id + src.max_distance = max_distance + src.mintargetdist = mintargetdist + src.simulated_only = simulated_only + src.avoid = avoid + +/** + * search() is the proc you call to kick off and handle the actual pathfinding, and kills the pathfind datum instance when it's done. + * + * If a valid path was found, it's returned as a list. If invalid or cross-z-level params are entered, or if there's no valid path found, we + * return null, which [/proc/get_path_to] translates to an empty list (notable for simple bots, who need empty lists) + */ +/datum/tg_jps_pathfind/proc/search() + start = get_turf(caller) + if(!start || !end) + stack_trace("Invalid A* start or destination") + return + if(start.z != end.z || start == end ) //no pathfinding between z levels + return + if(max_distance && (max_distance < get_dist(start, end))) //if start turf is farther than max_distance from end turf, no need to do anything + return + + //initialization + var/datum/tg_jps_node/current_processed_node = new (start, -1, 0, end) + open.insert(current_processed_node) + sources[start] = start // i'm sure this is fine + + //then run the main loop + while(!open.is_empty() && !path) + if(!caller) + return + current_processed_node = open.pop() //get the lower f_value turf in the open list + if(max_distance && (current_processed_node.number_tiles > max_distance))//if too many steps, don't process that path + continue + + var/turf/current_turf = current_processed_node.tile + for(var/scan_direction in list(EAST, WEST, NORTH, SOUTH)) + lateral_scan_spec(current_turf, scan_direction, current_processed_node) + + for(var/scan_direction in list(NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST)) + diag_scan_spec(current_turf, scan_direction, current_processed_node) + + CHECK_TICK + + //we're done! reverse the path to get it from start to finish + if(path) + for(var/i = 1 to round(0.5 * length(path))) + path.Swap(i, length(path) - i + 1) + + sources = null + qdel(open) + return path + +/** + * Called when we've hit the goal with the node that represents the last tile, + * then sets the path var to that path so it can be returned by [datum/tg_jps_pathfind/proc/search] + */ +/datum/tg_jps_pathfind/proc/unwind_path(datum/tg_jps_node/unwind_node) + path = new() + var/turf/iter_turf = unwind_node.tile + path.Add(iter_turf) + + while(unwind_node.previous_node) + var/dir_goal = get_dir(iter_turf, unwind_node.previous_node.tile) + for(var/i = 1 to unwind_node.jumps) + iter_turf = get_step(iter_turf,dir_goal) + path.Add(iter_turf) + unwind_node = unwind_node.previous_node + +/** + * For performing lateral scans from a given starting turf. + * + * These scans are called from both the main search loop, as well as subscans for diagonal scans, and they treat finding interesting turfs slightly differently. + * If we're doing a normal lateral scan, we already have a parent node supplied, so we just create the new node and immediately insert it into the heap, ezpz. + * If we're part of a subscan, we still need for the diagonal scan to generate a parent node, so we return a node datum with just the turf and let the diag scan + * proc handle transferring the values and inserting them into the heap. + * + * Arguments: + * * original_turf: What turf did we start this scan at? + * * heading: What direction are we going in? Obviously, should be cardinal + * * parent_node: Only given for normal lateral scans, if we don't have one, we're a diagonal subscan. +*/ +/datum/tg_jps_pathfind/proc/lateral_scan_spec(turf/original_turf, heading, datum/tg_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/tg_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/tg_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/tg_jps_pathfind/proc/diag_scan_spec(turf/original_turf, heading, datum/tg_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/tg_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/tg_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/tg_jps_node/newnode = new(current_turf, parent_node, steps_taken) + open.insert(newnode) + if(possible_child_node) + possible_child_node.update_parent(newnode) + open.insert(possible_child_node) + if(possible_child_node.tile == end || (mintargetdist && (get_dist(possible_child_node.tile, end) <= mintargetdist))) + unwind_path(possible_child_node) + return + +/** + * For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags + * + * Arguments: + * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach + * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf + * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? +*/ +/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID) + var/static/datum/pathfinding/whatever = new + return !global.default_pathfinding_adjacency(src, destination_turf, GLOB.generic_pathfinding_actor, whatever) + +#undef CAN_STEP +#undef STEP_NOT_HERE_BUT_THERE diff --git a/code/_helpers/pathfinding_ch/astar.dm b/code/_helpers/pathfinding_ch/astar.dm new file mode 100644 index 0000000000..901fb35ae7 --- /dev/null +++ b/code/_helpers/pathfinding_ch/astar.dm @@ -0,0 +1,258 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2023 Citadel Station developers. *// + +/// visualization; obviously slow as hell +// #define ASTAR_DEBUGGING + +#ifdef ASTAR_DEBUGGING + +#warn ASTAR pathfinding visualizations enabled +/// visualization delay +GLOBAL_VAR_INIT(astar_visualization_delay, 0.05 SECONDS) +/// how long to persist the visuals +GLOBAL_VAR_INIT(astar_visualization_persist, 3 SECONDS) +#define ASTAR_VISUAL_COLOR_CLOSED "#ff4444" +#define ASTAR_VISUAL_COLOR_OUT_OF_BOUNDS "#555555" +#define ASTAR_VISUAL_COLOR_OPEN "#4444ff" +#define ASTAR_VISUAL_COLOR_CURRENT "#ffff00" +#define ASTAR_VISUAL_COLOR_FOUND "#00ff00" + +#define ASTAR_TRACE_COLOR_REDIRECTED "#7777ff" + +/proc/astar_wipe_colors_after(list/turf/turfs, time) + set waitfor = FALSE + astar_wipe_colors_after_sleeping(turfs, time) + +/proc/astar_wipe_colors_after_sleeping(list/turf/turfs, time) + sleep(time) + for(var/turf/T in turfs) + T.color = null + T.maptext = null + T.overlays.len = 0 + +/proc/get_astar_scan_overlay(dir, forwards, color) + var/image/I = new + I.icon = icon('icons/screen/debug/pathfinding.dmi', "jps_scan", dir) + I.appearance_flags = KEEP_APART | RESET_ALPHA | RESET_COLOR | RESET_TRANSFORM + I.plane = OBJ_PLANE + I.color = color + if(dir & NORTH) + I.pixel_y = forwards? 16 : -16 + else if(dir & SOUTH) + I.pixel_y = forwards? -16 : 16 + if(dir & EAST) + I.pixel_x = forwards? 16 : -16 + else if(dir & WEST) + I.pixel_x = forwards? -16 : 16 + return I + +#endif + +/// this is almost a megabyte +#define ASTAR_SANE_NODE_LIMIT 15000 + +/datum/astar_node + /// turf + var/turf/pos + /// previous + var/datum/astar_node/prev + + /// our score + var/score + /// our inherent cost + var/weight + /// node depth to get to here + var/depth + /// cost to get here from prev - built off of prev + var/cost + +/datum/astar_node/New(turf/pos, datum/astar_node/prev, score, weight, depth, cost) + src.pos = pos + src.prev = prev + src.score = score + src.weight = weight + src.depth = depth + src.cost = cost + +/proc/cmp_astar_node(datum/astar_node/A, datum/astar_node/B) + return A.score - B.score + +#define ASTAR_HEURISTIC_CALL(TURF) (isnull(context)? call(heuristic_call)(TURF, goal) : call(context, heuristic_call)(TURF, goal)) +#define ASTAR_ADJACENCY_CALL(A, B) (isnull(context)? call(adjacency_call)(A, B, actor, src) : call(context, adjacency_call)(A, B, actor, src)) +#define ASTAR_HEURISTIC_WEIGHT 1.2 +#ifdef ASTAR_DEBUGGING + #define ASTAR_HELL_DEFINE(TURF, DIR) \ + if(!isnull(TURF)) { \ + if(ASTAR_ADJACENCY_CALL(current, considering)) { \ + considering_cost = top.cost + considering.path_weight; \ + considering_score = ASTAR_HEURISTIC_CALL(considering) * ASTAR_HEURISTIC_WEIGHT + considering_cost; \ + considering_node = node_by_turf[considering]; \ + if(isnull(considering_node)) { \ + considering_node = new /datum/astar_node(considering, top, considering_score, considering_cost, top.depth + 1, considering_cost); \ + open.enqueue(considering_node); \ + node_by_turf[considering] = considering_node; \ + turfs_got_colored[considering] = TRUE; \ + considering.color = ASTAR_VISUAL_COLOR_OPEN; \ + considering.maptext = MAPTEXT("[top.depth + 1], [considering_cost], [considering_score]"); \ + considering.overlays += get_astar_scan_overlay(DIR); \ + } \ + else { \ + if(considering_node.cost > considering_cost) { \ + considering_node.cost = considering_cost; \ + considering_node.depth = top.depth + 1; \ + considering_node.pos.maptext = MAPTEXT("X [top.depth + 1], [considering_cost], [considering_score]"); \ + considering.overlays += get_astar_scan_overlay(DIR, TRUE, ASTAR_TRACE_COLOR_REDIRECTED); \ + considering_node.prev = top; \ + } \ + } \ + } \ + } +#else + #define ASTAR_HELL_DEFINE(TURF, DIR) \ + if(!isnull(TURF)) { \ + if(ASTAR_ADJACENCY_CALL(current, considering)) { \ + considering_cost = top.cost + considering.path_weight; \ + considering_score = ASTAR_HEURISTIC_CALL(considering) * ASTAR_HEURISTIC_WEIGHT + considering_cost; \ + considering_node = node_by_turf[considering]; \ + if(isnull(considering_node)) { \ + considering_node = new /datum/astar_node(considering, top, considering_score, considering_cost, top.depth + 1, considering_cost); \ + open.enqueue(considering_node); \ + node_by_turf[considering] = considering_node; \ + } \ + else { \ + if(considering_node.cost > considering_cost) { \ + considering_node.cost = considering_cost; \ + considering_node.depth = top.depth + 1; \ + considering_node.prev = top; \ + } \ + } \ + } \ + } +#endif + +/** + * AStar + * * Non uniform grids + * * Slower than JPS + * * Inherently cardinals-only + * * Node limit is manhattan, so 128 is a lot less than BYOND's get_dist(128). + */ +/datum/pathfinding/astar + +/datum/pathfinding/astar/search() + ASSERT(isturf(src.start) && isturf(src.goal) && src.start.z == src.goal.z) + if(src.start == src.goal) + return list() + // too far away + if(get_manhattan_dist(src.start, src.goal) > max_path_length) + return null + #ifdef ASTAR_DEBUGGING + var/list/turf/turfs_got_colored = list() + #endif + // cache for sanic speed + var/max_depth = src.max_path_length + var/turf/goal = src.goal + var/target_distance = src.target_distance + var/atom/movable/actor = src.actor + var/adjacency_call = src.adjacency_call + var/heuristic_call = src.heuristic_call + var/datum/context = src.context + // add operating vars + var/turf/current + var/turf/considering + var/considering_score + var/considering_cost + var/datum/astar_node/considering_node + var/list/node_by_turf = list() + // make queue + var/datum/priority_queue/open = new /datum/priority_queue(/proc/cmp_astar_node) + // add initial node + var/datum/astar_node/initial_node = new(start, null, ASTAR_HEURISTIC_CALL(start), 0, 0, 0) + open.enqueue(initial_node) + node_by_turf[start] = initial_node + + #ifdef ASTAR_DEBUGGING + turfs_got_colored[start] = TRUE + start.color = ASTAR_VISUAL_COLOR_OPEN + #endif + + while(length(open.array)) + // get best node + var/datum/astar_node/top = open.dequeue() + current = top.pos + #ifdef ASTAR_DEBUGGING + top.pos.color = ASTAR_VISUAL_COLOR_CURRENT + turfs_got_colored[top.pos] = TRUE + sleep(GLOB.astar_visualization_delay) + #else + CHECK_TICK + #endif + + // get distance and check completion + if(get_dist(current, goal) <= target_distance && (target_distance != 1 || !require_adjacency_when_going_adjacent || current.TurfAdjacency(goal))) + // found; build path end to start of nodes + var/list/path_built = list() + while(top) + path_built += top.pos + #ifdef ASTAR_DEBUGGING + top.pos.color = ASTAR_VISUAL_COLOR_FOUND + turfs_got_colored[top] = TRUE + #endif + top = top.prev + // reverse + var/head = 1 + var/tail = length(path_built) + while(head < tail) + path_built.Swap(head++, tail--) + #ifdef ASTAR_DEBUGGING + astar_wipe_colors_after(turfs_got_colored, GLOB.astar_visualization_persist) + #endif + return path_built + + // too deep, abort + if(top.depth + get_dist(current, goal) > max_depth) + #ifdef ASTAR_DEBUGGING + top.pos.color = ASTAR_VISUAL_COLOR_OUT_OF_BOUNDS + turfs_got_colored[top.pos] = TRUE + #endif + continue + + considering = get_step(current, NORTH) + ASTAR_HELL_DEFINE(considering, NORTH) + considering = get_step(current, SOUTH) + ASTAR_HELL_DEFINE(considering, SOUTH) + considering = get_step(current, EAST) + ASTAR_HELL_DEFINE(considering, EAST) + considering = get_step(current, WEST) + ASTAR_HELL_DEFINE(considering, WEST) + + #ifdef ASTAR_DEBUGGING + top.pos.color = ASTAR_VISUAL_COLOR_CLOSED + turfs_got_colored[top.pos] = TRUE + #endif + + if(length(open.array) > ASTAR_SANE_NODE_LIMIT) + #ifdef ASTAR_DEBUGGING + astar_wipe_colors_after(turfs_got_colored, GLOB.astar_visualization_persist) + #endif + CRASH("A* hit node limit - something went horribly wrong! args: [json_encode(args)]; vars: [json_encode(vars)]") + + #ifdef ASTAR_DEBUGGING + astar_wipe_colors_after(turfs_got_colored, GLOB.astar_visualization_persist) + #endif + +#undef ASTAR_HELL_DEFINE +#undef ASTAR_HEURISTIC_CALL +#undef ASTAR_ADJACENCY_CALL + +#undef ASTAR_SANE_NODE_LIMIT +#undef ASTAR_HEURISTIC_WEIGHT + +#ifdef ASTAR_DEBUGGING + #undef ASTAR_DEBUGGING + + #undef ASTAR_VISUAL_COLOR_CLOSED + #undef ASTAR_VISUAL_COLOR_OPEN + #undef ASTAR_VISUAL_COLOR_CURRENT + #undef ASTAR_VISUAL_COLOR_FOUND +#endif diff --git a/code/_helpers/pathfinding_ch/common.dm b/code/_helpers/pathfinding_ch/common.dm new file mode 100644 index 0000000000..5e3c1678ad --- /dev/null +++ b/code/_helpers/pathfinding_ch/common.dm @@ -0,0 +1,199 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2023 Citadel Station developers. *// + +/** + * Default object used during pathfinder checks + */ +GLOBAL_DATUM_INIT(generic_pathfinding_actor, /atom/movable/pathfinding_predicate, new) + +/atom/movable/pathfinding_predicate + invisibility = INVISIBILITY_ABSTRACT + //pass_flags = ATOM_PASS_CLICK + //pass_flags_self = NONE + +/** + * datum used for pathfinding + * + * pathfinding is a specific version of otherwise generic graph/grid searches + * we only path via cardinals due to ss13's movement treating diagonals as two cardinal moves + * pixel movement is explicitly non-supported at this time + * + * for overmaps / similar pixel-move-ish tasks, please write a new pathfinding system if you want + * accurate results. + */ +/datum/pathfinding + //* basics + /// thing trying to get a path + var/atom/movable/actor + /// start turf + var/turf/start + /// goal turf + var/turf/goal + + //* options + /// how far away to the end we want to get; 0 = get ontop of the tile, 1 = get adjacent to the tile + /// keep in mind that pathing with 0 to a dense object is usually going to fail! + /// this is in byond distance, *not* pathfinding distance + /// this means that 1 tile away diagonally = 1, 2 diagonally away = 2, etc. + var/target_distance + /// if target distance is one, we require adjacency + var/require_adjacency_when_going_adjacent = TRUE + /// how far away total we can search + /// this is not distance from source we want to go, this is how far away we can *search* + /// (the former might be the case for some algorithms, though). + /// this should not be used to limit pathfinding max distance / path distance + /// this just tells the algorithm when it should give up + /// different algorithms respond differently to this. + var/max_path_length + /// context to call adjacency/distance call on + /// null = global proc + var/datum/context + /// checks if we can go to a turf + /// defaults to default density / canpass / etc checks + /// called with (turf/A, turf/B, atom/movable/actor, datum/pathfinding/pathfinding) + /// it should return the distance to that turf + var/adjacency_call = /proc/default_pathfinding_adjacency + /// checks distance from turf to target / end turf + /// defaults to just get dist + /// called with (turf/current, turf/goal) + var/heuristic_call = /proc/default_pathfinding_heuristic + /// danger flags to ignore + var/turf_path_danger_ignore = NONE + + //* ss13-specific things + /// access list ; used to get through doors and other objects if set + var/list/ss13_with_access + +/datum/pathfinding/New(atom/movable/actor, turf/start, turf/goal, target_distance, max_path_length) + src.actor = actor + src.start = start + src.goal = goal + src.target_distance = target_distance + src.max_path_length = max_path_length + +/** + * returns raw list of nodes returned by algorithm + */ +/datum/pathfinding/proc/search() + RETURN_TYPE(/list) + CRASH("Not implemented on base type.") + +/datum/pathfinding/proc/debug_log_string() + return json_encode(vars) + +/datum/pathfinding_context + +/datum/pathfinding_context/proc/adjacency(turf/A, turf/B, atom/movable/actor, datum/pathfinding/search) + return default_pathfinding_adjacency(A, B, actor, search) + +/datum/pathfinding_context/proc/heuristic(turf/current, turf/goal) + return default_pathfinding_heuristic(current, goal) + +/datum/pathfinding_context/ignoring + /// ignore typecache + var/list/turf_ignore_typecache + /// ignore instance cache + var/list/turf_ignore_cache + +/datum/pathfinding_context/ignoring/adjacency(turf/A, turf/B, atom/movable/actor, datum/pathfinding/search) + if(!isnull(turf_ignore_typecache) && turf_ignore_typecache[B.type]) + return FALSE + if(!isnull(turf_ignore_cache) && turf_ignore_cache[B.type]) + return FALSE + return default_pathfinding_adjacency(A, B, actor, search) + +//* ENSURE BELOW PROCS MATCH EACH OTHER IN THEIR PAIRS *// +//* This allows for fast default implementations while *// +//* allowing for advanced checks when a pathfinding *// +//* context is supplied. *// + +/proc/default_pathfinding_adjacency(turf/A, turf/B, atom/movable/actor, datum/pathfinding/search) + // we really need to optimize this furthur + // this currently catches abstract stuff like lighting objects + // not great for performance. + + if(B.density) + return FALSE + if((B.turf_path_danger & search.turf_path_danger_ignore) != B.turf_path_danger) + return FALSE + + var/dir = get_dir(A, B) + + if(dir & (dir - 1)) + var/td1 = dir & (NORTH|SOUTH) + var/td2 = dir & (EAST|WEST) + var/turf/scan = get_step(A, td1) + if(!isnull(scan) && default_pathfinding_adjacency(A, scan, actor, search) && default_pathfinding_adjacency(scan, B, actor, search)) + return TRUE + scan = get_step(A, td2) + if(!isnull(scan) && default_pathfinding_adjacency(A, scan, actor, search) && default_pathfinding_adjacency(scan, B, actor, search)) + return TRUE + return FALSE + + var/rdir = turn(dir, 180) + + for(var/atom/movable/AM as anything in A) + if(!AM.can_pathfinding_exit(actor, dir, search)) + return FALSE + for(var/atom/movable/AM as anything in B) + if(!AM.can_pathfinding_enter(actor, rdir, search)) + return FALSE + return TRUE + +/proc/default_pathfinding_heuristic(turf/current, turf/goal) + return max(abs(current.x - goal.x), abs(current.y - goal.y)) + +/proc/jps_pathfinding_adjacency(turf/A, turf/B, atom/movable/actor, datum/pathfinding/search) + // we really need to optimize this furthur + // this currently catches abstract stuff like lighting objects + // not great for performance. + + if(B.density) + return FALSE + if((B.turf_path_danger & search.turf_path_danger_ignore) != B.turf_path_danger) + return FALSE + + var/dir = get_dir(A, B) + + if(dir & (dir - 1)) + var/td1 = dir & (NORTH|SOUTH) + var/td2 = dir & (EAST|WEST) + var/turf/scan = get_step(A, td1) + if(!isnull(scan) && jps_pathfinding_adjacency(A, scan, actor, search) && jps_pathfinding_adjacency(scan, B, actor, search)) + return TRUE + scan = get_step(A, td2) + if(!isnull(scan) && jps_pathfinding_adjacency(A, scan, actor, search) && jps_pathfinding_adjacency(scan, B, actor, search)) + return TRUE + return FALSE + + for(var/atom/movable/AM as anything in B) + if(!AM.can_pathfinding_pass(actor, search)) + return FALSE + return TRUE + +/** + * This is a pretty hot proc used during pathfinding to see if something + * should be able to pass through this movable in a certain direction. + * + * dir is where they're coming from + */ +/atom/movable/proc/can_pathfinding_enter(atom/movable/actor, dir, datum/pathfinding/search) + return !density /*|| (pass_flags_self & actor.pass_flags)*/ + +/** + * This is a pretty hot proc used during pathfinding to see if something + * should be able to pass out of this movable in a certain direction. + * + * dir is where they're going to + */ +/atom/movable/proc/can_pathfinding_exit(atom/movable/actor, dir, datum/pathfinding/search) + return !(flags & ON_BORDER) || !density /*|| (pass_flags_self & actor.pass_flags)*/ + +/** + * basically, non directional pathfinding enter/exit checks + * + * this is used for JPS because it does not at all play nicely with situations where one direction + * is blocked and another isn't. + */ +/atom/movable/proc/can_pathfinding_pass(atom/movable/actor, datum/pathfinding/search) + return !density /*|| (pass_flags_self & actor.pass_flags)*/ diff --git a/code/_helpers/pathfinding_ch/jps.dm b/code/_helpers/pathfinding_ch/jps.dm new file mode 100644 index 0000000000..9e1cdb5147 --- /dev/null +++ b/code/_helpers/pathfinding_ch/jps.dm @@ -0,0 +1,603 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2023 Citadel Station developers. *// + +/// visualization; obviously slow as hell +/// JPS visualization is currently not nearly as perfect as A*'s. +/// notably is sometimes marks stuff closed that isn't because of the weird backtracking stuff I put in. +// #define JPS_DEBUGGING + +#ifdef JPS_DEBUGGING + +#warn JPS pathfinding visualizations enabled +/// visualization delay +GLOBAL_VAR_INIT(jps_visualization_delay, 0.05 SECONDS) +/// how long to persist the visuals +GLOBAL_VAR_INIT(jps_visualization_persist, 3 SECONDS) +/// visualize nodes or finished path +GLOBAL_VAR_INIT(jps_visualization_resolve, TRUE) + +/proc/get_jps_scan_overlay(dir, forwards) + var/image/I = new + I.icon = icon('icons/screen/debug/pathfinding.dmi', "jps_scan", dir) + I.appearance_flags = KEEP_APART | RESET_ALPHA | RESET_COLOR | RESET_TRANSFORM + I.plane = OBJ_PLANE + if(dir & NORTH) + I.pixel_y = forwards? 16 : -16 + else if(dir & SOUTH) + I.pixel_y = forwards? -16 : 16 + if(dir & EAST) + I.pixel_x = forwards? 16 : -16 + else if(dir & WEST) + I.pixel_x = forwards? -16 : 16 + return I + +#define JPS_VISUAL_DELAY 10 SECONDS +#define JPS_VISUAL_COLOR_CLOSED "#ff3333" +#define JPS_VISUAL_COLOR_OUT_OF_BOUNDS "#555555" +#define JPS_VISUAL_COLOR_OPEN "#7777ff" +#define JPS_VISUAL_COLOR_FOUND "#33ff33" +#define JPS_VISUAL_COLOR_CURRENT "#ffff00" +#define JPS_VISUAL_COLOR_INTERMEDIATE "#ff00ff" + +/proc/jps_wipe_colors_after(list/turf/turfs, time) + set waitfor = FALSE + jps_wipe_colors_after_sleeping(turfs, time) + +/proc/jps_wipe_colors_after_sleeping(list/turf/turfs, time) + sleep(time) + for(var/turf/T in turfs) + T.color = null + T.maptext = null + // lol just cut all this is a debug proc anyways + T.overlays.len = 0 + +#endif + +/datum/jps_node + /// our turf + var/turf/pos + /// previous node + var/datum/jps_node/prev + + /// our heuristic to goal + var/heuristic + /// our node depth - for jps, this is just the amount turfs passed to go from start to here. + var/depth + /// our jump direction + var/dir + /// our score - built from heuristic and cost + var/score + +/datum/jps_node/New(turf/pos, datum/jps_node/prev, heuristic, depth, dir) + #ifdef JPS_DEBUGGING + ASSERT(isturf(pos)) + #endif + src.pos = pos + src.prev = prev + src.heuristic = heuristic + src.depth = depth + src.dir = dir + + src.score = depth + heuristic + +/proc/cmp_jps_node(datum/jps_node/A, datum/jps_node/B) + return A.score - B.score + +/** + * JPS (jump point search) + * + * * flat routes + * * inherently emits diagonals + * * emits a bunch nodes to walk to instead of a clear path + * * all tiles are treated as 1 distance - including diagonals. + * * max_dist is *really* weird. It uses JPs path lengths, so, you probably need it a good bit higher than your target distance. + * * jps cannot handle turfs that allow in one dir only at all. for precision navigation in those cases, you'll need A*. + */ +/datum/pathfinding/jps + adjacency_call = /proc/jps_pathfinding_adjacency + +/datum/pathfinding/jps/search() + //* define ops + #define JPS_HEURISTIC_CALL(TURF) (isnull(context)? call(heuristic_call)(TURF, goal) : call(context, heuristic_call)(TURF, goal)) + #define JPS_ADJACENCY_CALL(A, B) (isnull(context)? call(adjacency_call)(A, B, actor, src) : call(context, adjacency_call)(A, B, actor, src)) + //* preliminary checks + ASSERT(isturf(src.start) && isturf(src.goal) && src.start.z == src.goal.z) + if(src.start == src.goal) + return list() + // too far away + if(get_chebyshev_dist(src.start, src.goal) > max_path_length) + return null + #ifdef JPS_DEBUGGING + //* set up debugging vars + // turf associated to how many open nodes are on it; once 0, it becomes closed. if setting to something other than closed, set to -1. + var/list/turf/turfs_got_colored = list() + #endif + //* cache for sanic speed + var/max_depth = src.max_path_length + var/turf/goal = src.goal + var/target_distance = src.target_distance + var/atom/movable/actor = src.actor + var/adjacency_call = src.adjacency_call + var/heuristic_call = src.heuristic_call + var/datum/context = src.context + if(SSpathfinder.pathfinding_cycle >= SHORT_REAL_LIMIT) + SSpathfinder.pathfinding_cycle = 0 + // our cycle. used to determine if a turf was pathed on by us. in theory, this isn't entirely collision resistant, + // but i don't really care :> + var/cycle = ++SSpathfinder.pathfinding_cycle + //* variables - run + // open priority queue + var/datum/priority_queue/open = new /datum/priority_queue(/proc/cmp_jps_node) + // used when creating a node if we need to reference it + var/datum/jps_node/node_creating + // the top node that we fetch at start of cycle + var/datum/jps_node/node_top + // turf of top node + var/turf/node_top_pos + // dir of top node + var/node_top_dir + //* variables - diagonal scan + // turf we're on right now + var/turf/dscan_current + // turf we're about to hop to + var/turf/dscan_next + // side dir 1 for cardinal scan + var/dscan_dir1 + // side dir 2 for cardinal scan + var/dscan_dir2 + // did a forced neighbor get detected in either cardinal scan + var/dscan_pass + // current number of steps in the scan + var/dscan_steps + // where we started at, steps wise, so we can properly trim by depth + var/dscan_initial + // diagonal node - this is held here because if we get a potential spot on cardinal we need to immediately + // make the diagonal node + var/datum/jps_node/dscan_node + //* variables - cardinal scan + // turf we're on right now + var/turf/cscan_current + // turf we're about to hop to + var/turf/cscan_next + // turf we were on last so we can make a node there when we have a forced neighbor + var/turf/cscan_last + // turf we're scanning to side + var/turf/cscan_turf1 + // turf we're scanning to side + var/turf/cscan_turf2 + // perpendicular dir 1 + var/cscan_dir1 + // perpendicular dir 2 + var/cscan_dir2 + // perpendicular dir 1 didn't fail + var/cscan_dir1_pass + // perpendicular dir 2 didn't fail + var/cscan_dir2_pass + // did a forced neighbor get detected? + var/cscan_pass + // current number of steps in the scan + var/cscan_steps + // where we started at, steps wise, so we can properly trim by depth + var/cscan_initial + //* start + // get start heuristic + var/start_heuristic = JPS_HEURISTIC_CALL(start) + // for best case, we estimate the 'right' dir to go at first + var/start_dir = jps_estimate_dir(start, goal) + // dir being checked + var/start_check_dir + // turf being checked + var/turf/start_check + #ifdef JPS_DEBUGGING + turfs_got_colored[start] = 8 + start.color = JPS_VISUAL_COLOR_OPEN + #define JPS_START_DIR(DIR) \ + start_check_dir = DIR ; \ + start_check = get_step(start, start_check_dir); \ + if(!isnull(start_check) && JPS_ADJACENCY_CALL(start, start_check)) { \ + start.overlays += get_jps_scan_overlay(DIR, TRUE); \ + node_creating = new /datum/jps_node(start, null, start_heuristic, 0, start_check_dir) ; \ + open.enqueue(node_creating); \ + } + #else + #define JPS_START_DIR(DIR) \ + start_check_dir = DIR ; \ + start_check = get_step(start, start_check_dir); \ + if(!isnull(start_check) && JPS_ADJACENCY_CALL(start, start_check)) { \ + node_creating = new /datum/jps_node(start, null, start_heuristic, 0, start_check_dir) ; \ + open.enqueue(node_creating); \ + } + #endif + JPS_START_DIR(start_dir) + JPS_START_DIR(turn(start_dir, 45)) + JPS_START_DIR(turn(start_dir, -45)) + JPS_START_DIR(turn(start_dir, 90)) + JPS_START_DIR(turn(start_dir, -90)) + JPS_START_DIR(turn(start_dir, 135)) + JPS_START_DIR(turn(start_dir, -135)) + JPS_START_DIR(turn(start_dir, 180)) + //* define completion check + #define JPS_COMPLETION_CHECK(TURF) (get_dist(TURF, goal) <= target_distance && (target_distance != 1 || !require_adjacency_when_going_adjacent || TURF.TurfAdjacency(goal))) + //* define cardinal scan helpers + #define JPS_CARDINAL_DURING_DIAGONAL (node_top_dir & (node_top_dir - 1)) + //* define cardinal scan + // things to note: + // - unlike diagonal / cardinal scan branches, this does not + // skip the first tile. this is because when it's used in a diagonal + // scan, it outright should not be skipping the first tile. + // order of ops: + // - check out of bounds/depth + // - check completion + // - place debug overlays + // - check sides and mark pass/fail; if it was already failing, mark the cpass fail and make diagonal nodes + // - if cpass failed, we also want to make our cardinal nodes + // - if any node is made, ensure that we are either not in diagonal mode, or if we are, the diagonal node was created + // - check and go to next turf + #ifdef JPS_DEBUGGING + #define JPS_CARDINAL_SCAN(TURF, DIR) \ + cscan_dir1 = turn(DIR, 90); \ + cscan_dir2 = turn(DIR, -90); \ + cscan_steps = 0; \ + cscan_pass = TRUE; \ + cscan_dir1_pass = TRUE; \ + cscan_dir2_pass = TRUE; \ + cscan_current = TURF; \ + cscan_last = null; \ + cscan_initial = JPS_CARDINAL_DURING_DIAGONAL? node_top.depth + dscan_steps : node_top.depth; \ + do { \ + if(cscan_steps + cscan_initial + get_dist(cscan_current, goal) > max_depth) { \ + cscan_current.color = JPS_VISUAL_COLOR_OUT_OF_BOUNDS; \ + break; \ + } \ + if(JPS_COMPLETION_CHECK(cscan_current)) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_current, dscan_node, JPS_HEURISTIC_CALL(cscan_current), dscan_node.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_current, node_top, JPS_HEURISTIC_CALL(cscan_current), node_top.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + open.enqueue(node_creating); \ + return jps_unwind_path(node_creating, turfs_got_colored); \ + } \ + turfs_got_colored[cscan_current] = turfs_got_colored[cscan_current] || 0; \ + cscan_current.overlays += get_jps_scan_overlay(DIR, JPS_CARDINAL_DURING_DIAGONAL); \ + cscan_turf1 = get_step(cscan_current, cscan_dir1); \ + cscan_turf2 = get_step(cscan_current, cscan_dir2); \ + if(!isnull(cscan_turf1)) { \ + if(!JPS_ADJACENCY_CALL(cscan_current, cscan_turf1)) { \ + cscan_dir1_pass = FALSE ; \ + } \ + else if(cscan_dir1_pass == FALSE) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_last, dscan_node, JPS_HEURISTIC_CALL(cscan_last), dscan_node.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_last, node_top, JPS_HEURISTIC_CALL(cscan_last), node_top.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + turfs_got_colored[cscan_last] = turfs_got_colored[cscan_last] + 1; \ + cscan_last.color = JPS_VISUAL_COLOR_OPEN; \ + open.enqueue(node_creating); \ + cscan_pass = FALSE; \ + } \ + } \ + if(!isnull(cscan_turf2)) { \ + if(!JPS_ADJACENCY_CALL(cscan_current, cscan_turf2)) { \ + cscan_dir2_pass = FALSE ; \ + } \ + else if(cscan_dir2_pass == FALSE) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_last, dscan_node, JPS_HEURISTIC_CALL(cscan_last), dscan_node.depth + cscan_steps - 1, DIR | cscan_dir2); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_last, node_top, JPS_HEURISTIC_CALL(cscan_last), node_top.depth + cscan_steps - 1, DIR | cscan_dir2); \ + } \ + turfs_got_colored[cscan_last] = turfs_got_colored[cscan_last] + 1; \ + cscan_last.color = JPS_VISUAL_COLOR_OPEN; \ + open.enqueue(node_creating); \ + cscan_pass = FALSE; \ + } \ + } \ + if(!cscan_pass) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_last, dscan_node, JPS_HEURISTIC_CALL(cscan_last), dscan_node.depth + cscan_steps - 1, DIR); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_last, node_top, JPS_HEURISTIC_CALL(cscan_last), node_top.depth + cscan_steps - 1, DIR); \ + } \ + turfs_got_colored[cscan_last] = turfs_got_colored[cscan_last] + 1; \ + cscan_last.color = JPS_VISUAL_COLOR_OPEN; \ + open.enqueue(node_creating); \ + break; \ + } \ + cscan_next = get_step(cscan_current, DIR); \ + if(isnull(cscan_next) || (cscan_next.pathfinding_cycle == cycle) || !JPS_ADJACENCY_CALL(cscan_current, cscan_next)) { \ + break; \ + } \ + cscan_current.pathfinding_cycle = cycle; \ + cscan_last = cscan_current; \ + cscan_current = cscan_next; \ + cscan_steps++; \ + } \ + while(TRUE); + #else + #define JPS_CARDINAL_SCAN(TURF, DIR) \ + cscan_dir1 = turn(DIR, 90); \ + cscan_dir2 = turn(DIR, -90); \ + cscan_steps = 0; \ + cscan_pass = TRUE; \ + cscan_dir1_pass = TRUE; \ + cscan_dir2_pass = TRUE; \ + cscan_current = TURF; \ + cscan_last = null; \ + cscan_initial = JPS_CARDINAL_DURING_DIAGONAL? node_top.depth + dscan_steps : node_top.depth; \ + do { \ + if(cscan_steps + cscan_initial + get_dist(cscan_current, goal) > max_depth) { \ + break; \ + } \ + if(JPS_COMPLETION_CHECK(cscan_current)) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_current, dscan_node, JPS_HEURISTIC_CALL(cscan_current), dscan_node.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_current, node_top, JPS_HEURISTIC_CALL(cscan_current), node_top.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + open.enqueue(node_creating); \ + return jps_unwind_path(node_creating); \ + } \ + cscan_turf1 = get_step(cscan_current, cscan_dir1); \ + cscan_turf2 = get_step(cscan_current, cscan_dir2); \ + if(!isnull(cscan_turf1)) { \ + if(!JPS_ADJACENCY_CALL(cscan_current, cscan_turf1)) { \ + cscan_dir1_pass = FALSE ; \ + } \ + else if(cscan_dir1_pass == FALSE) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_last, dscan_node, JPS_HEURISTIC_CALL(cscan_last), dscan_node.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_last, node_top, JPS_HEURISTIC_CALL(cscan_last), node_top.depth + cscan_steps - 1, DIR | cscan_dir1); \ + } \ + open.enqueue(node_creating); \ + cscan_pass = FALSE; \ + } \ + } \ + if(!isnull(cscan_turf2)) { \ + if(!JPS_ADJACENCY_CALL(cscan_current, cscan_turf2)) { \ + cscan_dir2_pass = FALSE ; \ + } \ + else if(cscan_dir2_pass == FALSE) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_last, dscan_node, JPS_HEURISTIC_CALL(cscan_last), dscan_node.depth + cscan_steps - 1, DIR | cscan_dir2); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_last, node_top, JPS_HEURISTIC_CALL(cscan_last), node_top.depth + cscan_steps - 1, DIR | cscan_dir2); \ + } \ + open.enqueue(node_creating); \ + cscan_pass = FALSE; \ + } \ + } \ + if(!cscan_pass) { \ + if(JPS_CARDINAL_DURING_DIAGONAL && isnull(dscan_node)) { \ + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir); \ + node_creating = new /datum/jps_node(cscan_last, dscan_node, JPS_HEURISTIC_CALL(cscan_last), dscan_node.depth + cscan_steps - 1, DIR); \ + } \ + else { \ + node_creating = new /datum/jps_node(cscan_last, node_top, JPS_HEURISTIC_CALL(cscan_last), node_top.depth + cscan_steps - 1, DIR); \ + } \ + open.enqueue(node_creating); \ + break; \ + } \ + cscan_next = get_step(cscan_current, DIR); \ + if(isnull(cscan_next) || (cscan_next.pathfinding_cycle == cycle) || !JPS_ADJACENCY_CALL(cscan_current, cscan_next)) { \ + break; \ + } \ + cscan_current.pathfinding_cycle = cycle; \ + cscan_last = cscan_current; \ + cscan_current = cscan_next; \ + cscan_steps++; \ + } \ + while(TRUE); + #endif + //* loop + while(length(open.array)) + node_top = open.dequeue() + node_top_pos = node_top.pos + #ifdef JPS_DEBUGGING + node_top.pos.color = JPS_VISUAL_COLOR_CURRENT + sleep(GLOB.jps_visualization_delay) + #else + CHECK_TICK + #endif + + // get distance and check completion + if(JPS_COMPLETION_CHECK(node_top_pos)) + #ifdef JPS_DEBUGGING + return jps_unwind_path(node_top, turfs_got_colored) + #else + return jps_unwind_path(node_top) + #endif + + // too deep, abort + if(node_top.depth + get_dist(node_top_pos, goal) >= max_depth) + #ifdef JPS_DEBUGGING + node_top.pos.color = JPS_VISUAL_COLOR_OUT_OF_BOUNDS + turfs_got_colored[node_top.pos] = turfs_got_colored[node_top.pos] || 0 + #endif + continue + + #ifdef JPS_DEBUGGING + if(!(turfs_got_colored[node_top.pos] -= 1)) + node_top.pos.color = JPS_VISUAL_COLOR_CLOSED + else if(turfs_got_colored[node_top.pos] > 0) + node_top.pos.color = JPS_VISUAL_COLOR_OPEN + node_top_pos.maptext = MAPTEXT("d [node_top.depth]
s [node_top.score]
o [max(turfs_got_colored[node_top.pos], 0)]") + #endif + + // get dir and run based on dir + node_top_dir = node_top.dir + if(node_top_dir & (node_top_dir - 1)) + // node is diagonal + dscan_dir1 = turn(node_top_dir, -45) + dscan_dir2 = turn(node_top_dir, 45) + dscan_node = null + dscan_current = node_top_pos + dscan_pass = TRUE + dscan_steps = 0 + dscan_initial = node_top.depth + do + // check if we're out of bounds + if(dscan_steps + dscan_initial + get_dist(dscan_current, goal) > max_depth) + #ifdef JPS_DEBUGGING + dscan_current.color = JPS_VISUAL_COLOR_OUT_OF_BOUNDS + turfs_got_colored[dscan_current] = -1 + #endif + break + // get next turf + // we don't do current turf because it's assumed already ran + dscan_next = get_step(dscan_current, node_top_dir) + #ifdef JPS_DEBUGGING + dscan_current.overlays += get_jps_scan_overlay(node_top_dir, TRUE) + turfs_got_colored[dscan_current] = turfs_got_colored[dscan_current] || 0 + #endif + // check it's 1. there and 2. we haven't checked it yet and + // 3. we can reach it; if not this is just pointless + if(isnull(dscan_next) || (dscan_next.pathfinding_cycle == cycle) || !JPS_ADJACENCY_CALL(dscan_current, dscan_next)) + break + // move up + dscan_current = dscan_next + ++dscan_steps + // check if it's close enough to goal + if(JPS_COMPLETION_CHECK(dscan_current)) + node_creating = new(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir) + #ifdef JPS_DEBUGGING + return jps_unwind_path(node_creating, turfs_got_colored) + #else + return jps_unwind_path(node_creating) + #endif + // perform the two cardinal scans + JPS_CARDINAL_SCAN(dscan_current, dscan_dir1) + if(!cscan_pass) + dscan_pass = FALSE + JPS_CARDINAL_SCAN(dscan_current, dscan_dir2) + if(!cscan_pass) + dscan_pass = FALSE + // check if scans did anything; if so, inject the diagonal node, which should already be + // proper linked with the created cardinal nodes + if(!dscan_pass) + if(isnull(dscan_node)) + dscan_node = new /datum/jps_node(dscan_current, node_top, JPS_HEURISTIC_CALL(dscan_current), node_top.depth + dscan_steps, node_top_dir) + #ifdef JPS_DEBUGGING + dscan_current.color = JPS_VISUAL_COLOR_OPEN + turfs_got_colored[dscan_current] = turfs_got_colored[dscan_current] + 1 + #endif + open.enqueue(dscan_node) + break + // set pathfinder cycle to prevent re-iteration of the same turfs + dscan_current.pathfinding_cycle = cycle + while(TRUE) + else + // node is cardinal + // check that it's valid and not blocked + cscan_current = get_step(node_top_pos, node_top_dir) + #ifdef JPS_DEBUGGING + cscan_current.overlays += get_jps_scan_overlay(node_top_dir, TRUE) + turfs_got_colored[cscan_current] = turfs_got_colored[cscan_current] || 0 + #endif + // check it's 1. there and 2. we haven't checked it yet and + // 3. we can reach it; if not this is just pointless + if(isnull(cscan_current) || (cscan_current.pathfinding_cycle == cycle) || !JPS_ADJACENCY_CALL(node_top_pos, cscan_current)) + else + // perform iteration + JPS_CARDINAL_SCAN(cscan_current, node_top_dir) + + //* clean up debugging + #ifdef JPS_DEBUGGING + jps_wipe_colors_after(turfs_got_colored, GLOB.jps_visualization_persist) + #endif + + //* clean up defines + #undef JPS_START_DIR + #undef JPS_COMPLETION_CHECK + #undef JPS_CARDINAL_DURING_DIAGONAL + #undef JPS_CARDINAL_SCAN + +/** + * The proc used to grab the nodes back in order from start to finish after the algorithm runs. + */ +#ifdef JPS_DEBUGGING +/datum/pathfinding/jps/proc/jps_unwind_path(datum/jps_node/top, list/turfs_got_colored) +#else +/datum/pathfinding/jps/proc/jps_unwind_path(datum/jps_node/top) +#endif + // found; build path end to start of nodes + var/list/path_built = list() + while(top) + path_built += top.pos + #ifdef JPS_DEBUGGING + top.pos.color = GLOB.jps_visualization_resolve? JPS_VISUAL_COLOR_INTERMEDIATE : JPS_VISUAL_COLOR_FOUND + turfs_got_colored[top] = TRUE + #endif + top = top.prev + // reverse + var/head = 1 + var/tail = length(path_built) + while(head < tail) + path_built.Swap(head++, tail--) + #ifdef JPS_DEBUGGING + if(GLOB.jps_visualization_resolve) + for(var/turf/T in jps_output_turfs(path_built)) + T.color = JPS_VISUAL_COLOR_FOUND + turfs_got_colored[top] = TRUE + jps_wipe_colors_after(turfs_got_colored, GLOB.jps_visualization_persist) + #endif + return path_built + +/datum/pathfinding/jps/proc/jps_estimate_dir(turf/start, turf/goal) + var/dx = abs(start.x - goal.x) + var/dy = abs(start.y - goal.y) + if(dx > dy) + return get_dir(start, goal) & (EAST|WEST) + else + return get_dir(start, goal) & (NORTH|SOUTH) + +/** + * takes a list of turf nodes from JPS return and converts it into a proper list of turfs to walk + */ +/proc/jps_output_turfs(list/turf/nodes) + if(isnull(nodes)) + return + . = list() + switch(length(nodes)) + if(0) + return + if(1) + return list(nodes[1]) + var/index = 1 + while(index < length(nodes)) + var/turf/current = nodes[index] + var/turf/next = nodes[index + 1] + var/safety = get_dist(current, next) + while(current && current != next) + . += current + current = get_step_towards(current, next) + if(!safety--) + CRASH("failed jps output processing due to running out of safety, that shouldn't be possible") + ++index + + . += nodes[index] + +#ifdef JPS_DEBUGGING + #undef JPS_DEBUGGING + + #undef JPS_VISUAL_COLOR_CLOSED + #undef JPS_VISUAL_COLOR_OPEN + #undef JPS_VISUAL_COLOR_CURRENT + #undef JPS_VISUAL_COLOR_FOUND +#endif diff --git a/code/_helpers/priority_queue_ch.dm b/code/_helpers/priority_queue_ch.dm new file mode 100644 index 0000000000..a77f218a04 --- /dev/null +++ b/code/_helpers/priority_queue_ch.dm @@ -0,0 +1,89 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2023 Citadel Station developers. *// + +/** + * An array-backed priority queue. + * + * The "front" of the queue is popped first; check comparators.dm for what this means. + */ +/datum/priority_queue + /// comparaison function + var/procpath/comparison + /// internal array + var/list/array = list() + +/datum/priority_queue/New(cmp) + src.comparison = cmp + array = list() + +/datum/priority_queue/proc/is_empty() + return length(array) == 0 + +/datum/priority_queue/proc/enqueue(entry) + array += entry + bubble_up(length(array)) + +/datum/priority_queue/proc/dequeue() + if(length(array) == 0) + return null + . = array[1] + array.Swap(1, length(array)) + --array.len + bubble_down(1) + +/datum/priority_queue/proc/peek() + return length(array)? array[1] : null + +// todo: define this +/datum/priority_queue/proc/bubble_up(index) + while(index >= 2 && call(comparison)(array[index], array[index / 2]) < 0) + array.Swap(index, index / 2) + index /= 2 + +// todo: define this +/datum/priority_queue/proc/bubble_down(index) + var/length = length(array) + var/next = index * 2 + while(next <= length) + // left always exists, right doesn't necessarily exist + if(call(comparison)(array[next], array[index]) < 0) + if(next < length && call(comparison)(array[next], array[next + 1]) > 0) + array.Swap(index, next + 1) + index = next + 1 + else + array.Swap(index, next) + index = next + else if(next < length && call(comparison)(array[next + 1], array[index]) < 0) + array.Swap(index, next + 1) + index = next + 1 + else + break + next = index * 2 + +/** + * returns copy of list of entries in no particular order + */ +/datum/priority_queue/proc/flattened() + return array.Copy() + +/datum/priority_queue/proc/remove_index(index) + var/length = length(array) + if(!index || index > length) + return + if(index == length) + . = array[index] + --array.len + return + . = array[index] + array.Swap(index, length) + --array.len + bubble_down(index) + +/datum/priority_queue/proc/find(entry) + return array.Find(entry) + +/datum/priority_queue/proc/remove_entry(entry) + return remove_index(array.Find(entry)) + +/datum/priority_queue/proc/size() + return length(array) diff --git a/code/_onclick/adjacent.dm b/code/_onclick/adjacent.dm index 25997f90ed..eab387e62e 100644 --- a/code/_onclick/adjacent.dm +++ b/code/_onclick/adjacent.dm @@ -141,4 +141,57 @@ Quick adjacency (to turf): useless. Throwpass may later need to be removed and replaced with a passcheck (bitfield on movable atom passflags). Since I don't want to complicate the click code rework by messing with unrelated systems it won't be changed here. -*/ \ No newline at end of file +*/ +//CHOMPEdit Begin +///True if the dir is north or south, false therwise +#define NSCOMPONENT(d) (d&(NORTH|SOUTH)) +///True if the dir is east/west, false otherwise +#define EWCOMPONENT(d) (d&(EAST|WEST)) +/** + * Turf adjacency + * + * - Always true if you're in the same turf + * - If you're vertically/horizontally adjacent, ensure there's no border obects + * - If you're diagonally adjacent, ensure you can pass to it with mutually adjacent squares + */ +/turf/proc/TurfAdjacency(turf/neighbor_turf, atom/target, atom/movable/mover) + if(neighbor_turf == src) + return TRUE + if(get_dist(src, neighbor_turf) > 1 || z != neighbor_turf.z) + return FALSE + // non diagonal + if(neighbor_turf.x == x || neighbor_turf.y == y) + return ClickCross(get_dir(src, neighbor_turf), TRUE, target, mover) && neighbor_turf.ClickCross(get_dir(neighbor_turf, src), TRUE, target, mover) + + // diagonal + var/reverse_dir = get_dir(neighbor_turf, src) + var/d1 = NSCOMPONENT(reverse_dir) + var/d2 = EWCOMPONENT(reverse_dir) + var/turf/checking + + // because byond's parser is awful and doesn't let us skip lines on ifs with comments after '\'s, + // we're going to comment above: + // criteria in order for both are: + // - not dense + // - could leave target + // - could go from diagonal to self + // - could go from diagonal to target + // - could leave self + checking = get_step(neighbor_turf, d1) + if(!checking.density && \ + neighbor_turf.ClickCross(d1, TRUE, target, mover) && \ + checking.ClickCross(d2, FALSE, target, mover) && \ + checking.ClickCross(turn(d1, 180), FALSE, target, mover) && \ + ClickCross(turn(d2, 180), TRUE, target, mover)) + return TRUE + checking = get_step(neighbor_turf, d2) + if(!checking.density && \ + neighbor_turf.ClickCross(d2, TRUE, target, mover) && \ + checking.ClickCross(d1, FALSE, target, mover) && \ + checking.ClickCross(turn(d2, 180), FALSE, target, mover) && \ + ClickCross(turn(d1, 180), TRUE, target, mover)) + return TRUE + return FALSE +#undef NSCOMPONENT +#undef EWCOMPONENT +//CHOMPEdit End diff --git a/code/controllers/subsystems/pathfinder_ch.dm b/code/controllers/subsystems/pathfinder_ch.dm new file mode 100644 index 0000000000..6127279a71 --- /dev/null +++ b/code/controllers/subsystems/pathfinder_ch.dm @@ -0,0 +1,135 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2023 Citadel Station developers. *// + +#define PATHFINDER_TIMEOUT 50 + +SUBSYSTEM_DEF(pathfinder) + name = "Pathfinder" + flags = SS_NO_INIT | SS_NO_FIRE + + /// pathfinding mutex - most algorithms depend on this + /// multi "threading" in byond just adds overhead + /// from everything trying to re-queue their executions + /// for this reason, much like with maploading, + /// it's somewhat pointless to have more than one operation going + /// at a time + var/pathfinding_mutex = FALSE + /// pathfinding calls blocked + var/pathfinding_blocked = 0 + /// pathfinding cycle - this is usable because of the mutex + /// this is used in place of a closed list in algorithms like JPS + /// to maximize performance. + var/tmp/pathfinding_cycle = 0 + +/** + * be aware that this emits a set of disjunct nodes + * use [jps_output_turfs()] to convert them into a proper turf path list. + * + * Please see [code/__HELPERS/pathfinding/jps.dm] for details on what JPS does/is. + */ +/datum/controller/subsystem/pathfinder/proc/get_path_jps(atom/movable/actor = GLOB.generic_pathfinding_actor, turf/goal, turf/start = get_turf(actor), target_distance = 1, max_path_length = 128) + var/datum/pathfinding/jps/instance = new(actor, start, goal, target_distance, max_path_length) + return run_pathfinding(instance) + +/** + * Please see [code/__HELPERS/pathfinding/astar.dm] for details on what JPS does/is. + */ +/datum/controller/subsystem/pathfinder/proc/get_path_astar(atom/movable/actor = GLOB.generic_pathfinding_actor, turf/goal, turf/start = get_turf(actor), target_distance = 1, max_path_length = 128) + var/datum/pathfinding/astar/instance = new(actor, start, goal, target_distance, max_path_length) + return run_pathfinding(instance) + +/datum/controller/subsystem/pathfinder/proc/default_ai_pathfinding(datum/ai_holder/holder, turf/goal, min_dist = 1, max_path = 128) + var/datum/pathfinding/astar/instance = new(holder.holder, get_turf(holder.holder), goal, min_dist, max_path * 2) + var/obj/item/weapon/card/id/potential_id = holder.holder.GetIdCard() + if(!isnull(potential_id)) + instance.ss13_with_access = potential_id.access?.Copy() + return run_pathfinding(instance) + +/datum/controller/subsystem/pathfinder/proc/default_circuit_pathfinding(obj/item/device/electronic_assembly/assembly, turf/goal, min_dist = 1, max_path = 128, var/list/access) + var/datum/pathfinding/jps/instance = new(assembly, get_turf(assembly), goal, min_dist, max_path) + instance.ss13_with_access = access.Copy() + return jps_output_turfs(run_pathfinding(instance)) + +/datum/controller/subsystem/pathfinder/proc/default_bot_pathfinding(mob/living/bot/bot, turf/goal, min_dist = 1, max_path = 128) + var/datum/pathfinding/jps/instance = new(bot, get_turf(bot), goal, min_dist, max_path) + instance.ss13_with_access = bot.botcard.access?.Copy() + return jps_output_turfs(run_pathfinding(instance)) + +/datum/controller/subsystem/pathfinder/proc/run_pathfinding(datum/pathfinding/instance) + var/started = world.time + ++pathfinding_blocked + if(pathfinding_blocked < 10) + while(pathfinding_mutex) + stoplag(1) + if(world.time > started + PATHFINDER_TIMEOUT) + stack_trace("pathfinder timeout; check debug logs.") + log_debug("pathfinder timeout of instance with debug variables [instance.debug_log_string()]") + return + else + while(pathfinding_mutex) + stoplag(3) + if(world.time > started + PATHFINDER_TIMEOUT) + stack_trace("pathfinder timeout; check debug logs.") + log_debug("pathfinder timeout of instance with debug variables [instance.debug_log_string()]") + return + --pathfinding_blocked + pathfinding_mutex = TRUE + . = instance.search() + if(world.time > started + PATHFINDER_TIMEOUT) + stack_trace("pathfinder timeout; check debug logs.") + log_debug("pathfinder timeout of instance with debug variables [instance.debug_log_string()]") + pathfinding_mutex = FALSE + +#undef PATHFINDER_TIMEOUT + +/proc/astar_debug(turf/target) + if(isnull(target)) + return + return SSpathfinder.get_path_astar(usr, target, get_turf(usr)) + +/proc/jps_debug(turf/target) + if(isnull(target)) + return + return SSpathfinder.get_path_jps(usr, target, get_turf(usr)) + +/proc/old_astar_debug(turf/target) + if(isnull(target)) + return + return graph_astar(get_turf(usr), target, TYPE_PROC_REF(/turf, CardinalTurfsWithAccess), TYPE_PROC_REF(/turf, Distance), 0, 128, 1) + +/proc/old_jps_debug(turf/target) + var/turf/start = get_turf(usr) + var/atom/movable/delegate_for_tg = new(start) + var/datum/tg_jps_pathfind/tg_instance = new(delegate_for_tg, target, null, 128, 1, FALSE, null) + return tg_instance.search() + +/proc/pathfinding_run_all(turf/start = get_turf(usr), turf/goal) + var/pass_silicons_astar = SSpathfinder.get_path_astar(goal = goal, start = start, target_distance = 1, max_path_length = 256) + var/pass_silicons_jps = SSpathfinder.get_path_jps(goal = goal, start = start, target_distance = 1, max_path_length = 256) + // old astar has been cut because it's such horrible code it's not worth benchmarking against the other 3. + // var/pass_old_astar = graph_astar( + // start, + // goal, + // TYPE_PROC_REF(/turf, CardinalTurfsWithAccess), + // TYPE_PROC_REF(/turf, Distance), + // 0, + // 128, + // 1, + // ) + var/atom/movable/delegate_for_tg = new(start) + var/datum/tg_jps_pathfind/tg_instance = new(delegate_for_tg, goal, null, 256, 1, FALSE, null) + var/pass_tg_jps = tg_instance.search() + pass_silicons_astar = !!length(pass_silicons_astar) + pass_silicons_jps = !!length(pass_silicons_jps) + // pass_old_astar = !!length(pass_old_astar) + pass_tg_jps = !!length(pass_tg_jps) + if(pass_silicons_astar != pass_silicons_jps || pass_silicons_jps != pass_tg_jps) + log_and_message_admins("turf pair [COORD(start)], [COORD(goal)] mismatch silicons-astar [pass_silicons_astar] silicons-jps [pass_silicons_jps] tg-jps [pass_tg_jps]") + else + log_and_message_admins("turf pair [COORD(start)], [COORD(goal)] succeeded") + +/proc/pathfinding_run_benchmark(times = 1000, turf/source = get_turf(usr)) + var/list/turf/nearby = RANGE_TURFS(100, source) + for(var/i in 1 to min(times, 10000)) + var/turf/picked = pick(nearby) + pathfinding_run_all(source, picked) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 6b0b4744af..db5c3dc49f 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -522,37 +522,10 @@ return 1 else return 0 - -// Show a message to all mobs and objects in sight of this atom -// Use for objects performing visible actions -// message is output to anyone who can see, e.g. "The [src] does something!" -// blind_message (optional) is what blind people will hear e.g. "You hear something!" +//CHOMPEdit Begin /atom/proc/visible_message(var/message, var/blind_message, var/list/exclude_mobs, var/range = world.view, var/runemessage = "👁") - - //VOREStation Edit - var/list/see - if(isbelly(loc)) - var/obj/belly/B = loc - see = B.get_mobs_and_objs_in_belly() - else - see = get_mobs_and_objs_in_view_fast(get_turf(src), range, remote_ghosts = FALSE) - //VOREStation Edit End - - var/list/seeing_mobs = see["mobs"] - var/list/seeing_objs = see["objs"] - if(LAZYLEN(exclude_mobs)) - seeing_mobs -= exclude_mobs - - for(var/obj/O as anything in seeing_objs) - O.show_message(message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE) - for(var/mob/M as anything in seeing_mobs) - if(M.see_invisible >= invisibility && MOB_CAN_SEE_PLANE(M, plane)) - M.show_message(message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE) - if(runemessage != -1) - M.create_chat_message(src, "[runemessage]", FALSE, list("emote"), audible = FALSE) - else if(blind_message) - M.show_message(blind_message, AUDIBLE_MESSAGE) - + SEND_GLOBAL_SIGNAL(COMSIG_VISIBLE_MESSAGE, src, message, blind_message, exclude_mobs, range, runemessage, isbelly(loc)) +//CHOMPEdit End // Show a message to all mobs and objects in earshot of this atom // Use for objects performing audible actions // message is the message output to anyone who can hear. diff --git a/code/game/atoms_movable_ch.dm b/code/game/atoms_movable_ch.dm index 2a7acdfd72..8d02ef8c03 100644 --- a/code/game/atoms_movable_ch.dm +++ b/code/game/atoms_movable_ch.dm @@ -60,3 +60,6 @@ /atom/movable/proc/exit_belly(obj/belly/B) return + +/atom/movable/proc/show_message(msg, type, alt, alt_type)//Message, type of message (1 or 2), alternative message, alt message type (1 or 2) + return diff --git a/code/game/gamemodes/changeling/changeling_powers.dm b/code/game/gamemodes/changeling/changeling_powers.dm index 0fc98997ce..699486b6a4 100644 --- a/code/game/gamemodes/changeling/changeling_powers.dm +++ b/code/game/gamemodes/changeling/changeling_powers.dm @@ -209,7 +209,7 @@ var/global/list/possible_changeling_IDs = list("Alpha","Beta","Gamma","Delta","E to_chat(src, "We cannot reach \the [M] with a sting!") return 0 //One is inside, the other is outside something. // Maximum queued turfs set to 25; I don't *think* anything raises sting_range above 2, but if it does the 25 may need raising - if(!AStar(src.loc, M.loc, /turf/proc/AdjacentTurfsRangedSting, /turf/proc/Distance, max_nodes=25, max_node_depth=sting_range)) //If we can't find a path, fail + if(!SSpathfinder.get_path_jps(src, get_turf(src), get_turf(M), max_path_length = 25)) //CHOMPEdit to_chat(src, "We cannot find a path to sting \the [M] by!") return 0 return 1 diff --git a/code/game/machinery/doors/airlock_ch.dm b/code/game/machinery/doors/airlock_ch.dm index da661f4659..00cfa79644 100644 --- a/code/game/machinery/doors/airlock_ch.dm +++ b/code/game/machinery/doors/airlock_ch.dm @@ -1,6 +1,9 @@ /obj/machinery/door/airlock/scp name = "SCP Access" icon = 'icons/obj/doors/SCPdoor.dmi' - //req_one_access = list(access_maint_tunnels) + //req_one_access = list(access_maint_tunnels) open_sound_powered = 'sound/machines/scp1o.ogg' - close_sound_powered = 'sound/machines/scp1c.ogg' \ No newline at end of file + close_sound_powered = 'sound/machines/scp1c.ogg' + +/obj/machinery/door/airlock/can_pathfinding_enter(atom/movable/actor, dir, datum/pathfinding/search) + return ..() || (has_access(req_access, req_one_access, search.ss13_with_access) && !locked && !inoperable()) diff --git a/code/game/machinery/doors/firedoor_ch.dm b/code/game/machinery/doors/firedoor_ch.dm index be3b96b3da..63f68c0245 100644 --- a/code/game/machinery/doors/firedoor_ch.dm +++ b/code/game/machinery/doors/firedoor_ch.dm @@ -5,3 +5,9 @@ glass = 1 open_sound = 'sound/machines/firewide1o.ogg' close_sound = 'sound/machines/firewide1c.ogg' + +/obj/machinery/door/firedoor/border_only/can_pathfinding_exit(atom/movable/actor, dir, datum/pathfinding/search) + return (src.dir != dir) || ..() + +/obj/machinery/door/firedoor/border_only/can_pathfinding_enter(atom/movable/actor, dir, datum/pathfinding/search) + return (src.dir != dir) || ..() diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm index 7b350805c2..513f1a7f83 100644 --- a/code/game/machinery/doors/windowdoor.dm +++ b/code/game/machinery/doors/windowdoor.dm @@ -89,7 +89,13 @@ if(get_dir(mover, target) == reverse_dir[dir]) // From elsewhere to here, can't move against our dir return !density return TRUE +//CHOMPEdit Begin +/obj/machinery/door/window/can_pathfinding_enter(atom/movable/actor, dir, datum/pathfinding/search) + return (src.dir != dir) || ..() || (has_access(req_access, req_one_access, search.ss13_with_access) && !inoperable()) +/obj/machinery/door/window/can_pathfinding_exit(atom/movable/actor, dir, datum/pathfinding/search) + return (src.dir != dir) || ..() || (has_access(req_access, req_one_access, search.ss13_with_access) && !inoperable()) +//CHOMPEdit End /obj/machinery/door/window/Uncross(atom/movable/mover, turf/target) if(istype(mover) && mover.checkpass(PASSGLASS)) return TRUE @@ -97,6 +103,7 @@ return !density return TRUE + /obj/machinery/door/window/CanZASPass(turf/T, is_zone) if(get_dir(T, loc) == turn(dir, 180)) if(is_zone) // No merging allowed. diff --git a/code/game/objects/items/weapons/id cards/station_ids.dm b/code/game/objects/items/weapons/id cards/station_ids.dm index fe35bdc453..7d6f8479f7 100644 --- a/code/game/objects/items/weapons/id cards/station_ids.dm +++ b/code/game/objects/items/weapons/id cards/station_ids.dm @@ -8,7 +8,7 @@ SPECIES_TESHARI = 'icons/mob/species/teshari/id.dmi' ) - var/access = list() + var/list/access = list() //CHOMPEdit var/registered_name = "Unknown" // The name registered_name on the card slot_flags = SLOT_ID | SLOT_EARS diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 3debc36657..45f728e67c 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -186,10 +186,10 @@ /obj/proc/see_emote(mob/M as mob, text, var/emote_type) return - +/* CHOMP Removal /obj/proc/show_message(msg, type, alt, alt_type)//Message, type of message (1 or 2), alternative message, alt message type (1 or 2) return - +*/ // Used to mark a turf as containing objects that are dangerous to step onto. /obj/proc/register_dangerous_to_step() var/turf/T = get_turf(src) diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm index ae26157f39..5c89bb1cc6 100644 --- a/code/game/objects/structures/plasticflaps.dm +++ b/code/game/objects/structures/plasticflaps.dm @@ -27,7 +27,20 @@ return else return - +//CHOMPEdit Begin +/obj/structure/plasticflaps/can_pathfinding_enter(atom/movable/actor, dir, datum/pathfinding/search) + if(isliving(actor)) + var/mob/living/L = actor + if(isbot(L)) + return TRUE + if(L.can_ventcrawl()) + return TRUE + if(L.mob_size <= MOB_TINY) + return TRUE + return FALSE + return TRUE + //return isnull(actor.pulling)? TRUE : can_pathfinding_enter(actor.pulling, dir, search) +//CHOMPEdit End /obj/structure/plasticflaps/CanPass(atom/A, turf/T) if(istype(A) && A.checkpass(PASSGLASS)) return prob(60) @@ -65,4 +78,4 @@ name = "airtight plastic flaps" desc = "Heavy duty, airtight, plastic flaps. Have extra safety installed, preventing passage of living beings." can_atmos_pass = ATMOS_PASS_NO - can_pass_lying = FALSE \ No newline at end of file + can_pass_lying = FALSE diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index 15eed997f1..962e7f7d9f 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -116,7 +116,13 @@ take_damage(proj_damage) return +//CHOMPEdit Begin +/obj/structure/window/can_pathfinding_enter(atom/movable/actor, dir, datum/pathfinding/search) + return ..() || (!fulltile && (src.dir) != dir) +/obj/structure/window/can_pathfinding_exit(atom/movable/actor, dir, datum/pathfinding/search) + return ..() || (!fulltile && (src.dir != dir)) +//CHOMPEdit End /obj/structure/window/ex_act(severity) switch(severity) if(1.0) @@ -703,4 +709,4 @@ qdel(src) return TRUE return FALSE -*/ \ No newline at end of file +*/ diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 90c9cddfe9..61be96cb27 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -12,6 +12,18 @@ var/nitrogen = 0 var/phoron = 0 + //CHOMPEdit Begin + //* Movement / Pathfinding + /// How much the turf slows down movement, if any. + var/slowdown = 0 + /// Pathfinding cost + var/path_weight = 1 + /// danger flags to avoid + var/turf_path_danger = NONE + /// pathfinding id - used to avoid needing a big closed list to iterate through every cycle of jps + var/pathfinding_cycle + //CHOMPEdit End + //Properties for airtight tiles (/wall) var/thermal_conductivity = 0.05 var/heat_capacity = 1 @@ -49,8 +61,8 @@ directional_opacity = ALL_CARDINALS //Pathfinding related - if(movement_cost && pathweight == 1) // This updates pathweight automatically. - pathweight = movement_cost + if(movement_cost && path_weight == 1) // This updates pathweight automatically. //CHOMPEdit + path_weight = movement_cost var/turf/Ab = GetAbove(src) if(Ab) @@ -262,7 +274,7 @@ /turf/proc/Distance(turf/t) if(get_dist(src,t) == 1) var/cost = (src.x - t.x) * (src.x - t.x) + (src.y - t.y) * (src.y - t.y) - cost *= (pathweight+t.pathweight)/2 + cost *= ((isnull(path_weight)? slowdown : path_weight) + (isnull(t.path_weight)? t.slowdown : t.path_weight))/2 //CHOMPEdit return cost else return get_dist(src,t) diff --git a/code/modules/ai/ai_holder_follow.dm b/code/modules/ai/ai_holder_follow.dm index 45d0d1e7a0..387d8d7f5b 100644 --- a/code/modules/ai/ai_holder_follow.dm +++ b/code/modules/ai/ai_holder_follow.dm @@ -65,4 +65,4 @@ return FALSE if(get_dist(holder, leader) > follow_distance) return TRUE - return FALSE \ No newline at end of file + return FALSE diff --git a/code/modules/ai/ai_holder_movement.dm b/code/modules/ai/ai_holder_movement.dm index b553b86b7e..efa96c7b03 100644 --- a/code/modules/ai/ai_holder_movement.dm +++ b/code/modules/ai/ai_holder_movement.dm @@ -191,4 +191,4 @@ directions += L.target_up if(directions.len) - L.climbLadder(holder, pick(directions)) \ No newline at end of file + L.climbLadder(holder, pick(directions)) diff --git a/code/modules/ai/ai_holder_pathfinding.dm b/code/modules/ai/ai_holder_pathfinding.dm index fed867ee24..0b87c58e57 100644 --- a/code/modules/ai/ai_holder_pathfinding.dm +++ b/code/modules/ai/ai_holder_pathfinding.dm @@ -29,6 +29,12 @@ if(!A) ai_log("calculate_path() : Called without an atom. Exiting.",AI_LOG_WARNING) return + //CHOMPEdit Begin + var/turf/T = get_turf(A) + if(!istype(T) || T.z != holder.z) + ai_log("calculate_path() : Called with invalid destination. Exiting.",AI_LOG_WARNING) + return + //CHOMPEdit End if(!use_astar) // If we don't use A* then this is pointless. ai_log("calculate_path() : Not using A*, Exiting.", AI_LOG_DEBUG) @@ -42,7 +48,7 @@ /datum/ai_holder/proc/get_path(var/turf/target,var/get_to = 1, var/max_distance = world.view*6) ai_log("get_path() : Entering.",AI_LOG_DEBUG) forget_path() - var/list/new_path = AStar(get_turf(holder.loc), target, astar_adjacent_proc, /turf/proc/Distance, min_target_dist = get_to, max_node_depth = max_distance, id = holder.IGetID(), exclude = obstacles) + var/list/new_path = SSpathfinder.default_ai_pathfinding(src, target, get_to) //CHOMPEdit if(new_path && new_path.len) path = new_path @@ -55,4 +61,4 @@ return 0 ai_log("get_path() : Exiting.", AI_LOG_DEBUG) - return path.len \ No newline at end of file + return path.len diff --git a/code/modules/ai/ai_holder_targeting.dm b/code/modules/ai/ai_holder_targeting.dm index 97165eaa32..ba2b33e174 100644 --- a/code/modules/ai/ai_holder_targeting.dm +++ b/code/modules/ai/ai_holder_targeting.dm @@ -30,16 +30,19 @@ // A lot of this is based off of /TG/'s AI code. +//CHOMPEdit Begin // Step 1, find out what we can see. /datum/ai_holder/proc/list_targets() - . = ohearers(vision_range, holder) - . -= dview_mob // Not the dview mob! + . = hearers(vision_range, holder) - holder // Remove ourselves to prevent suicidal decisions. ~ SRC is the ai_holder. - var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/blob)) + var/static/list/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha)) + var/static/list/ignore = typecacheof(list(/mob/observer)) for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines)) if(can_see(holder, HM, vision_range)) . += HM + . = typecache_filter_list_reverse(., ignore) +//CHOMPEdit End // Step 2, filter down possible targets to things we actually care about. /datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE) @@ -123,8 +126,7 @@ return closest_targets /datum/ai_holder/proc/can_attack(atom/movable/the_target, var/vision_required = TRUE) - if(!can_see_target(the_target) && vision_required) - return FALSE + //CHOMP Removal (optimizing by making most intense check last) if(!belly_attack) if(isbelly(holder.loc)) return FALSE @@ -185,6 +187,17 @@ return TRUE // return FALSE +//CHOMPEdit Begin +//It may seem a bit funny to define a proc above and then immediately override it in the same file +//But this is basically layering the checks so that the vision check will always come last +/datum/ai_holder/can_attack(atom/movable/the_target, var/vision_required = TRUE) + if(!..()) + return FALSE + if(vision_required && !can_see_target(the_target)) + return FALSE + return TRUE +//CHOMPEdit End + // 'Soft' loss of target. They may still exist, we still have some info about them maybe. /datum/ai_holder/proc/lose_target() ai_log("lose_target() : Entering.", AI_LOG_TRACE) diff --git a/code/modules/integrated_electronics/subtypes/smart.dm b/code/modules/integrated_electronics/subtypes/smart.dm index 0e16cd4079..99b7551071 100644 --- a/code/modules/integrated_electronics/subtypes/smart.dm +++ b/code/modules/integrated_electronics/subtypes/smart.dm @@ -31,4 +31,51 @@ set_pin_data(IC_OUTPUT, 1, desired_dir) push_data() - activate_pin(2) \ No newline at end of file + activate_pin(2) //CHOMPEdit + +//CHOMPEdit Begin +/obj/item/integrated_circuit/smart/advanced_pathfinder + name = "advanced pathfinder" + desc = "This circuit uses a complex processor for long-range pathfinding." + extended_desc = "This circuit uses absolute coordinates to find its target. A path will be generated to the target, taking obstacles into account, \ + and pathing around any instances of said input. The passkey provided from a card reader is used to calculate a valid path through airlocks." + icon_state = "numberpad" + complexity = 40 + cooldown_per_use = 50 + inputs = list("X target" = IC_PINTYPE_NUMBER,"Y target" = IC_PINTYPE_NUMBER,"obstacle" = IC_PINTYPE_REF,"access" = IC_PINTYPE_STRING) + outputs = list("X" = IC_PINTYPE_LIST,"Y" = IC_PINTYPE_LIST) + activators = list("calculate path" = IC_PINTYPE_PULSE_IN, "on calculated" = IC_PINTYPE_PULSE_OUT,"not calculated" = IC_PINTYPE_PULSE_OUT) + spawn_flags = IC_SPAWN_RESEARCH + power_draw_per_use = 80 + var/obj/item/weapon/card/id/idc + +/obj/item/integrated_circuit/smart/advanced_pathfinder/Initialize(mapload) + .=..() + idc = new(src) + +/obj/item/integrated_circuit/smart/advanced_pathfinder/do_work() + if(!assembly) + activate_pin(3) + return + //idc.access = assembly.access_card.access + var/turf/a_loc = get_turf(assembly) + + var/turf/target_turf = locate(get_pin_data(IC_INPUT, 1), get_pin_data(IC_INPUT, 2), a_loc.z) + var/list/P = SSpathfinder.default_circuit_pathfinding(src, target_turf, 0, 200) + + if(!P) + activate_pin(3) + return + else + var/list/Xn = new/list(P.len) + var/list/Yn = new/list(P.len) + var/turf/T + for(var/i =1 to P.len) + T=P[i] + Xn[i] = T.x + Yn[i] = T.y + set_pin_data(IC_OUTPUT, 1, Xn) + set_pin_data(IC_OUTPUT, 2, Yn) + push_data() + activate_pin(2) +//CHOMPEdit End diff --git a/code/modules/mob/living/bot/bot.dm b/code/modules/mob/living/bot/bot.dm index 8a5e382ed8..9254e9aba2 100644 --- a/code/modules/mob/living/bot/bot.dm +++ b/code/modules/mob/living/bot/bot.dm @@ -334,7 +334,7 @@ /mob/living/bot/proc/startPatrol() var/turf/T = getPatrolTurf() if(T) - patrol_path = AStar(get_turf(loc), T, /turf/proc/CardinalTurfsWithAccess, /turf/proc/Distance, 0, max_patrol_dist, id = botcard, exclude = obstacle) + target_path = SSpathfinder.default_bot_pathfinding(src, T, 1) //CHOMPEdit if(!patrol_path) patrol_path = list() obstacle = null @@ -366,7 +366,7 @@ return /mob/living/bot/proc/calcTargetPath() - target_path = AStar(get_turf(loc), get_turf(target), /turf/proc/CardinalTurfsWithAccess, /turf/proc/Distance, 0, max_target_dist, id = botcard, exclude = obstacle) + target_path = SSpathfinder.default_bot_pathfinding(src, get_turf(target), 1) //CHOMPEdit if(!target_path) if(target && target.loc) ignore_list |= target diff --git a/code/modules/mob/living/bot/farmbot.dm b/code/modules/mob/living/bot/farmbot.dm index 41b2fb578c..111046b0cb 100644 --- a/code/modules/mob/living/bot/farmbot.dm +++ b/code/modules/mob/living/bot/farmbot.dm @@ -155,10 +155,9 @@ if(++times_idle == 150) turn_off() //VOREStation Add - Idle shutoff time /mob/living/bot/farmbot/calcTargetPath() // We need to land NEXT to the tray, because the tray itself is impassable - for(var/trayDir in list(NORTH, SOUTH, EAST, WEST)) - target_path = AStar(get_turf(loc), get_step(get_turf(target), trayDir), /turf/proc/CardinalTurfsWithAccess, /turf/proc/Distance, 0, max_target_dist, id = botcard) - if(target_path) - break + if(isnull(target)) + return + target_path = SSpathfinder.default_bot_pathfinding(src, get_turf(target), 1, 32) //CHOMPEdit if(!target_path) ignore_list |= target target = null diff --git a/code/modules/mob/living/simple_mob/subtypes/humanoid/mercs/mercs.dm b/code/modules/mob/living/simple_mob/subtypes/humanoid/mercs/mercs.dm index 448abce63b..fbcd298800 100644 --- a/code/modules/mob/living/simple_mob/subtypes/humanoid/mercs/mercs.dm +++ b/code/modules/mob/living/simple_mob/subtypes/humanoid/mercs/mercs.dm @@ -56,7 +56,7 @@ for(var/mob/M in range(T, 2)) if(M.faction == faction) // Don't grenade our friends return FALSE - if(M in oview(src, special_attack_max_range)) // And lets check if we can actually see at least two people before we throw a grenade + if(M!=src && can_see(M)) // And lets check if we can actually see at least two people before we throw a grenade //CHOMPEdit dear god if(!M.stat) // Dead things don't warrant a grenade mob_count ++ if(mob_count < 2) @@ -88,6 +88,7 @@ wander = TRUE // ... but "patrol" a little. intelligence_level = AI_SMART // Also knows not to walk while confused if it risks death. threaten_delay = 30 SECONDS // Mercs will give you 30 seconds to leave or get shot. + use_astar = TRUE //CHOMPEdit /datum/ai_holder/simple_mob/merc/ranged pointblank = TRUE // They get close? Just shoot 'em! diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 67865fa918..19101727f7 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -64,7 +64,7 @@ . = ..() //return QDEL_HINT_HARDDEL_NOW Just keep track of mob references. They delete SO much faster now. -/mob/proc/show_message(msg, type, alt, alt_type)//Message, type of message (1 or 2), alternative message, alt message type (1 or 2) +/mob/show_message(msg, type, alt, alt_type)//Message, type of message (1 or 2), alternative message, alt message type (1 or 2) //CHOMPEdit show_message() moved to /atom/movable var/time = say_timestamp() if(!client && !teleop) return diff --git a/code/modules/mob/mob_movement_ch.dm b/code/modules/mob/mob_movement_ch.dm new file mode 100644 index 0000000000..94acb65227 --- /dev/null +++ b/code/modules/mob/mob_movement_ch.dm @@ -0,0 +1,13 @@ +/mob/living/can_pathfinding_enter(atom/movable/actor, dir, datum/pathfinding/search) + // mobs are ignored by pathfinding for now + // in the future we'll need a way for mobs to not collide into + // each other during cooperative AI actions + // or even for say, mulebots moving around humans when blocked. + return TRUE + +/mob/living/can_pathfinding_exit(atom/movable/actor, dir, datum/pathfinding/search) + // mobs are ignored by pathfinding for now + // in the future we'll need a way for mobs to not collide into + // each other during cooperative AI actions + // or even for say, mulebots moving around humans when blocked. + return TRUE diff --git a/code/modules/multiz/turf.dm b/code/modules/multiz/turf.dm index eddc1743df..a8c80f7093 100644 --- a/code/modules/multiz/turf.dm +++ b/code/modules/multiz/turf.dm @@ -55,6 +55,7 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr can_build_into_floor = TRUE can_dirty = FALSE // It's open space can_start_dirty = FALSE + turf_path_danger = TURF_PATH_DANGER_FALL //CHOMPEdit /turf/simulated/open/vacuum oxygen = 0 diff --git a/code/modules/vore/eating/belly_obj_ch.dm b/code/modules/vore/eating/belly_obj_ch.dm index 64a9da370d..e90b6b7878 100644 --- a/code/modules/vore/eating/belly_obj_ch.dm +++ b/code/modules/vore/eating/belly_obj_ch.dm @@ -671,3 +671,9 @@ var/obj/item/I = thing surrounding.Add(get_belly_surrounding(I.contents)) return surrounding + +/obj/belly/proc/effective_emote_hearers() + . = list(loc) + for(var/atom/movable/AM as anything in contents) + //if(AM.atom_flags & ATOM_HEAR) + . += AM diff --git a/icons/screen/debug/pathfinding.dmi b/icons/screen/debug/pathfinding.dmi new file mode 100644 index 0000000000..0c80859823 Binary files /dev/null and b/icons/screen/debug/pathfinding.dmi differ diff --git a/modular_chomp/code/modules/client/hearable.dm b/modular_chomp/code/modules/client/hearable.dm new file mode 100644 index 0000000000..318898c7a0 --- /dev/null +++ b/modular_chomp/code/modules/client/hearable.dm @@ -0,0 +1,134 @@ +// visible_message is way too fucking expensive because it recursively searches through turf contents to find something which should actually recieve the message. +// so I'm sick of it and I'm making a component to keep track of anything that needs visible_message +// note: this is seperate from normal Say(), objects that need to listen to dialogue don't need this component. +// Love and sorry if this horribly breaks - Cadyn +/datum/component/hearer + var/atom/movable/parent_atom + +/datum/component/hearer/RegisterWithParent() + . = ..() + parent_atom = parent + if(!istype(parent_atom)) + CRASH("hearer intialized on non-atom") + RegisterSignal(SSdcs, COMSIG_VISIBLE_MESSAGE, PROC_REF(on_message)) + +/datum/component/hearer/UnregisterFromParent() + . = ..() + parent_atom = null + UnregisterSignal(SSdcs, COMSIG_VISIBLE_MESSAGE) + +/datum/component/hearer/proc/on_message(var/dcs,var/atom/source, var/message, var/blind_message, var/list/exclude_mobs, var/range, var/runemessage = "👁", var/inbelly) + if(!AreConnectedZLevels(source.z,parent_atom.z)) + return + if(inbelly && !(parent_atom.loc == source.loc)) + return + if(parent_atom in exclude_mobs) + return + + var/turf/source_turf = get_turf(source) + var/turf/parent_turf = get_turf(parent_atom) + + if(!istype(source_turf) || !istype(parent_turf)) + return + + //Most expensive checks last + if(get_dist(source_turf,parent_turf) > range) + return + + if(source_turf.z != parent_turf.z) + if(source_turf.z > parent_turf.z) + var/turf/curturf = GetAbove(parent_turf) + while(isopenspace(curturf) && curturf.z != source_turf.z) + curturf = GetAbove(curturf) + + if(!isopenspace(curturf)) //Last also has to be open space + return + + if(curturf.z != source_turf.z) + return + else + var/turf/curturf = GetAbove(source_turf) + + while(isopenspace(curturf) && curturf.z != parent_turf.z) + curturf = GetAbove(curturf) + + if(!isopenspace(curturf)) //Last also has to be open space + return + + if(curturf.z != parent_turf.z) + return + + + if(ismob(parent_atom)) + var/mob/M = parent_atom + if(!M.ckey) + return + if((M.see_invisible >= source.invisibility) && MOB_CAN_SEE_PLANE(M, source.plane)) + M.show_message(message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE) + if(runemessage != -1) + M.create_chat_message(source, "[runemessage]", FALSE, list("emote"), audible = FALSE) + else if(blind_message) + M.show_message(blind_message, AUDIBLE_MESSAGE) + else + parent_atom.show_message(message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE) + +//Atom definition (base) + +/atom/movable + var/datum/component/hearer/hearer + +/atom/movable/Destroy() + if(hearer) QDEL_NULL(hearer) + . = ..() + +//Mob definition + +/mob/Login() + . = ..() + if(!hearer) hearer = AddComponent(/datum/component/hearer) //Only add these to mobs that have a client at some point in time + +//Overmap ship definition (I have no idea why they use show_message?? but ok) + +/obj/effect/overmap/visitable/ship/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) + +//Holopad definition + +/obj/machinery/hologram/holopad/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) + +//UAV definition + +/obj/item/device/uav/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) + +//AIcard definition + +/obj/item/device/aicard/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) + +//Tape recorder definition + +/obj/item/device/taperecorder/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) + +//Portal definition + +/obj/effect/map_effect/portal/master/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) + +//Communicator definition + +/obj/item/device/communicator/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) + +/obj/item/device/paicard/Initialize() + . = ..() + hearer = AddComponent(/datum/component/hearer) diff --git a/tools/build/build-testing.ps1 b/tools/build/build-testing.ps1 new file mode 100644 index 0000000000..2076350f14 --- /dev/null +++ b/tools/build/build-testing.ps1 @@ -0,0 +1,6 @@ +Set-Variable -Name "basedir" -Value "$PSScriptRoot/../.." + +(Get-Content "$basedir/vorestation.dme").Replace('#include "maps\southern_cross\southern_cross.dm"', '#include "maps\virgo_minitest\virgo_minitest.dm"') | Set-Content "$basedir/vorestation.dme" +& "$basedir/tools/build/build.bat" +(Get-Content "$basedir/vorestation.dme").Replace('#include "maps\virgo_minitest\virgo_minitest.dm"', '#include "maps\southern_cross\southern_cross.dm"') | Set-Content "$basedir/vorestation.dme" +Read-Host -Prompt "Press any key to continue" \ No newline at end of file diff --git a/vorestation.dme b/vorestation.dme index d1329dc221..690608f115 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -127,6 +127,7 @@ #include "code\__defines\wires.dm" #include "code\__defines\xenoarcheaology.dm" #include "code\__defines\ZAS.dm" +#include "code\__defines\_flags\turf_flags_ch.dm" #include "code\__defines\dcs\flags.dm" #include "code\__defines\dcs\helpers.dm" #include "code\__defines\dcs\signals.dm" @@ -150,6 +151,7 @@ #include "code\_helpers\_lists.dm" #include "code\_helpers\atmospherics.dm" #include "code\_helpers\atom_movables.dm" +#include "code\_helpers\distance_ch.dm" #include "code\_helpers\events.dm" #include "code\_helpers\files.dm" #include "code\_helpers\game.dm" @@ -159,12 +161,14 @@ #include "code\_helpers\icons.dm" #include "code\_helpers\icons_procs.dm" #include "code\_helpers\icons_vr.dm" +#include "code\_helpers\legacy_tg_path_ch.dm" #include "code\_helpers\lighting.dm" #include "code\_helpers\logging.dm" #include "code\_helpers\logging_vr.dm" #include "code\_helpers\matrices.dm" #include "code\_helpers\mobs.dm" #include "code\_helpers\names.dm" +#include "code\_helpers\priority_queue_ch.dm" #include "code\_helpers\sanitize_values.dm" #include "code\_helpers\shell.dm" #include "code\_helpers\storage.dm" @@ -180,8 +184,12 @@ #include "code\_helpers\view.dm" #include "code\_helpers\visual_filters.dm" #include "code\_helpers\widelists_ch.dm" +#include "code\_helpers\graphs\astar_ch.dm" #include "code\_helpers\icons\flatten.dm" #include "code\_helpers\logging\ui.dm" +#include "code\_helpers\pathfinding_ch\astar.dm" +#include "code\_helpers\pathfinding_ch\common.dm" +#include "code\_helpers\pathfinding_ch\jps.dm" #include "code\_helpers\sorts\__main.dm" #include "code\_helpers\sorts\comparators.dm" #include "code\_helpers\sorts\TimSort.dm" @@ -320,6 +328,7 @@ #include "code\controllers\subsystems\nightshift.dm" #include "code\controllers\subsystems\overlays.dm" #include "code\controllers\subsystems\overmap_renamer_vr.dm" +#include "code\controllers\subsystems\pathfinder_ch.dm" #include "code\controllers\subsystems\persist_vr.dm" #include "code\controllers\subsystems\persistence.dm" #include "code\controllers\subsystems\ping.dm" @@ -2935,6 +2944,7 @@ #include "code\modules\mob\mob_helpers.dm" #include "code\modules\mob\mob_helpers_vr.dm" #include "code\modules\mob\mob_movement.dm" +#include "code\modules\mob\mob_movement_ch.dm" #include "code\modules\mob\mob_planes.dm" #include "code\modules\mob\mob_planes_vr.dm" #include "code\modules\mob\mob_transformation_simple.dm" @@ -4719,6 +4729,7 @@ #include "modular_chomp\code\modules\artifice\deadringer.dm" #include "modular_chomp\code\modules\balloon_alert\balloon_alert.dm" #include "modular_chomp\code\modules\casino\casino_map_atoms.dm" +#include "modular_chomp\code\modules\client\hearable.dm" #include "modular_chomp\code\modules\client\preferences.dm" #include "modular_chomp\code\modules\client\preferences_spawnpoints.dm" #include "modular_chomp\code\modules\client\preference_setup\global\setting_datums.dm"