Skip to content

Commit

Permalink
Modsuit Pathfinder module fix & JPS improvement (#885)
Browse files Browse the repository at this point in the history
* jps fixes

* recall module feedback

* improper

* improve visual

* jps uses access lists not ids
  • Loading branch information
Kapu1178 authored Mar 17, 2024
1 parent c921149 commit f25b24b
Show file tree
Hide file tree
Showing 33 changed files with 158 additions and 106 deletions.
2 changes: 1 addition & 1 deletion code/__DEFINES/ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
///The implant the AI was created from
#define BB_MOD_IMPLANT "BB_mod_implant"
///Range for a MOD AI controller.
#define MOD_AI_RANGE 100
#define MOD_AI_RANGE 200

///Vending machine AI controller blackboard keys
#define BB_VENDING_CURRENT_TARGET "BB_vending_current_target"
Expand Down
28 changes: 14 additions & 14 deletions code/__HELPERS/path.dm
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
* * end: What we're trying to path to. It doesn't matter if this is a turf or some other atom, we're gonna just path to the turf it's on anyway
* * max_distance: The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
* * mintargetdistance: Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example.
* * id: An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
* * access: A list representing what access we have and what doors we can open.
* * simulated_only: Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
* * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
* * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures.
* * diagonal_safety: ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary
*/
/proc/get_path_to(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE)
/proc/get_path_to(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, list/access, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE)
var/list/path = list()
// We're guarenteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list
var/datum/callback/await = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), path)
if(!SSpathfinder.pathfind(caller, end, max_distance, mintargetdist, id, simulated_only, exclude, skip_first, diagonal_safety, await))
if(!SSpathfinder.pathfind(caller, end, max_distance, mintargetdist, access, simulated_only, exclude, skip_first, diagonal_safety, await))
return list()

UNTIL(length(path))
Expand All @@ -44,7 +44,7 @@
* 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 && isspaceturf(next)) && !cur_turf.LinkBlockedWithAccess(next,caller, id) && (next != avoid))
#define CAN_STEP(cur_turf, next) (next && !next.density && !(simulated_only && isspaceturf(next)) && !cur_turf.LinkBlockedWithAccess(next, caller, access) && (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))))

Expand Down Expand Up @@ -110,8 +110,8 @@
var/list/path

// general pathfinding vars/args
/// An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
var/obj/item/card/id/id
/// A list representing what access we have and what doors we can open.
var/list/access
/// 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
Expand All @@ -127,12 +127,12 @@
/// The callback to invoke when we're done working, passing in the completed var/list/path
var/datum/callback/on_finish

/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, skip_first, diagonal_safety, datum/callback/on_finish)
/datum/pathfind/New(atom/movable/caller, atom/goal, access, max_distance, mintargetdist, simulated_only, avoid, skip_first, diagonal_safety, datum/callback/on_finish)
src.caller = caller
end = get_turf(goal)
open = new /datum/heap(GLOBAL_PROC_REF(HeapPathWeightCompare))
sources = new()
src.id = id
src.access = access
src.max_distance = max_distance
src.mintargetdist = mintargetdist
src.simulated_only = simulated_only
Expand Down Expand Up @@ -407,19 +407,19 @@
*
* Arguments:
* * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach
* * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf
* * access: A list that decides if we can gain access to doors that would otherwise block a turf
* * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space?
* * no_id: When true, doors with public access will count as impassible
*/
/turf/proc/LinkBlockedWithAccess(turf/destination_turf, atom/movable/caller, ID, no_id = FALSE)
/turf/proc/LinkBlockedWithAccess(turf/destination_turf, atom/movable/caller, list/access, no_id = FALSE)
if(destination_turf.x != x && destination_turf.y != y) //diagonal
var/in_dir = get_dir(destination_turf,src) // eg. northwest (1+8) = 9 (00001001)
var/first_step_direction_a = in_dir & 3 // eg. north (1+8)&3 (0000 0011) = 1 (0000 0001)
var/first_step_direction_b = in_dir & 12 // eg. west (1+8)&12 (0000 1100) = 8 (0000 1000)

