Skip to content

Commit

Permalink
[MIRROR] Reworks targeting behavior to fall back onto proximity monit…
Browse files Browse the repository at this point in the history
…ors. Refactors ai cooldowns a bit (#2931)

* Reworks targeting behavior to fall back onto proximity monitors. Refactors ai cooldowns a bit (#82640)

## About The Pull Request

Nother bit ripped out of #79498
[Implements a get_cooldown() proc to get around dumb manual overrides
and empower me to optimize the findtarget
logic](tgstation/tgstation@7047d29)

[Adds modify_cooldown, uses it to optimize find_potential_targets
further](tgstation/tgstation@4ebc8ce)

No sense running the behavior if we're just waiting on its output, so
let's run it once a minute just in case, then push an update instantly
if we find something

[Optimizes connect_range and
promxity_monitors](tgstation/tgstation@bcf7d7c)

We know what turfs exist before and after a move
We can use this information to prevent trying to update turfs we don't
care about.

This is important because post these changes mobs with fields will be
moving a lot more, so it's gotta be cheap

[Implements a special kind of field to handle ai
targeting](tgstation/tgstation@80b63b3)

If we run targeting and don't like, find anything, we should setup a
field that listens for things coming near us and then handle those
things as we find them.

This incurs a slight startup cost but saves so much time on the churn of
constant costs

Note:
We should also work to figure out a way to avoid waking ais if none is
near them/they aren't doing anything interesting

We don't need to do that immediately this acts as somewhat of a stopgap
(and would be good regardless) but it is worth keeping in mind)

## IMPORTANT

I am unsure whether this is worth it anymore since #82539 was merged. As
I say it was done as a stopgap because ais didn't know how to idle.
If not I'll rip er out and we'll keep the other
refactoring/optimizations.

## Why It's Good For The Game

Cleaner basic ai code, maybe? faster basic ai code, for sure faster
proximity monitors (significantly)

* Reworks targeting behavior to fall back onto proximity monitors. Refactors ai cooldowns a bit

---------

Co-authored-by: NovaBot <[email protected]>
Co-authored-by: LemonInTheDark <[email protected]>
  • Loading branch information
3 people authored Apr 17, 2024
1 parent 8782827 commit c2dc840
Show file tree
Hide file tree
Showing 20 changed files with 342 additions and 75 deletions.
5 changes: 5 additions & 0 deletions code/__DEFINES/ai/ai_blackboard.dm
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,8 @@

///Text we display when we befriend someone
#define BB_FRIENDLY_MESSAGE "friendly_message"

// Keys used by one and only one behavior
// Used to hold state without making bigass lists
/// For /datum/ai_behavior/find_potential_targets, what if any field are we using currently
#define BB_FIND_TARGETS_FIELD(type) "bb_find_targets_field_[type]"
4 changes: 4 additions & 0 deletions code/__DEFINES/dcs/signals/signals_ai_controller.dm
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@

///sent from ai controllers when they possess a pawn: (datum/ai_controller/source_controller)
#define COMSIG_AI_CONTROLLER_POSSESSED_PAWN "ai_controller_possessed_pawn"
///sent from ai controllers when they pick behaviors: (list/datum/ai_behavior/old_behaviors, list/datum/ai_behavior/new_behaviors)
#define COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS "ai_controller_picked_behaviors"
///sent from ai controllers when a behavior is inserted into the queue: (list/new_arguments)
#define AI_CONTROLLER_BEHAVIOR_QUEUED(type) "ai_controller_behavior_queued_[type]"
8 changes: 7 additions & 1 deletion code/datums/ai/_ai_behavior.dm
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
///Flags for extra behavior
var/behavior_flags = NONE
///Cooldown between actions performances, defaults to the value of CLICK_CD_MELEE because that seemed like a nice standard for the speed of AI behavior
///Do not read directly or mutate, instead use get_cooldown()
var/action_cooldown = CLICK_CD_MELEE

/// Returns the delay to use for this behavior in the moment
/// Override to return a conditional delay
/datum/ai_behavior/proc/get_cooldown(datum/ai_controller/cooldown_for)
return action_cooldown

/// Called by the ai controller when first being added. Additional arguments depend on the behavior type.
/// Return FALSE to cancel
/datum/ai_behavior/proc/setup(datum/ai_controller/controller, ...)
return TRUE

///Called by the AI controller when this action is performed
/datum/ai_behavior/proc/perform(seconds_per_tick, datum/ai_controller/controller, ...)
controller.behavior_cooldowns[src] = world.time + action_cooldown
controller.behavior_cooldowns[src] = world.time + get_cooldown(controller)
return

///Called when the action is finished. This needs the same args as perform besides the default ones
Expand Down
12 changes: 8 additions & 4 deletions code/datums/ai/_ai_controller.dm
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ multiple modular subtrees with behaviors
if(ai_traits & CAN_ACT_WHILE_DEAD)
return AI_STATUS_ON
return AI_STATUS_OFF

var/turf/pawn_turf = get_turf(mob_pawn)
#ifdef TESTING
if(!pawn_turf)
Expand Down Expand Up @@ -214,7 +214,6 @@ multiple modular subtrees with behaviors
return FALSE
return TRUE


///Runs any actions that are currently running
/datum/ai_controller/process(seconds_per_tick)

Expand Down Expand Up @@ -242,7 +241,7 @@ multiple modular subtrees with behaviors
// Convert the current behaviour action cooldown to realtime seconds from deciseconds.current_behavior
// Then pick the max of this and the seconds_per_tick passed to ai_controller.process()
// Action cooldowns cannot happen faster than seconds_per_tick, so seconds_per_tick should be the value used in this scenario.
var/action_seconds_per_tick = max(current_behavior.action_cooldown * 0.1, seconds_per_tick)
var/action_seconds_per_tick = max(current_behavior.get_cooldown(src) * 0.1, seconds_per_tick)

if(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //Might need to move closer
if(!current_movement_target)
Expand Down Expand Up @@ -298,6 +297,7 @@ multiple modular subtrees with behaviors
if(subtree.SelectBehaviors(src, seconds_per_tick) == SUBTREE_RETURN_FINISH_PLANNING)
break

SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS, current_behaviors, planned_behaviors)
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
if(LAZYACCESS(planned_behaviors, current_behavior))
continue
Expand All @@ -311,7 +311,7 @@ multiple modular subtrees with behaviors
/datum/ai_controller/proc/set_ai_status(new_ai_status)
if(ai_status == new_ai_status)
return FALSE //no change

//remove old status, if we've got one
if(ai_status)
SSai_controllers.ai_controllers_by_status[ai_status] -= src
Expand All @@ -330,6 +330,9 @@ multiple modular subtrees with behaviors
/datum/ai_controller/proc/PauseAi(time)
paused_until = world.time + time

/datum/ai_controller/proc/modify_cooldown(datum/ai_behavior/behavior, new_cooldown)
behavior_cooldowns[behavior.type] = new_cooldown

///Call this to add a behavior to the stack.
/datum/ai_controller/proc/queue_behavior(behavior_type, ...)
var/datum/ai_behavior/behavior = GET_AI_BEHAVIOR(behavior_type)
Expand All @@ -351,6 +354,7 @@ multiple modular subtrees with behaviors
behavior_args[behavior_type] = arguments
else
behavior_args -= behavior_type
SEND_SIGNAL(src, AI_CONTROLLER_BEHAVIOR_QUEUED(behavior_type), arguments)

/datum/ai_controller/proc/ProcessBehavior(seconds_per_tick, datum/ai_behavior/behavior)
var/list/arguments = list(seconds_per_tick, src)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
var/atom/movable/target = controller.blackboard[target_key]
return ismovable(target) && isturf(target.loc) && ismob(controller.pawn)

/datum/ai_behavior/stop_and_stare/get_cooldown(datum/ai_controller/cooldown_for)
return cooldown_for.blackboard[BB_STATIONARY_COOLDOWN]

/datum/ai_behavior/stop_and_stare/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
// i don't really like doing this but we wanna make sure that the cooldown is pertinent to what we need for this specific controller before we invoke parent
action_cooldown = controller.blackboard[BB_STATIONARY_COOLDOWN]
. = ..()
var/atom/movable/target = controller.blackboard[target_key]
if(!ismovable(target) || !isturf(target.loc)) // just to make sure that nothing funky happened between setup and perform
Expand Down
94 changes: 92 additions & 2 deletions code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/// List of objects that AIs will treat as targets
GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)
/// Static typecache list of things we are interested in
/// Consider this a union of the for loop and the hearers call from below
/// Must be kept up to date with the contents of hostile_machines
GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/porta_turret, /obj/vehicle/sealed/mecha)))

