mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-05 22:43:46 +00:00
* Adds pathmaps, refactors pathfinding a bit (#78684) ## About The Pull Request Implements /datum/pathfind/sssp, which generates /datum/path_map /datum/path_maps allow us to very efficently generate paths to any turf they contain from their central point. We're effectively running the single source shortest paths algorithm. We expand from the center turf, adding turfs as they're found, and then processing them in order of addition. As we go, we remember what turf "found" us first. Reversing this chain gives us the shortest possible path from the center turf to any turf in its range (or the inverse). This isn't all that useful on its own, outside of a few niche cases (Like if we wanted to get the farthest reachable turf from the center) but if we could reuse the map more then once, we'd be able to swarm to/from a point very easily. Reuse is a bit troublesome, reqiures a timeout system and a way to compare different movables trying to get paths. I've implemented it tho. I've refactored CanAStarPass to take a datum, /datum/can_pass_info. This is built from a movable and a list of access, and copies all the properties that would impact pathfinding over onto itself. There is one case where we don't do this, pathing over openspace requires checking if we'd fall through the openspace, and the proc for that takes an atom. So instead we use the weakref to the owner that we hold onto, and hold copies of all the values that would impact the check on the datum. When someone requests a swarmed path their pass info is compared with the pass info of all other path_maps centered on their target turf. If it matches and their requested timeout isn't too short, we just reuse the map. Timeout is a tricky thing because the longer a map exists the more out of date it gets. I've added a few age defines that let you modulate your level of risk here. We default to only allowing maps that are currently being generated, or finished generating in our tick. Hopefully this prevents falling into trouble, but consumers will need to allow "failed" movements. As a part of this datumized pass info, I've refactored pathfinding to use access lists, rather then id cards directly. This also avoids some dumbass harddel oppertunities, and prevents an idcard from changing mid path. Did a few things to the zPass procs, they took args that they did NOT need, and I thought it'd be better to yeet em. If you'd all like I could undo the caching/can_pass_info stuff if you'd all like. I think it's useful generally because it avoids stuff changing mid pathfind attempt, but if it's too clunky I could nuke it. Oh also I added optional args to jps that constricts how it handles diagonals. I've used this to fix bot paths. ## Why It's Good For The Game Much of this is redundant currently. I'm adding it because it could have saved hugglebippers, and because I get the feeling it'll be useful for "grouping" mobs like bees and such. We're doing more basic mob work currently and I want to provide extra tools for that work. https://github.com/tgstation/tgstation/assets/58055496/66aca1f9-c6e7-4173-9c38-c40516d6d853 ## Changelog 🆑 add: Adds swarmed pathfinding, trading accuracy for potential optimization of used correctly fix: Bots will no longer take diagonal paths, preventing weirdo looking path visuals refactor: Refactored bits of pathfinding code, hopefully easier to add new pathfinding strategies now /🆑 * Adds pathmaps, refactors pathfinding a bit --------- Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
208 lines
9.7 KiB
Plaintext
208 lines
9.7 KiB
Plaintext
/// Queues and manages JPS pathfinding steps
|
|
SUBSYSTEM_DEF(pathfinder)
|
|
name = "Pathfinder"
|
|
init_order = INIT_ORDER_PATH
|
|
priority = FIRE_PRIORITY_PATHFINDING
|
|
wait = 0.5
|
|
/// List of pathfind datums we are currently trying to process
|
|
var/list/datum/pathfind/active_pathing = list()
|
|
/// List of pathfind datums being ACTIVELY processed. exists to make subsystem stats readable
|
|
var/list/datum/pathfind/currentrun = list()
|
|
/// List of uncheccked source_to_map entries
|
|
var/list/currentmaps = list()
|
|
/// Assoc list of target turf -> list(/datum/path_map) centered on the turf
|
|
var/list/source_to_maps = list()
|
|
var/static/space_type_cache
|
|
|
|
/datum/controller/subsystem/pathfinder/Initialize()
|
|
space_type_cache = typecacheof(/turf/open/space)
|
|
return SS_INIT_SUCCESS
|
|
|
|
/datum/controller/subsystem/pathfinder/stat_entry(msg)
|
|
msg = "P:[length(active_pathing)]"
|
|
return ..()
|
|
|
|
// This is another one of those subsystems (hey lighting) in which one "Run" means fully processing a queue
|
|
// We'll use a copy for this just to be nice to people reading the mc panel
|
|
/datum/controller/subsystem/pathfinder/fire(resumed)
|
|
if(!resumed)
|
|
src.currentrun = active_pathing.Copy()
|
|
src.currentmaps = deep_copy_list(source_to_maps)
|
|
|
|
// Dies of sonic speed from caching datum var reads
|
|
var/list/currentrun = src.currentrun
|
|
while(length(currentrun))
|
|
var/datum/pathfind/path = currentrun[length(currentrun)]
|
|
if(!path.search_step()) // Something's wrong
|
|
path.early_exit()
|
|
currentrun.len--
|
|
continue
|
|
if(MC_TICK_CHECK)
|
|
return
|
|
path.finished()
|
|
// Next please
|
|
currentrun.len--
|
|
|
|
// Go over our existing pathmaps, clear out the ones we aren't using
|
|
var/list/currentmaps = src.currentmaps
|
|
var/oldest_time = world.time - MAP_REUSE_SLOWEST
|
|
while(length(currentmaps))
|
|
var/turf/source = currentmaps[length(currentmaps)]
|
|
var/list/datum/path_map/owned_maps = currentmaps[source]
|
|
for(var/datum/path_map/map as anything in owned_maps)
|
|
if(map.creation_time < oldest_time && !map.building)
|
|
source_to_maps[source] -= map
|
|
owned_maps.len--
|
|
if(MC_TICK_CHECK)
|
|
return
|
|
if(!length(source_to_maps[source]))
|
|
source_to_maps -= source
|
|
|
|
currentmaps.len--
|
|
|
|
/// Initiates a pathfind. Returns true if we're good, FALSE if something's failed
|
|
/datum/controller/subsystem/pathfinder/proc/pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, access = list(), simulated_only = TRUE, turf/exclude, skip_first = TRUE, diagonal_handling = DIAGONAL_REMOVE_CLUNKY, list/datum/callback/on_finish)
|
|
var/datum/pathfind/jps/path = new()
|
|
path.setup(caller, access, max_distance, simulated_only, exclude, on_finish, end, mintargetdist, skip_first, diagonal_handling)
|
|
if(path.start())
|
|
active_pathing += path
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/// Initiates a swarmed pathfind. Returns TRUE if we're good, FALSE if something's failed
|
|
/// If a valid pathmap exists for the TARGET turf we'll use that, otherwise we have to build a new one
|
|
/datum/controller/subsystem/pathfinder/proc/swarmed_pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist = 0, age = MAP_REUSE_INSTANT, access = list(), simulated_only = TRUE, turf/exclude, skip_first = TRUE, list/datum/callback/on_finish)
|
|
var/turf/target = get_turf(end)
|
|
var/datum/can_pass_info/pass_info = new(caller, access)
|
|
// If there's a map we can use already, use it
|
|
var/datum/path_map/valid_map = get_valid_map(pass_info, target, simulated_only, exclude, age, include_building = TRUE)
|
|
if(valid_map && valid_map.expand(max_distance))
|
|
path_map_passalong(on_finish, get_turf(caller), mintargetdist, skip_first, valid_map)
|
|
return TRUE
|
|
|
|
// Otherwise we're gonna make a new one, and turn it into a path for the callbacks passed into us
|
|
var/list/datum/callback/pass_in = list()
|
|
pass_in += CALLBACK(GLOBAL_PROC, /proc/path_map_passalong, on_finish, get_turf(caller), mintargetdist, skip_first)
|
|
// And to allow subsequent calls to reuse the same map, we'll put a placeholder in the cache, and fill it up when the pathing finishes
|
|
var/datum/path_map/empty = new()
|
|
empty.pass_info = new(caller, access)
|
|
empty.start = target
|
|
empty.pass_space = simulated_only
|
|
empty.avoid = exclude
|
|
empty.building = TRUE
|
|
path_map_cache(target, empty)
|
|
pass_in += CALLBACK(src, PROC_REF(path_map_fill), target, empty)
|
|
if(!SSpathfinder.can_pass_build_map(pass_info, target, max_distance, simulated_only, exclude, pass_in))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// We generate a path for the passed in callbacks, and then pipe it over
|
|
/proc/path_map_passalong(list/datum/callback/return_callbacks, turf/target, mintargetdist = 0, skip_first = TRUE, datum/path_map/hand_back)
|
|
var/list/requested_path
|
|
if(istype(hand_back, /datum/path_map))
|
|
requested_path = hand_back.get_path_from(target, skip_first, mintargetdist)
|
|
for(var/datum/callback/return_callback as anything in return_callbacks)
|
|
return_callback.Invoke(requested_path)
|
|
|
|
/// Caches the passed in path_map, allowing for reuse in future
|
|
/datum/controller/subsystem/pathfinder/proc/path_map_cache(turf/target, datum/path_map/hand_back)
|
|
// Cache our path_map
|
|
if(!target || !hand_back)
|
|
return
|
|
source_to_maps[target] += list(hand_back)
|
|
|
|
/datum/controller/subsystem/pathfinder/proc/path_map_fill(turf/target, datum/path_map/fill_into, datum/path_map/hand_back)
|
|
fill_into.building = FALSE
|
|
if(!fill_into.compare_against(hand_back))
|
|
source_to_maps[target] -= fill_into
|
|
return
|
|
fill_into.copy_from(hand_back)
|
|
fill_into.creation_time = hand_back.creation_time
|
|
// If we aren't in the source list anymore don't go trying to clear it out yeah?
|
|
if(!source_to_maps[target] || !(fill_into in source_to_maps[target]))
|
|
return
|
|
// Let's remove anything we're better than
|
|
for(var/datum/path_map/same_target as anything in source_to_maps[target])
|
|
if(fill_into == same_target || !same_target.compare_against(hand_back))
|
|
continue
|
|
// If it's still being made it'll be fresher then us
|
|
if(same_target.building)
|
|
continue
|
|
// We assume that we are fresher, and that's all we care about
|
|
// If it's being expanded it'll get updated when that finishes, then clear when all the refs drop
|
|
source_to_maps[target] -= same_target
|
|
|
|
/// Initiates a SSSP run. Returns true if we're good, FALSE if something's failed
|
|
/datum/controller/subsystem/pathfinder/proc/build_map(atom/movable/caller, turf/source, max_distance = 30, access = list(), simulated_only = TRUE, turf/exclude, list/datum/callback/on_finish)
|
|
var/datum/pathfind/sssp/path = new()
|
|
path.setup(caller, access, source, max_distance, simulated_only, exclude, on_finish)
|
|
if(path.start())
|
|
active_pathing += path
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/// Initiates a SSSP run from a pass_info datum. Returns true if we're good, FALSE if something's failed
|
|
/datum/controller/subsystem/pathfinder/proc/can_pass_build_map(datum/can_pass_info/pass_info, turf/source, max_distance = 30, simulated_only = TRUE, turf/exclude, list/datum/callback/on_finish)
|
|
var/datum/pathfind/sssp/path = new()
|
|
path.setup_from_canpass(pass_info, source, max_distance, simulated_only, exclude, on_finish)
|
|
if(path.start())
|
|
active_pathing += path
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/// Begins to handle a pathfinding run based off the input /datum/pathfind datum
|
|
/// You should not use this, it exists to allow for shenanigans. You do not know how to do shenanigans
|
|
/datum/controller/subsystem/pathfinder/proc/run_pathfind(datum/pathfind/run)
|
|
active_pathing += run
|
|
return TRUE
|
|
|
|
/// Takes a set of pathfind info, returns the first valid pathmap that would work if one exists
|
|
/// Optionally takes a max age to accept (defaults to 0 seconds) and a minimum acceptable range
|
|
/// If include_building is true and we can only find a building path, ew'll use that instead. tho we will wait for it to finish first
|
|
/datum/controller/subsystem/pathfinder/proc/get_valid_map(datum/can_pass_info/pass_info, turf/target, simulated_only = TRUE, turf/exclude, age = MAP_REUSE_INSTANT, min_range = -INFINITY, include_building = FALSE)
|
|
// Walk all the maps that match our caller's turf OR our target's
|
|
// Then hold onto em. If their cache time is short we can reuse/expand them, if not we'll have to make a new one
|
|
var/oldest_time = world.time - age
|
|
/// Backup return value used if no finished pathmaps are found
|
|
var/datum/path_map/constructing
|
|
for(var/datum/path_map/shared_source as anything in source_to_maps[target])
|
|
if(!shared_source.compare_against_args(pass_info, target, simulated_only, exclude))
|
|
continue
|
|
var/max_dist = 0
|
|
if(shared_source.distances.len)
|
|
max_dist = shared_source.distances[shared_source.distances.len]
|
|
if(max_dist < min_range)
|
|
continue
|
|
if(oldest_time > shared_source.creation_time && !shared_source.building)
|
|
continue
|
|
if(shared_source.building)
|
|
if(include_building)
|
|
constructing = constructing || shared_source
|
|
continue
|
|
|
|
return shared_source
|
|
if(constructing)
|
|
UNTIL(constructing.building == FALSE)
|
|
return constructing
|
|
return null
|
|
|
|
/// Takes a set of pathfind info, returns all valid pathmaps that would work
|
|
/// Takes an optional minimum range arg
|
|
/datum/controller/subsystem/pathfinder/proc/get_valid_maps(datum/can_pass_info/pass_info, turf/target, simulated_only = TRUE, turf/exclude, age = MAP_REUSE_INSTANT, min_range = -INFINITY, include_building = FALSE)
|
|
// Walk all the maps that match our caller's turf OR our target's
|
|
// Then hold onto em. If their cache time is short we can reuse/expand them, if not we'll have to make a new one
|
|
var/list/valid_maps = list()
|
|
var/oldest_time = world.time - age
|
|
for(var/datum/path_map/shared_source as anything in source_to_maps[target])
|
|
if(shared_source.compare_against_args(pass_info, target, simulated_only, exclude))
|
|
continue
|
|
var/max_dist = shared_source.distances[shared_source.distances.len]
|
|
if(max_dist < min_range)
|
|
continue
|
|
if(oldest_time > shared_source.creation_time)
|
|
continue
|
|
if(!include_building && shared_source.building)
|
|
continue
|
|
valid_maps += shared_source
|
|
return valid_maps
|