for(var/first_step_direction in list(first_step_direction_a,first_step_direction_b))
var/turf/midstep_turf = get_step(destination_turf,first_step_direction)
var/way_blocked = midstep_turf.density || LinkBlockedWithAccess(midstep_turf,caller,ID, no_id = no_id) || midstep_turf.LinkBlockedWithAccess(destination_turf,caller,ID, no_id = no_id)
var/way_blocked = midstep_turf.density || LinkBlockedWithAccess(midstep_turf,caller, access, no_id = no_id) || midstep_turf.LinkBlockedWithAccess(destination_turf,caller,access, no_id = no_id)
if(!way_blocked)
return FALSE
return TRUE
Expand All @@ -432,7 +432,7 @@
// if(destination_turf.density)
// return TRUE
if(TURF_PATHING_PASS_PROC)
if(!destination_turf.CanAStarPass(ID, actual_dir , caller, no_id = no_id))
if(!destination_turf.CanAStarPass(access, actual_dir , caller, no_id = no_id))
return TRUE
if(TURF_PATHING_PASS_NO)
return TRUE
Expand All @@ -444,7 +444,7 @@
continue
if(!border.density && border.can_astar_pass == CANASTARPASS_DENSITY)
continue
if(!border.CanAStarPass(ID, actual_dir, no_id = no_id))
if(!border.CanAStarPass(access, actual_dir, no_id = no_id))
return TRUE

// Destination blockers check
Expand All @@ -453,6 +453,6 @@
// This is an optimization because of the massive call count of this code
if(!iter_object.density && iter_object.can_astar_pass == CANASTARPASS_DENSITY)
continue
if(!iter_object.CanAStarPass(ID, reverse_dir, caller, no_id))
if(!iter_object.CanAStarPass(access, reverse_dir, caller, no_id))
return TRUE
return FALSE
30 changes: 11 additions & 19 deletions code/controllers/subsystem/movement/movement_types.dm
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@
repath_delay,
max_path_length,
minimum_distance,
obj/item/card/id/id,
list/access,
simulated_only,
turf/avoid,
skip_first,
Expand All @@ -329,7 +329,7 @@
repath_delay,
max_path_length,
minimum_distance,
id,
access,
simulated_only,
avoid,
skip_first,
Expand All @@ -342,8 +342,8 @@
var/max_path_length
///Minimum distance to the target before path returns
var/minimum_distance
///An ID card representing what access we have and what doors we can open. Kill me
var/obj/item/card/id/id
///A list representing what access we have and what doors we can open.
var/list/access
///Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
var/simulated_only
///A perticular turf to avoid
Expand All @@ -363,24 +363,21 @@
. = ..()
on_finish_callback = CALLBACK(src, PROC_REF(on_finish_pathing))

/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, obj/item/card/id/id, simulated_only, turf/avoid, skip_first, list/initial_path)
/datum/move_loop/has_target/jps/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, list/initial_path)
. = ..()
if(!.)
return
src.repath_delay = repath_delay
src.max_path_length = max_path_length
src.minimum_distance = minimum_distance
src.id = id
src.access = access
src.simulated_only = simulated_only
src.avoid = avoid
src.skip_first = skip_first
movement_path = initial_path.Copy()

if(istype(id, /obj/item/card/id))
RegisterSignal(id, COMSIG_PARENT_QDELETING, PROC_REF(handle_no_id)) //I prefer erroring to harddels. If this breaks anything consider making id info into a datum or something
movement_path = initial_path?.Copy()

/datum/move_loop/has_target/jps/compare_loops(datum/move_loop/loop_type, priority, flags, extra_info, delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, obj/item/card/id/id, simulated_only, turf/avoid, skip_first, initial_path)
if(..() && repath_delay == src.repath_delay && max_path_length == src.max_path_length && minimum_distance == src.minimum_distance && id == src.id && simulated_only == src.simulated_only && avoid == src.avoid)
if(..() && repath_delay == src.repath_delay && max_path_length == src.max_path_length && minimum_distance == src.minimum_distance && access ~= src.access && simulated_only == src.simulated_only && avoid == src.avoid)
return TRUE
return FALSE

Expand All @@ -394,20 +391,15 @@
movement_path = null

/datum/move_loop/has_target/jps/Destroy()
id = null //Kill me
avoid = null
return ..()

/datum/move_loop/has_target/jps/proc/handle_no_id()
SIGNAL_HANDLER
id = null

///Tries to calculate a new path for this moveloop.
/datum/move_loop/has_target/jps/proc/recalculate_path()
if(!COOLDOWN_FINISHED(src, repath_cooldown))
return
COOLDOWN_START(src, repath_cooldown, repath_delay)
if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, id, simulated_only, avoid, skip_first, on_finish = on_finish_callback))
if(SSpathfinder.pathfind(moving, target, max_path_length, minimum_distance, access, simulated_only, avoid, skip_first, on_finish = on_finish_callback))
is_pathing = TRUE
SEND_SIGNAL(src, COMSIG_MOVELOOP_JPS_REPATH)

