mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 18:22:14 +00:00
Renames all uses of caller, as they (currently) shadow the new byond var and will in future error Ups our "wan if compiled after" experiement compile version to 516 Adds an alternate 516 unit test
380 lines
17 KiB
Plaintext
380 lines
17 KiB
Plaintext
/**
|
|
* This is the proc you use whenever you want to have pathfinding more complex than "try stepping towards the thing".
|
|
* If no path was found, returns an empty list, which is important for bots like medibots who expect an empty list rather than nothing.
|
|
* It will yield until a path is returned, using magic
|
|
*
|
|
* Arguments:
|
|
* * requester: The movable atom that's trying to find the path
|
|
* * end: What we're trying to path to. It doesn't matter if this is a turf or some other atom, we're gonna just path to the turf it's on anyway
|
|
* * max_distance: The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
|
|
* * mintargetdistance: Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example.
|
|
* * access: A list representing what access we have and what doors we can open.
|
|
* * simulated_only: Whether we consider tur fs without atmos simulation (AKA do we want to ignore space)
|
|
* * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
|
|
* * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures.
|
|
* * diagonal_handling: defines how we handle diagonal moves. see __DEFINES/path.dm
|
|
*/
|
|
/proc/get_path_to(atom/movable/requester, atom/end, max_distance = 30, mintargetdist, access=list(), simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_handling=DIAGONAL_REMOVE_CLUNKY)
|
|
var/list/hand_around = list()
|
|
// We're guaranteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
|
|
var/datum/callback/await = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around))
|
|
if(!SSpathfinder.pathfind(requester, end, max_distance, mintargetdist, access, simulated_only, exclude, skip_first, diagonal_handling, await))
|
|
return list()
|
|
|
|
UNTIL(length(hand_around))
|
|
var/list/return_val = hand_around[1]
|
|
if(!islist(return_val) || (QDELETED(requester) || QDELETED(end))) // It's trash, just hand back empty to make it easy
|
|
return list()
|
|
return return_val
|
|
|
|
/**
|
|
* POTENTIALLY cheaper version of get_path_to
|
|
* This proc generates a path map for the end atom's turf, which allows us to cheaply do pathing operations "at" it
|
|
* Generation is significantly SLOWER then get_path_to, but if many things are/might be pathing at something then it is much faster
|
|
* Runs the risk of returning an suboptimal or INVALID PATH if the delay between map creation and use is too long
|
|
*
|
|
* If no path was found, returns an empty list, which is important for bots like medibots who expect an empty list rather than nothing.
|
|
* It will yield until a path is returned, using magic
|
|
*
|
|
* Arguments:
|
|
* * requester: The movable atom that's trying to find the path
|
|
* * end: What we're trying to path to. It doesn't matter if this is a turf or some other atom, we're gonna just path to the turf it's on anyway
|
|
* * max_distance: The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
|
|
* * mintargetdistance: Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example.
|
|
* * age: How old a path map can be before we'll avoid reusing it. Use the defines found in [code/__DEFINES/path.dm], values larger then MAP_REUSE_SLOWEST will be discarded
|
|
* * access: A list representing what access we have and what doors we can open.
|
|
* * simulated_only: Whether we consider tur fs without atmos simulation (AKA do we want to ignore space)
|
|
* * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
|
|
* * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures.
|
|
*/
|
|
/proc/get_swarm_path_to(atom/movable/requester, atom/end, max_distance = 30, mintargetdist, age = MAP_REUSE_INSTANT, access = list(), simulated_only = TRUE, turf/exclude, skip_first=TRUE)
|
|
var/list/hand_around = list()
|
|
// We're guaranteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
|
|
var/datum/callback/await = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around))
|
|
if(!SSpathfinder.swarmed_pathfind(requester, end, max_distance, mintargetdist, age, access, simulated_only, exclude, skip_first, await))
|
|
return list()
|
|
|
|
UNTIL(length(hand_around))
|
|
var/list/return_val = hand_around[1]
|
|
if(!islist(return_val) || (QDELETED(requester) || QDELETED(end))) // It's trash, just hand back empty to make it easy
|
|
return list()
|
|
return return_val
|
|
|
|
/proc/get_sssp(atom/movable/requester, max_distance = 30, access = list(), simulated_only = TRUE, turf/exclude)
|
|
var/list/hand_around = list()
|
|
// We're guaranteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
|
|
var/datum/callback/await = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around))
|
|
if(!SSpathfinder.build_map(requester, get_turf(requester), max_distance, access, simulated_only, exclude, await))
|
|
return null
|
|
|
|
UNTIL(length(hand_around))
|
|
var/datum/path_map/return_val = hand_around[1]
|
|
if(!istype(return_val, /datum/path_map) || (QDELETED(requester))) // It's trash, just hand back null to make it easy
|
|
return null
|
|
return return_val
|
|
|
|
/// Uses funny pass by reference bullshit to take the output created by pathfinding, and insert it into a return list
|
|
/// We'll be able to use this return list to tell a sleeping proc to continue execution
|
|
/proc/pathfinding_finished(list/return_list, hand_back)
|
|
// We use += here to behave nicely with lists
|
|
return_list += LIST_VALUE_WRAP_LISTS(hand_back)
|
|
|
|
/// The datum used to handle the JPS pathfinding, completely self-contained
|
|
/datum/pathfind
|
|
/// The turf we started at
|
|
var/turf/start
|
|
|
|
// general pathfinding vars/args
|
|
/// Limits how far we can search before giving up on a path
|
|
var/max_distance = 30
|
|
/// Space is big and empty, if this is TRUE then we ignore pathing through unsimulated tiles
|
|
var/simulated_only
|
|
/// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through
|
|
var/turf/avoid
|
|
/// The callbacks to invoke when we're done working, passing in the completed product
|
|
/// Invoked in order
|
|
var/list/datum/callback/on_finish
|
|
/// Datum that holds the canpass info of this pathing attempt. This is what CanAstarPass sees
|
|
var/datum/can_pass_info/pass_info
|
|
|
|
/datum/pathfind/Destroy(force)
|
|
. = ..()
|
|
SSpathfinder.active_pathing -= src
|
|
SSpathfinder.currentrun -= src
|
|
hand_back(null)
|
|
avoid = null
|
|
|
|
/**
|
|
* "starts" off the pathfinding, by storing the values this datum will need to work later on
|
|
* returns FALSE if it fails to setup properly, TRUE otherwise
|
|
*/
|
|
/datum/pathfind/proc/start()
|
|
if(!start)
|
|
stack_trace("Invalid pathfinding start")
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/**
|
|
* search_step() is the workhorse of pathfinding. It'll do the searching logic, and will slowly build up a path
|
|
* returns TRUE if everything is stable, FALSE if the pathfinding logic has failed, and we need to abort
|
|
*/
|
|
/datum/pathfind/proc/search_step()
|
|
return TRUE
|
|
|
|
/**
|
|
* early_exit() is called when something goes wrong in processing, and we need to halt the pathfinding NOW
|
|
*/
|
|
/datum/pathfind/proc/early_exit()
|
|
hand_back(null)
|
|
qdel(src)
|
|
|
|
/**
|
|
* Cleanup pass for the pathfinder. This tidies up the path, and fufills the pathfind's obligations
|
|
*/
|
|
/datum/pathfind/proc/finished()
|
|
qdel(src)
|
|
|
|
/**
|
|
* Call to return a value to whoever spawned this pathfinding work
|
|
* Will fail if it's already been called
|
|
*/
|
|
/datum/pathfind/proc/hand_back(value)
|
|
for(var/datum/callback/finished as anything in on_finish)
|
|
finished.Invoke(value)
|
|
on_finish = null
|
|
|
|
/**
|
|
* Processes a path (list of turfs), removes any diagonal moves that would lead to a weird bump
|
|
*
|
|
* path - The path to process down
|
|
* pass_info - Holds all the info about what this path attempt can go through
|
|
* simulated_only - If we are not allowed to pass space turfs
|
|
* avoid - A turf to be avoided
|
|
*/
|
|
/proc/remove_clunky_diagonals(list/path, datum/can_pass_info/pass_info, simulated_only, turf/avoid)
|
|
if(length(path) < 2)
|
|
return path
|
|
var/list/modified_path = list()
|
|
|
|
for(var/i in 1 to length(path) - 1)
|
|
var/turf/current_turf = path[i]
|
|
modified_path += current_turf
|
|
var/turf/next_turf = path[i+1]
|
|
var/movement_dir = get_dir(current_turf, next_turf)
|
|
if(!(movement_dir & (movement_dir - 1))) //cardinal movement, no need to verify
|
|
continue
|
|
//If the first diagonal movement step is invalid (north/south), replace with a sidestep first, with an implied vertical step in next_turf
|
|
var/vertical_only = movement_dir & (NORTH|SOUTH)
|
|
if(!CAN_STEP(current_turf,get_step(current_turf, vertical_only), simulated_only, pass_info, avoid))
|
|
modified_path += get_step(current_turf, movement_dir & ~vertical_only)
|
|
modified_path += path[length(path)]
|
|
|
|
return modified_path
|
|
|
|
/**
|
|
* Processes a path (list of turfs), removes any diagonal moves
|
|
*
|
|
* path - The path to process down
|
|
* pass_info - Holds all the info about what this path attempt can go through
|
|
* simulated_only - If we are not allowed to pass space turfs
|
|
* avoid - A turf to be avoided
|
|
*/
|
|
/proc/remove_diagonals(list/path, datum/can_pass_info/pass_info, simulated_only, turf/avoid)
|
|
if(length(path) < 2)
|
|
return path
|
|
var/list/modified_path = list()
|
|
|
|
for(var/i in 1 to length(path) - 1)
|
|
var/turf/current_turf = path[i]
|
|
modified_path += current_turf
|
|
var/turf/next_turf = path[i+1]
|
|
var/movement_dir = get_dir(current_turf, next_turf)
|
|
if(!(movement_dir & (movement_dir - 1))) //cardinal movement, no need to verify
|
|
continue
|
|
var/vertical_only = movement_dir & (NORTH|SOUTH)
|
|
// If we can't go directly north/south, we will first go to the side,
|
|
if(!CAN_STEP(current_turf,get_step(current_turf, vertical_only), simulated_only, pass_info, avoid))
|
|
modified_path += get_step(current_turf, movement_dir & ~vertical_only)
|
|
else // Otherwise, we'll first go north/south, then to the side
|
|
modified_path += get_step(current_turf, vertical_only)
|
|
modified_path += path[length(path)]
|
|
|
|
return modified_path
|
|
|
|
/**
|
|
* For seeing if we can actually move between 2 given turfs while accounting for our access and the requester's pass_flags
|
|
*
|
|
* Assumes destinantion turf is non-dense - check and shortcircuit in code invoking this proc to avoid overhead.
|
|
* Makes some other assumptions, such as assuming that unless declared, non dense objects will not block movement.
|
|
* It's fragile, but this is VERY much the most expensive part of pathing, so it'd better be fast
|
|
*
|
|
* Arguments:
|
|
* * destination_turf - Where are we going from where we are?
|
|
* * pass_info - Holds all the info about what this path attempt can go through
|
|
*/
|
|
/turf/proc/LinkBlockedWithAccess(turf/destination_turf, datum/can_pass_info/pass_info)
|
|
if(destination_turf.x != x && destination_turf.y != y) //diagonal
|
|
var/in_dir = get_dir(destination_turf,src) // eg. northwest (1+8) = 9 (00001001)
|
|
var/first_step_direction_a = in_dir & 3 // eg. north (1+8)&3 (0000 0011) = 1 (0000 0001)
|
|
var/first_step_direction_b = in_dir & 12 // eg. west (1+8)&12 (0000 1100) = 8 (0000 1000)
|
|
|
|
for(var/first_step_direction in list(first_step_direction_a,first_step_direction_b))
|
|
var/turf/midstep_turf = get_step(destination_turf,first_step_direction)
|
|
var/way_blocked = midstep_turf.density || LinkBlockedWithAccess(midstep_turf, pass_info) || midstep_turf.LinkBlockedWithAccess(destination_turf, pass_info)
|
|
if(!way_blocked)
|
|
return FALSE
|
|
return TRUE
|
|
var/actual_dir = get_dir(src, destination_turf)
|
|
|
|
/// These are generally cheaper than looping contents so they go first
|
|
switch(destination_turf.pathing_pass_method)
|
|
// This is already assumed to be true
|
|
//if(TURF_PATHING_PASS_DENSITY)
|
|
// if(destination_turf.density)
|
|
// return TRUE
|
|
if(TURF_PATHING_PASS_PROC)
|
|
if(!destination_turf.CanAStarPass(actual_dir, pass_info))
|
|
return TRUE
|
|
if(TURF_PATHING_PASS_NO)
|
|
return TRUE
|
|
|
|
var/static/list/directional_blocker_cache = typecacheof(list(/obj/structure/window, /obj/machinery/door/window, /obj/structure/railing, /obj/machinery/door/firedoor/border_only))
|
|
// Source border object checks
|
|
for(var/obj/border in src)
|
|
if(!directional_blocker_cache[border.type])
|
|
continue
|
|
if(!border.density && border.can_astar_pass == CANASTARPASS_DENSITY)
|
|
continue
|
|
if(!border.CanAStarPass(actual_dir, pass_info))
|
|
return TRUE
|
|
|
|
// Destination blockers check
|
|
var/reverse_dir = get_dir(destination_turf, src)
|
|
for(var/obj/iter_object in destination_turf)
|
|
// This is an optimization because of the massive call count of this code
|
|
if(!iter_object.density && iter_object.can_astar_pass == CANASTARPASS_DENSITY)
|
|
continue
|
|
if(!iter_object.CanAStarPass(reverse_dir, pass_info))
|
|
return TRUE
|
|
return FALSE
|
|
|
|
// Could easily be a struct if/when we get that
|
|
/**
|
|
* Holds all information about what an atom can move through
|
|
* Passed into CanAStarPass to provide context for a pathing attempt
|
|
*
|
|
* Also used to check if using a cached path_map is safe
|
|
* There are some vars here that are unused. They exist to cover cases where requester_ref is used
|
|
* They're the properties of requester_ref used in those cases.
|
|
* It's kinda annoying, but there's some proc chains we can't convert to this datum
|
|
*/
|
|
/datum/can_pass_info
|
|
/// If we have no id, public airlocks are walls
|
|
var/no_id = FALSE
|
|
|
|
/// What we can pass through. Mirrors /atom/movable/pass_flags
|
|
var/pass_flags = NONE
|
|
/// What access we have, airlocks, windoors, etc
|
|
var/list/access = null
|
|
/// What sort of movement do we have. Mirrors /atom/movable/movement_type
|
|
var/movement_type = NONE
|
|
/// Are we being thrown?
|
|
var/thrown = FALSE
|
|
/// Are we anchored
|
|
var/anchored = FALSE
|
|
|
|
/// Are we a ghost? (they have effectively unique pathfinding)
|
|
var/is_observer = FALSE
|
|
/// Are we a living mob?
|
|
var/is_living = FALSE
|
|
/// Are we a bot?
|
|
var/is_bot = FALSE
|
|
/// Can we ventcrawl?
|
|
var/can_ventcrawl = FALSE
|
|
/// What is the size of our mob
|
|
var/mob_size = null
|
|
/// Is our mob incapacitated
|
|
var/incapacitated = FALSE
|
|
/// Is our mob incorporeal
|
|
var/incorporeal_move = FALSE
|
|
/// If our mob has a rider, what does it look like
|
|
var/datum/can_pass_info/rider_info = null
|
|
/// If our mob is buckled to something, what's it like
|
|
var/datum/can_pass_info/buckled_info = null
|
|
|
|
/// Do we have gravity
|
|
var/has_gravity = TRUE
|
|
/// Pass information for the object we are pulling, if any
|
|
var/datum/can_pass_info/pulling_info = null
|
|
|
|
/// Cameras have a lot of BS can_z_move overrides
|
|
/// Let's avoid this
|
|
var/camera_type
|
|
|
|
/// Weakref to the requester used to generate this info
|
|
/// Should not use this almost ever, it's for context and to allow for proc chains that
|
|
/// Require a movable
|
|
var/datum/weakref/requester_ref = null
|
|
|
|
/datum/can_pass_info/New(atom/movable/construct_from, list/access, no_id = FALSE, call_depth = 0)
|
|
// No infiniloops
|
|
if(call_depth > 10)
|
|
return
|
|
if(access)
|
|
src.access = access.Copy()
|
|
src.no_id = no_id
|
|
|
|
if(isnull(construct_from))
|
|
return
|
|
|
|
src.requester_ref = WEAKREF(construct_from)
|
|
src.pass_flags = construct_from.pass_flags
|
|
src.movement_type = construct_from.movement_type
|
|
src.thrown = !!construct_from.throwing
|
|
src.anchored = construct_from.anchored
|
|
src.has_gravity = construct_from.has_gravity()
|
|
if(ismob(construct_from))
|
|
var/mob/living/mob_construct = construct_from
|
|
src.incapacitated = mob_construct.incapacitated
|
|
if(mob_construct.buckled)
|
|
src.buckled_info = new(mob_construct.buckled, access, no_id, call_depth + 1)
|
|
if(isobserver(construct_from))
|
|
src.is_observer = TRUE
|
|
if(isliving(construct_from))
|
|
var/mob/living/living_construct = construct_from
|
|
src.is_living = TRUE
|
|
src.can_ventcrawl = HAS_TRAIT(living_construct, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(living_construct, TRAIT_VENTCRAWLER_NUDE)
|
|
src.mob_size = living_construct.mob_size
|
|
src.incorporeal_move = living_construct.incorporeal_move
|
|
if(iseyemob(construct_from))
|
|
src.camera_type = construct_from.type
|
|
src.is_bot = isbot(construct_from)
|
|
|
|
if(construct_from.pulling)
|
|
src.pulling_info = new(construct_from.pulling, access, no_id, call_depth + 1)
|
|
|
|
/// List of vars on /datum/can_pass_info to use when checking two instances for equality
|
|
GLOBAL_LIST_INIT(can_pass_info_vars, GLOBAL_PROC_REF(can_pass_check_vars))
|
|
|
|
/proc/can_pass_check_vars()
|
|
var/datum/can_pass_info/lamb = new()
|
|
var/datum/isaac = new()
|
|
var/list/altar = assoc_to_keys(lamb.vars - isaac.vars)
|
|
// Don't compare against calling atom, it's not relevant here
|
|
altar -= "requester_ref"
|
|
ASSERT("requester_ref" in lamb.vars, "requester_ref var was not found in /datum/can_pass_info, why are we filtering for it?")
|
|
// We will bespoke handle pulling_info
|
|
altar -= "pulling_info"
|
|
ASSERT("pulling_info" in lamb.vars, "pulling_info var was not found in /datum/can_pass_info, why are we filtering for it?")
|
|
return altar
|
|
|
|
/datum/can_pass_info/proc/compare_against(datum/can_pass_info/check_against)
|
|
for(var/comparable_var in GLOB.can_pass_info_vars)
|
|
if(!(vars[comparable_var] ~= check_against.vars[comparable_var]))
|
|
return FALSE
|
|
if(!pulling_info != !check_against.pulling_info)
|
|
return FALSE
|
|
if(pulling_info && !pulling_info.compare_against(check_against.pulling_info))
|
|
return FALSE
|
|
return TRUE
|