From 4fe27ddf365bfc4dea0e396ee07d9f91f335ae83 Mon Sep 17 00:00:00 2001 From: AnturK Date: Thu, 7 Apr 2022 22:57:48 +0200 Subject: [PATCH] JPS Improvements and fixes (#66015) Extends CanAStarPass to atom level and allows turfs to specify their pass check method to speed this up a bit. Allows pathing over openspace with lattices/by flying things, otherwise disallowed. Solves issue with diagonal movement where diagonal movement first step would use turf that was marked as impassable in pathfinder by splitting invalid diagonal move into valid orthogonal ones. In most cases this is pretty inconsequential but for openspace/other dangerous non-dense tiles it's pretty important. --- code/__DEFINES/turfs.dm | 7 ++++ code/__HELPERS/path.dm | 58 +++++++++++++++++++++++++++++-- code/game/atoms.dm | 18 ++++++++++ code/game/objects/objs.dm | 16 --------- code/game/turfs/open/openspace.dm | 6 ++++ code/game/turfs/turf.dm | 4 +++ 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm index 5988614eb5f..4fdd57f8457 100644 --- a/code/__DEFINES/turfs.dm +++ b/code/__DEFINES/turfs.dm @@ -64,3 +64,10 @@ * Use instead of `A.loc.loc`. */ #define get_area(A) (isarea(A) ? A : get_step(A, 0)?.loc) + +/// Turf will be passable if density is 0 +#define TURF_PATHING_PASS_DENSITY 0 +/// Turf will be passable depending on [CanAStarPass] return value +#define TURF_PATHING_PASS_PROC 1 +/// Turf is never passable +#define TURF_PATHING_PASS_NO 2 diff --git a/code/__HELPERS/path.dm b/code/__HELPERS/path.dm index 72ed8da8190..5d1816e7e53 100644 --- a/code/__HELPERS/path.dm +++ b/code/__HELPERS/path.dm @@ -17,8 +17,9 @@ * * simulated_only: Whether we consider turfs without atmos simulation (AKA do we want to ignore space) * * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf * * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures. + * * diagonal_safety: ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary */ -/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE) +/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE) if(!caller || !get_turf(end)) return @@ -28,7 +29,7 @@ l = SSpathfinder.mobs.getfree(caller) var/list/path - var/datum/pathfind/pathfind_datum = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude) + var/datum/pathfind/pathfind_datum = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude, diagonal_safety) path = pathfind_datum.search() qdel(pathfind_datum) @@ -43,6 +44,7 @@ * A helper macro to see if it's possible to step from the first turf into the second one, minding things like door access and directional windows. * Note that this can only be used inside the [datum/pathfind][pathfind datum] since it uses variables from said datum. * If you really want to optimize things, optimize this, cuz this gets called a lot. + * We do early next.density check despite it being already checked in LinkBlockedWithAccess for short-circuit performance */ #define CAN_STEP(cur_turf, next) (next && !next.density && !(simulated_only && SSpathfinder.space_type_cache[next.type]) && !cur_turf.LinkBlockedWithAccess(next,caller, id) && (next != avoid)) /// Another helper macro for JPS, for telling when a node has forced neighbors that need expanding @@ -120,8 +122,10 @@ var/simulated_only /// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through var/turf/avoid + /// Ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary + var/diagonal_safety = TRUE -/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid) +/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, diagonal_safety) src.caller = caller end = get_turf(goal) open = new /datum/heap(/proc/HeapPathWeightCompare) @@ -131,6 +135,7 @@ src.mintargetdist = mintargetdist src.simulated_only = simulated_only src.avoid = avoid + src.diagonal_safety = diagonal_safety /** * search() is the proc you call to kick off and handle the actual pathfinding, and kills the pathfind datum instance when it's done. @@ -177,6 +182,10 @@ sources = null qdel(open) + + if(diagonal_safety) + path = diagonal_movement_safety() + return path /// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search] @@ -192,6 +201,35 @@ path.Add(iter_turf) unwind_node = unwind_node.previous_node +/datum/pathfind/proc/diagonal_movement_safety() + if(length(path) < 2) + return + var/list/modified_path = list() + + for(var/i in 1 to length(path) - 1) + var/turf/current_turf = path[i] + var/turf/next_turf = path[i+1] + var/movement_dir = get_dir(current_turf, next_turf) + if(!(movement_dir & (movement_dir - 1))) //cardinal movement, no need to verify + modified_path += current_turf + continue + //If default diagonal movement step is invalid, replace with alternative two steps + if(movement_dir & NORTH) + if(!CAN_STEP(current_turf,get_step(current_turf,NORTH))) + modified_path += current_turf + modified_path += get_step(current_turf, movement_dir & ~NORTH) + else + modified_path += current_turf + else + if(!CAN_STEP(current_turf,get_step(current_turf,SOUTH))) + modified_path += current_turf + modified_path += get_step(current_turf, movement_dir & ~SOUTH) + else + modified_path += current_turf + modified_path += path[length(path)] + + return modified_path + /** * For performing lateral scans from a given starting turf. * @@ -332,6 +370,8 @@ /** * For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags * + * Assumes destinantion turf is non-dense - check and shortcircuit in code invoking this proc to avoid overhead. + * * Arguments: * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf @@ -352,6 +392,18 @@ var/actual_dir = get_dir(src, destination_turf) + /// These are generally cheaper than looping contents so they go first + switch(destination_turf.pathing_pass_method) + // This is already assumed to be true + //if(TURF_PATHING_PASS_DENSITY) + // if(destination_turf.density) + // return TRUE + if(TURF_PATHING_PASS_PROC) + if(!destination_turf.CanAStarPass(ID, actual_dir , caller)) + return TRUE + if(TURF_PATHING_PASS_NO) + return TRUE + // Source border object checks for(var/obj/structure/window/iter_window in src) if(!iter_window.CanAStarPass(ID, actual_dir)) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 6141a5c84d9..063d98d77b4 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -2192,3 +2192,21 @@ new /datum/merger(id, allowed_types, src) candidate = mergers[id] return candidate + +/** + * This proc is used for telling whether something can pass by this atom in a given direction, for use by the pathfinding system. + * + * Trying to generate one long path across the station will call this proc on every single object on every single tile that we're seeing if we can move through, likely + * multiple times per tile since we're likely checking if we can access said tile from multiple directions, so keep these as lightweight as possible. + * + * For turfs this will only be used if pathing_pass_method is TURF_PATHING_PASS_PROC + * + * Arguments: + * * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference + * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions + * * caller- The movable we're checking pass flags for, if we're making any such checks + **/ +/atom/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) + if(istype(caller) && (caller.pass_flags & pass_flags_self)) + return TRUE + . = !density diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 2537f5b5760..288ec91c7c3 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -209,22 +209,6 @@ /obj/get_dumping_location() return get_turf(src) -/** - * This proc is used for telling whether something can pass by this object in a given direction, for use by the pathfinding system. - * - * Trying to generate one long path across the station will call this proc on every single object on every single tile that we're seeing if we can move through, likely - * multiple times per tile since we're likely checking if we can access said tile from multiple directions, so keep these as lightweight as possible. - * - * Arguments: - * * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference - * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions - * * caller- The movable we're checking pass flags for, if we're making any such checks - **/ -/obj/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) - if(istype(caller) && (caller.pass_flags & pass_flags_self)) - return TRUE - . = !density - /obj/proc/check_uplink_validity() return 1 diff --git a/code/game/turfs/open/openspace.dm b/code/game/turfs/open/openspace.dm index 031820c9d70..f2d890b5c63 100644 --- a/code/game/turfs/open/openspace.dm +++ b/code/game/turfs/open/openspace.dm @@ -19,6 +19,7 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr overfloor_placed = FALSE underfloor_accessibility = UNDERFLOOR_INTERACTABLE mouse_opacity = MOUSE_OPACITY_TRANSPARENT + pathing_pass_method = TURF_PATHING_PASS_PROC var/can_cover_up = TRUE var/can_build_on = TRUE @@ -157,6 +158,11 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr /turf/open/openspace/rust_heretic_act() return FALSE +/turf/open/openspace/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) + if(caller && !caller.can_z_move(DOWN, src, null , ZMOVE_FALL_FLAGS)) //If we can't fall here (flying/lattice), it's fine to path through + return TRUE + return FALSE + /turf/open/openspace/icemoon name = "ice chasm" baseturfs = /turf/open/openspace/icemoon diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index bded667be31..36d5d42f270 100755 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -75,6 +75,10 @@ GLOBAL_LIST_EMPTY(station_turfs) /// See __DEFINES/construction.dm for RCD_MEMORY_*. var/rcd_memory + /// How pathing algorithm will check if this turf is passable by itself (not including content checks). By default it's just density check. + /// WARNING: Currently to use a density shortcircuiting this does not support dense turfs with special allow through function + var/pathing_pass_method = TURF_PATHING_PASS_DENSITY + /turf/vv_edit_var(var_name, new_value) var/static/list/banned_edits = list(NAMEOF(src, x), NAMEOF(src, y), NAMEOF(src, z)) if(var_name in banned_edits)