Expand All @@ -428,7 +420,8 @@
var/atom/old_loc = moving.loc
//KAPU NOTE: WE DO NOT HAVE THIS
//moving.Move(next_step, get_dir(moving, next_step), FALSE, !(flags & MOVEMENT_LOOP_NO_DIR_UPDATE))
moving.Move(next_step, get_dir(moving, next_step))
var/movement_dir = get_dir(moving, next_step)
moving.Move(next_step, movement_dir)
. = (old_loc != moving?.loc) ? MOVELOOP_SUCCESS : MOVELOOP_FAILURE

// this check if we're on exactly the next tile may be overly brittle for dense objects who may get bumped slightly
Expand All @@ -439,7 +432,6 @@
INVOKE_ASYNC(src, PROC_REF(recalculate_path))
return MOVELOOP_FAILURE


///Base class of move_to and move_away, deals with the distance and target aspect of things
/datum/move_loop/has_target/dist_bound
var/distance = 0
Expand Down
4 changes: 2 additions & 2 deletions code/controllers/subsystem/pathfinder.dm
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ SUBSYSTEM_DEF(pathfinder)
currentrun.len--

/// Initiates a pathfind. Returns true if we're good, FALSE if something's failed
/datum/controller/subsystem/pathfinder/proc/pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE, datum/callback/on_finish)
var/datum/pathfind/path = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude, skip_first, diagonal_safety, on_finish)
/datum/controller/subsystem/pathfinder/proc/pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, list/access=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE, datum/callback/on_finish)
var/datum/pathfind/path = new(caller, end, access, max_distance, mintargetdist, simulated_only, exclude, skip_first, diagonal_safety, on_finish)
if(path.start())
active_pathing += path
return TRUE
Expand Down
2 changes: 1 addition & 1 deletion code/datums/ai/_ai_controller.dm
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ multiple modular subtrees with behaviors
///Stored arguments for behaviors given during their initial creation
var/list/behavior_args = list()
///Tracks recent pathing attempts, if we fail too many in a row we fail our current plans.
var/pathing_attempts
var/consecutive_pathing_attempts
///Can the AI remain in control if there is a client?
var/continue_processing_when_client = FALSE
///distance to give up on target
Expand Down
2 changes: 1 addition & 1 deletion code/datums/ai/dog/dog_controller.dm
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
if(!istype(simple_pawn))
return

return simple_pawn.access_card
return simple_pawn.access_card.GetAccess()

/// Someone has thrown something, see if it's someone we care about and start listening to the thrown item so we can see if we want to fetch it when it lands
/datum/ai_controller/dog/proc/listened_throw(datum/source, mob/living/carbon/carbon_thrower)
Expand Down
21 changes: 14 additions & 7 deletions code/datums/ai/movement/_ai_movement.dm
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@
var/list/moving_controllers = list()
///How many times a given controller can fail on their route before they just give up
var/max_pathing_attempts
var/max_path_length = AI_MAX_PATH_LENGTH

//Override this to setup the moveloop you want to use
/datum/ai_movement/proc/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
SHOULD_CALL_PARENT(TRUE)
controller.pathing_attempts = 0
controller.consecutive_pathing_attempts = 0
controller.blackboard[BB_CURRENT_MIN_MOVE_DISTANCE] = min_distance
moving_controllers[controller] = current_movement_target

/datum/ai_movement/proc/stop_moving_towards(datum/ai_controller/controller)
controller.pathing_attempts = 0
controller.consecutive_pathing_attempts = 0
moving_controllers -= controller
SSmove_manager.stop_looping(controller.pawn, SSai_movement)

/datum/ai_movement/proc/increment_pathing_failures(datum/ai_controller/controller)
controller.pathing_attempts++
if(controller.pathing_attempts >= max_pathing_attempts)
controller.consecutive_pathing_attempts++
if(controller.consecutive_pathing_attempts >= max_pathing_attempts)
controller.CancelActions()

/datum/ai_movement/proc/reset_pathing_failures(datum/ai_controller/controller)
controller.consecutive_pathing_attempts = 0

///Should the movement be allowed to happen? return TRUE if it can, FALSE otherwise
/datum/ai_movement/proc/allowed_to_move(datum/move_loop/source)
SHOULD_BE_PURE(TRUE)
Expand All @@ -46,7 +50,7 @@
///Anything to do before moving; any checks if the pawn should be able to move should be placed in allowed_to_move() and called by this proc
/datum/ai_movement/proc/pre_move(datum/move_loop/source)
SIGNAL_HANDLER
SHOULD_NOT_OVERRIDE(TRUE)
SHOULD_CALL_PARENT(TRUE)

var/datum/ai_controller/controller = source.extra_info

Expand All @@ -66,7 +70,10 @@
//Anything to do post movement
/datum/ai_movement/proc/post_move(datum/move_loop/source, succeeded)
SIGNAL_HANDLER
if(succeeded != FALSE)
return
SHOULD_CALL_PARENT(TRUE)