/datum/ai_behavior/find_potential_targets
action_cooldown = 2 SECONDS
Expand All @@ -8,8 +12,12 @@ GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)
/// Blackboard key for aggro range, uses vision range if not specified
var/aggro_range_key = BB_AGGRO_RANGE

/datum/ai_behavior/find_potential_targets/get_cooldown(datum/ai_controller/cooldown_for)
if(cooldown_for.blackboard[BB_FIND_TARGETS_FIELD(type)])
return 60 SECONDS
return ..()

/datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
var/mob/living/living_mob = controller.pawn
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])

Expand All @@ -25,13 +33,18 @@ GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)

controller.clear_blackboard_key(target_key)

// If we're using a field rn, just don't do anything yeah?
if(controller.blackboard[BB_FIND_TARGETS_FIELD(type)])
return

var/list/potential_targets = hearers(aggro_range, get_turf(controller.pawn)) - living_mob //Remove self, so we don't suicide

for (var/atom/hostile_machine as anything in GLOB.hostile_machines)
if (can_see(living_mob, hostile_machine, aggro_range))
potential_targets += hostile_machine

if(!potential_targets.len)
failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
finish_action(controller, succeeded = FALSE)
return

Expand All @@ -43,6 +56,7 @@ GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)
continue

if(!filtered_targets.len)
failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
finish_action(controller, succeeded = FALSE)
return

Expand All @@ -56,10 +70,86 @@ GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)

finish_action(controller, succeeded = TRUE)

/datum/ai_behavior/find_potential_targets/finish_action(datum/ai_controller/controller, succeeded, ...)
/datum/ai_behavior/find_potential_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
// takes the larger between our range() input and our implicit hearers() input (world.view)
aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2))
// Alright, here's the interesting bit
// We're gonna use this max range to hook into a proximity field so we can just await someone interesting to come along
// Rather then trying to check every few seconds
var/datum/proximity_monitor/advanced/ai_target_tracking/detection_field = new(
controller.pawn,
aggro_range,
TRUE,
src,
controller,
target_key,
targeting_strategy_key,
hiding_location_key,
)
// We're gonna store this field in our blackboard, so we can clear it away if we end up finishing successsfully
controller.set_blackboard_key(BB_FIND_TARGETS_FIELD(type), detection_field)

/datum/ai_behavior/find_potential_targets/proc/new_turf_found(turf/found, datum/ai_controller/controller, datum/targeting_strategy/strategy)
var/valid_found = FALSE
var/mob/pawn = controller.pawn
for(var/maybe_target as anything in found)
if(maybe_target == pawn)
continue
if(!is_type_in_typecache(maybe_target, GLOB.target_interested_atoms))
continue
if(!strategy.can_attack(pawn, maybe_target))
continue
valid_found = TRUE
break
if(!valid_found)
return
// If we found any one thing we "could" attack, then run the full search again so we can select from the best possible canidate
var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)]
qdel(field) // autoclears so it's fine
// Fire instantly, you should find something I hope
controller.modify_cooldown(src, world.time)

/datum/ai_behavior/find_potential_targets/proc/atom_allowed(atom/movable/checking, datum/targeting_strategy/strategy, mob/pawn)
if(checking == pawn)
return FALSE
if(!ismob(checking) && !is_type_in_typecache(checking, GLOB.target_interested_atoms))
return FALSE
if(!strategy.can_attack(pawn, checking))
return FALSE
return TRUE

/datum/ai_behavior/find_potential_targets/proc/new_atoms_found(list/atom/movable/found, datum/ai_controller/controller, target_key, datum/targeting_strategy/strategy, hiding_location_key)
var/mob/pawn = controller.pawn
var/list/accepted_targets = list()
for(var/maybe_target as anything in found)
if(maybe_target == pawn)
continue
// Need to better handle viewers here
if(!ismob(maybe_target) && !is_type_in_typecache(maybe_target, GLOB.target_interested_atoms))
continue
if(!strategy.can_attack(pawn, maybe_target))
continue
accepted_targets += maybe_target

// Alright, we found something acceptable, let's use it yeah?
var/atom/target = pick_final_target(controller, accepted_targets)
controller.set_blackboard_key(target_key, target)

var/atom/potential_hiding_location = strategy.find_hidden_mobs(pawn, target)

if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
controller.set_blackboard_key(hiding_location_key, potential_hiding_location)

finish_action(controller, succeeded = TRUE)

/datum/ai_behavior/find_potential_targets/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if (succeeded)
var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)]
qdel(field) // autoclears so it's fine
controller.CancelActions() // On retarget cancel any further queued actions so that they will setup again with new target
controller.modify_cooldown(controller, get_cooldown(controller))

/// Returns the desired final target from the filtered list of targets
/datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
Expand Down
4 changes: 3 additions & 1 deletion code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
/datum/ai_behavior/crawl_through_vents
action_cooldown = 10 SECONDS

/datum/ai_behavior/crawl_through_vents/get_cooldown(datum/ai_controller/cooldown_for)
return cooldown_for.blackboard[BB_VENTCRAWL_COOLDOWN] || initial(action_cooldown)

/datum/ai_behavior/crawl_through_vents/setup(datum/ai_controller/controller, target_key)
action_cooldown = controller.blackboard[BB_VENTCRAWL_COOLDOWN] || initial(action_cooldown)
. = ..()
var/obj/machinery/atmospherics/components/unary/vent_pump/target = controller.blackboard[target_key] || controller.blackboard[BB_ENTRY_VENT_TARGET]
return istype(target) && isliving(controller.pawn) // only mobs can vent crawl in the current framework
Expand Down
2 changes: 1 addition & 1 deletion code/datums/ai/dog/dog_behaviors.dm
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
required_distance = 3