var/datum/ai_controller/controller = source.extra_info
if(succeeded != MOVELOOP_FAILURE)
reset_pathing_failures(controller)
return
increment_pathing_failures(controller)
27 changes: 22 additions & 5 deletions code/datums/ai/movement/ai_movement_jps.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* This movement datum represents smart-pathing
*/
/datum/ai_movement/jps
max_pathing_attempts = 4
max_pathing_attempts = 20

/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
. = ..()
Expand All @@ -12,10 +12,10 @@
var/datum/move_loop/loop = SSmove_manager.jps_move(moving,
current_movement_target,
delay,
repath_delay = 2 SECONDS,
max_path_length = AI_MAX_PATH_LENGTH,
repath_delay = 0.5 SECONDS,
max_path_length = max_path_length,
minimum_distance = controller.get_minimum_distance(),
id = controller.get_access(),
access = controller.get_access(),
subsystem = SSai_movement,
extra_info = controller,
initial_path = controller.blackboard[BB_PATH_TO_USE])
Expand All @@ -28,5 +28,22 @@
SIGNAL_HANDLER
var/datum/ai_controller/controller = source.extra_info

source.id = controller.get_access()
source.access = controller.get_access()
source.minimum_distance = controller.get_minimum_distance()

/datum/ai_movement/jps/modsuit
max_path_length = MOD_AI_RANGE

/datum/ai_movement/jps/modsuit/pre_move(datum/move_loop/source)
. = ..()
if(.)
return
var/datum/move_loop/has_target/jps/moveloop = source
if(!length(moveloop.movement_path))
return

var/datum/ai_controller/controller = source.extra_info
var/obj/item/mod = controller.pawn
var/angle = get_angle(mod, moveloop.movement_path[1])
mod.transform = matrix().Turn(angle)

4 changes: 2 additions & 2 deletions code/datums/ai/objects/mod.dm
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
BB_MOD_IMPLANT,
)
max_target_distance = MOD_AI_RANGE //a little spicy but its one specific item that summons it, and it doesnt run otherwise
ai_movement = /datum/ai_movement/jps
ai_movement = /datum/ai_movement/jps/modsuit
///ID card generated from the suit's required access. Used for pathing.
var/obj/item/card/id/advanced/id_card

Expand All @@ -28,7 +28,7 @@
queue_behavior(/datum/ai_behavior/mod_attach)

/datum/ai_controller/mod/get_access()
return id_card
return id_card.GetAccess()

/datum/ai_behavior/mod_attach
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT|AI_BEHAVIOR_MOVE_AND_PERFORM
Expand Down
2 changes: 1 addition & 1 deletion code/datums/ai/oldhostile/hostile_tameable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
if(!istype(simple_pawn))
return

return simple_pawn.access_card
return simple_pawn.access_card.GetAccess()

/datum/ai_controller/hostile_friend/proc/on_ridden_driver_move(atom/movable/movable_parent, mob/living/user, direction)
SIGNAL_HANDLER
Expand Down
4 changes: 2 additions & 2 deletions code/game/atoms.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2252,15 +2252,15 @@
* 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
* * access- A list representing what access we have (and thus if we can open things like airlocks or windows to pass through them).
* * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions
* * caller- The movable we're checking pass flags for, if we're making any such checks
* * no_id: When true, doors with public access will count as impassible
*
* IMPORTANT NOTE: /turf/proc/LinkBlockedWithAccess assumes that overrides of CanAStarPass will always return true if density is FALSE
* If this is NOT you, ensure you edit your can_astar_pass variable. Check __DEFINES/path.dm
**/
/atom/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
/atom/proc/CanAStarPass(list/access, to_dir, atom/movable/caller, no_id = FALSE)
if(caller && (caller.pass_flags & pass_flags_self))
return TRUE
. = !density
Expand Down
4 changes: 2 additions & 2 deletions code/game/machinery/doors/airlock.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1279,9 +1279,9 @@
assemblytype = initial(airlock.assemblytype)
update_appearance()

/obj/machinery/door/airlock/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE)
/obj/machinery/door/airlock/CanAStarPass(list/access, to_dir, atom/movable/caller, no_id = FALSE)
//Airlock is passable if it is open (!density), bot has access, and is not bolted shut or powered off)
return !density || (check_access(ID) && !locked && hasPower() && !no_id)
return !density || (check_access_list(access) && !locked && hasPower() && !no_id)

/obj/machinery/door/airlock/emag_act(mob/user, obj/item/card/emag/doorjack/D)
if(!operating && density && hasPower() && !(obj_flags & EMAGGED))
Expand Down
Loading

0 comments on commit f25b24b

Please sign in to comment.