/datum/ai_behavior/basic_melee_attack/dog/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
controller.behavior_cooldowns[src] = world.time + action_cooldown
controller.behavior_cooldowns[src] = world.time + get_cooldown(controller)
var/mob/living/living_pawn = controller.pawn
if(!(isturf(living_pawn.loc) || HAS_TRAIT(living_pawn, TRAIT_AI_BAGATTACK))) // Void puppies can attack from inside bags
finish_action(controller, FALSE, target_key, targeting_strategy_key, hiding_location_key)
Expand Down
31 changes: 19 additions & 12 deletions code/datums/components/connect_range.dm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/// An assoc list of signal -> procpath to register to the loc this object is on.
var/list/connections
/// The turfs currently connected to this component
var/list/turfs = list()
/**
* The atom the component is tracking. The component will delete itself if the tracked is deleted.
* Signals will also be updated whenever it moves (if it's a movable).
Expand Down Expand Up @@ -41,15 +43,15 @@
if(src.range == range && src.works_in_containers == works_in_containers)
return
//Unregister the signals with the old settings.
unregister_signals(isturf(tracked) ? tracked : tracked.loc)
unregister_signals(isturf(tracked) ? tracked : tracked.loc, turfs)
src.range = range
src.works_in_containers = works_in_containers
//Re-register the signals with the new settings.
update_signals(src.tracked)

/datum/component/connect_range/proc/set_tracked(atom/new_tracked)
if(tracked) //Unregister the signals from the old tracked and its surroundings
unregister_signals(isturf(tracked) ? tracked : tracked.loc)
unregister_signals(isturf(tracked) ? tracked : tracked.loc, turfs)
UnregisterSignal(tracked, list(
COMSIG_MOVABLE_MOVED,
COMSIG_QDELETING,
Expand All @@ -66,28 +68,34 @@
SIGNAL_HANDLER
qdel(src)

/datum/component/connect_range/proc/update_signals(atom/target, atom/old_loc, forced = FALSE)
/datum/component/connect_range/proc/update_signals(atom/target, atom/old_loc)
var/turf/current_turf = get_turf(target)
var/on_same_turf = current_turf == get_turf(old_loc) //Only register/unregister turf signals if it's moved to a new turf.
unregister_signals(old_loc, on_same_turf)

if(isnull(current_turf))
unregister_signals(old_loc, turfs)
turfs = list()
return

if(ismovable(target.loc))
if(!works_in_containers)
unregister_signals(old_loc, turfs)
turfs = list()
return
//Keep track of possible movement of all movables the target is in.
for(var/atom/movable/container as anything in get_nested_locs(target))
RegisterSignal(container, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))

if(on_same_turf && !forced)
//Only register/unregister turf signals if it's moved to a new turf.
if(current_turf == get_turf(old_loc))
unregister_signals(old_loc, null)
return
for(var/turf/target_turf in RANGE_TURFS(range, current_turf))
var/list/old_turfs = turfs
turfs = RANGE_TURFS(range, current_turf)
unregister_signals(old_loc, old_turfs - turfs)
for(var/turf/target_turf as anything in turfs - old_turfs)
for(var/signal in connections)
parent.RegisterSignal(target_turf, signal, connections[signal])

/datum/component/connect_range/proc/unregister_signals(atom/location, on_same_turf = FALSE)
/datum/component/connect_range/proc/unregister_signals(atom/location, list/remove_from)
//The location is null or is a container and the component shouldn't have register signals on it
if(isnull(location) || (!works_in_containers && !isturf(location)))
return
Expand All @@ -96,10 +104,9 @@
for(var/atom/movable/target as anything in (get_nested_locs(location) + location))
UnregisterSignal(target, COMSIG_MOVABLE_MOVED)

if(on_same_turf)
if(!length(remove_from))
return
var/turf/previous_turf = get_turf(location)
for(var/turf/target_turf in RANGE_TURFS(range, previous_turf))
for(var/turf/target_turf as anything in remove_from)
parent.UnregisterSignal(target_turf, connections)

/datum/component/connect_range/proc/on_moved(atom/movable/movable, atom/old_loc)
Expand Down
4 changes: 4 additions & 0 deletions code/datums/elements/hostile_machine.dm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
if (!isatom(target))
return ELEMENT_INCOMPATIBLE

#ifdef UNIT_TESTS
if(!GLOB.target_interested_atoms[target.type])
stack_trace("Tried to make a hostile machine without updating ai targeting to include it, they must be synced")
#endif
GLOB.hostile_machines += target

/datum/element/hostile_machine/Detach(datum/source)
Expand Down
Loading

0 comments on commit c2dc840

Please sign in to comment.