From c2dc8409672ddf38ce90d0ac244be948accd58b3 Mon Sep 17 00:00:00 2001 From: Iajret Creature <122297233+Steals-The-PRs@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:10:57 +0300 Subject: [PATCH] [MIRROR] Reworks targeting behavior to fall back onto proximity monitors. 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](https://github.com/tgstation/tgstation/commit/7047d294dd2675b7e38db2424d6d79c52f21783a) [Adds modify_cooldown, uses it to optimize find_potential_targets further](https://github.com/tgstation/tgstation/commit/4ebc8cedcefaa57a1c3d1afbbb485baa6fae0a4f) 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](https://github.com/tgstation/tgstation/commit/bcf7d7c5b371e46c02aa9039d4ccd4715b999400) 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](https://github.com/tgstation/tgstation/commit/80b63b3445778eb2c75fc2f8f61aca07ed482b1f) 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 <154629622+NovaBot13@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> --- code/__DEFINES/ai/ai_blackboard.dm | 5 + .../dcs/signals/signals_ai_controller.dm | 4 + code/datums/ai/_ai_behavior.dm | 8 +- code/datums/ai/_ai_controller.dm | 12 +- .../basic_ai_behaviors/stop_and_stare.dm | 5 +- .../basic_ai_behaviors/targeting.dm | 94 ++++++++++++++- .../basic_ai_behaviors/ventcrawling.dm | 4 +- code/datums/ai/dog/dog_behaviors.dm | 2 +- code/datums/components/connect_range.dm | 31 +++-- code/datums/elements/hostile_machine.dm | 4 + code/datums/proximity_monitor/field.dm | 92 +++++++++----- .../fields/ai_target_tracking.dm | 113 ++++++++++++++++++ .../proximity_monitor/fields/gravity.dm | 10 +- .../fields/projectile_dampener.dm | 11 +- .../proximity_monitor/fields/timestop.dm | 4 +- .../proximity_monitor/proximity_monitor.dm | 9 +- .../objects/effects/poster_motivational.dm | 2 +- .../traitor/components/demoraliser.dm | 2 +- .../transport/elevator/elev_music_zone.dm | 4 +- tgstation.dme | 1 + 20 files changed, 342 insertions(+), 75 deletions(-) create mode 100644 code/datums/proximity_monitor/fields/ai_target_tracking.dm diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm index df9b8be1075..0047c9c63a4 100644 --- a/code/__DEFINES/ai/ai_blackboard.dm +++ b/code/__DEFINES/ai/ai_blackboard.dm @@ -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]" diff --git a/code/__DEFINES/dcs/signals/signals_ai_controller.dm b/code/__DEFINES/dcs/signals/signals_ai_controller.dm index fa442c3d186..a405ad65e87 100644 --- a/code/__DEFINES/dcs/signals/signals_ai_controller.dm +++ b/code/__DEFINES/dcs/signals/signals_ai_controller.dm @@ -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]" diff --git a/code/datums/ai/_ai_behavior.dm b/code/datums/ai/_ai_behavior.dm index 98ab8f2fc7e..233b57b1577 100644 --- a/code/datums/ai/_ai_behavior.dm +++ b/code/datums/ai/_ai_behavior.dm @@ -5,8 +5,14 @@ ///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, ...) @@ -14,7 +20,7 @@ ///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 diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 0a7fc959e44..195117f69ed 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -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) @@ -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) @@ -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) @@ -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 @@ -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 @@ -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) @@ -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) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/stop_and_stare.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/stop_and_stare.dm index cc02528262d..df066ac0418 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/stop_and_stare.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/stop_and_stare.dm @@ -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 diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm index 8ba9624c21e..b54bfe82f90 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm @@ -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 @@ -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]) @@ -25,6 +33,10 @@ 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) @@ -32,6 +44,7 @@ GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom) 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 @@ -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 @@ -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) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm index e162cc61299..889b474ad03 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm @@ -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 diff --git a/code/datums/ai/dog/dog_behaviors.dm b/code/datums/ai/dog/dog_behaviors.dm index c7723eab1a1..eed116330fb 100644 --- a/code/datums/ai/dog/dog_behaviors.dm +++ b/code/datums/ai/dog/dog_behaviors.dm @@ -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) diff --git a/code/datums/components/connect_range.dm b/code/datums/components/connect_range.dm index 95cb560c868..d3407f46714 100644 --- a/code/datums/components/connect_range.dm +++ b/code/datums/components/connect_range.dm @@ -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). @@ -41,7 +43,7 @@ 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. @@ -49,7 +51,7 @@ /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, @@ -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 @@ -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) diff --git a/code/datums/elements/hostile_machine.dm b/code/datums/elements/hostile_machine.dm index 0a5f19287bb..cb846d830e8 100644 --- a/code/datums/elements/hostile_machine.dm +++ b/code/datums/elements/hostile_machine.dm @@ -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) diff --git a/code/datums/proximity_monitor/field.dm b/code/datums/proximity_monitor/field.dm index 12c033cc426..67bbef948ef 100644 --- a/code/datums/proximity_monitor/field.dm +++ b/code/datums/proximity_monitor/field.dm @@ -27,35 +27,59 @@ /datum/proximity_monitor/advanced/proc/cleanup_field() for(var/turf/turf as anything in edge_turfs) cleanup_edge_turf(turf) + edge_turfs = list() for(var/turf/turf as anything in field_turfs) cleanup_field_turf(turf) + field_turfs = list() //Call every time the field moves (done automatically if you use update_center) or a setup specification is changed. -/datum/proximity_monitor/advanced/proc/recalculate_field() +/datum/proximity_monitor/advanced/proc/recalculate_field(full_recalc = FALSE) var/list/new_turfs = update_new_turfs() - var/list/new_field_turfs = new_turfs[FIELD_TURFS_KEY] - var/list/new_edge_turfs = new_turfs[EDGE_TURFS_KEY] + var/list/old_field_turfs = field_turfs + var/list/old_edge_turfs = edge_turfs + field_turfs = new_turfs[FIELD_TURFS_KEY] + edge_turfs = new_turfs[EDGE_TURFS_KEY] + if(!full_recalc) + field_turfs = list() + edge_turfs = list() - for(var/turf/old_turf as anything in field_turfs) - if(!(old_turf in new_field_turfs)) - cleanup_field_turf(old_turf) - for(var/turf/old_turf as anything in edge_turfs) + for(var/turf/old_turf as anything in old_field_turfs - field_turfs) + if(QDELETED(src)) + return + cleanup_field_turf(old_turf) + for(var/turf/old_turf as anything in old_edge_turfs - edge_turfs) + if(QDELETED(src)) + return cleanup_edge_turf(old_turf) - for(var/turf/new_turf as anything in new_field_turfs) - field_turfs |= new_turf + if(full_recalc) + old_field_turfs = list() + old_edge_turfs = list() + field_turfs = new_turfs[FIELD_TURFS_KEY] + edge_turfs = new_turfs[EDGE_TURFS_KEY] + + for(var/turf/new_turf as anything in field_turfs - old_field_turfs) + if(QDELETED(src)) + return + field_turfs += new_turf setup_field_turf(new_turf) - for(var/turf/new_turf as anything in new_edge_turfs) - edge_turfs |= new_turf + for(var/turf/new_turf as anything in edge_turfs - old_edge_turfs) + if(QDELETED(src)) + return + edge_turfs += new_turf setup_edge_turf(new_turf) -/datum/proximity_monitor/advanced/on_entered(turf/source, atom/movable/entered) +/datum/proximity_monitor/advanced/on_initialized(turf/location, atom/created, init_flags) + . = ..() + on_entered(location, created, null) + +/datum/proximity_monitor/advanced/on_entered(turf/source, atom/movable/entered, turf/old_loc) . = ..() if(get_dist(source, host) == current_range) - field_edge_crossed(entered, source) + field_edge_crossed(entered, old_loc, source) else - field_turf_crossed(entered, source) + field_turf_crossed(entered, old_loc, source) /datum/proximity_monitor/advanced/on_moved(atom/movable/movable, atom/old_loc) . = ..() @@ -68,21 +92,22 @@ if(isturf(old_loc)) cleanup_field() return - recalculate_field() + recalculate_field(full_recalc = FALSE) /datum/proximity_monitor/advanced/on_uncrossed(turf/source, atom/movable/gone, direction) if(get_dist(source, host) == current_range) - field_edge_uncrossed(gone, source) + field_edge_uncrossed(gone, source, get_turf(gone)) else - field_turf_uncrossed(gone, source) + field_turf_uncrossed(gone, source, get_turf(gone)) /// Called when a turf in the field of the monitor is linked /datum/proximity_monitor/advanced/proc/setup_field_turf(turf/target) return /// Called when a turf in the field of the monitor is unlinked +/// Do NOT call this manually, requires management of the field_turfs list /datum/proximity_monitor/advanced/proc/cleanup_field_turf(turf/target) - field_turfs -= target + return /// Called when a turf in the edge of the monitor is linked /datum/proximity_monitor/advanced/proc/setup_edge_turf(turf/target) @@ -90,21 +115,22 @@ setup_field_turf(target) /// Called when a turf in the edge of the monitor is unlinked +/// Do NOT call this manually, requires management of the edge_turfs list /datum/proximity_monitor/advanced/proc/cleanup_edge_turf(turf/target) if(edge_is_a_field) // If the edge is considered a field, clean it up like one cleanup_field_turf(target) - edge_turfs -= target /datum/proximity_monitor/advanced/proc/update_new_turfs() - . = list(FIELD_TURFS_KEY = list(), EDGE_TURFS_KEY = list()) if(ignore_if_not_on_turf && !isturf(host.loc)) - return + return list(FIELD_TURFS_KEY = list(), EDGE_TURFS_KEY = list()) + var/list/local_field_turfs = list() + var/list/local_edge_turfs = list() var/turf/center = get_turf(host) - for(var/turf/target in RANGE_TURFS(current_range, center)) - if(get_dist(center, target) == current_range) - .[EDGE_TURFS_KEY] += target - else - .[FIELD_TURFS_KEY] += target + if(current_range > 0) + local_field_turfs += RANGE_TURFS(current_range - 1, center) + if(current_range > 1) + local_edge_turfs = local_field_turfs - RANGE_TURFS(current_range, center) + return list(FIELD_TURFS_KEY = local_field_turfs, EDGE_TURFS_KEY = local_edge_turfs) //Gets edge direction/corner, only works with square radius/WDH fields! /datum/proximity_monitor/advanced/proc/get_edgeturf_direction(turf/T, turf/center_override = null) @@ -124,19 +150,19 @@ if(T.y == (checking_from.y + current_range)) return NORTH -/datum/proximity_monitor/advanced/proc/field_turf_crossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/proc/field_turf_crossed(atom/movable/movable, turf/old_location, turf/new_location) return -/datum/proximity_monitor/advanced/proc/field_turf_uncrossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/proc/field_turf_uncrossed(atom/movable/movable, turf/old_location, turf/new_location) return -/datum/proximity_monitor/advanced/proc/field_edge_crossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/proc/field_edge_crossed(atom/movable/movable, turf/old_location, turf/new_location) if(edge_is_a_field) // If the edge is considered a field, pass crossed to that - field_turf_crossed(movable, location) + field_turf_crossed(movable, old_location, new_location) -/datum/proximity_monitor/advanced/proc/field_edge_uncrossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/proc/field_edge_uncrossed(atom/movable/movable, turf/old_location, turf/new_location) if(edge_is_a_field) // If the edge is considered a field, pass uncrossed to that - field_turf_uncrossed(movable, location) + field_turf_uncrossed(movable, old_location, new_location) //DEBUG FIELD ITEM /obj/item/multitool/field_debug @@ -153,7 +179,7 @@ current = new(src, 5, FALSE) current.set_fieldturf_color = "#aaffff" current.set_edgeturf_color = "#ffaaff" - current.recalculate_field() + current.recalculate_field(full_recalc = TRUE) /obj/item/multitool/field_debug/attack_self(mob/user) operating = !operating diff --git a/code/datums/proximity_monitor/fields/ai_target_tracking.dm b/code/datums/proximity_monitor/fields/ai_target_tracking.dm new file mode 100644 index 00000000000..46cde22aaff --- /dev/null +++ b/code/datums/proximity_monitor/fields/ai_target_tracking.dm @@ -0,0 +1,113 @@ +// Proximity monitor that checks to see if anything interesting enters our bounds +/datum/proximity_monitor/advanced/ai_target_tracking + edge_is_a_field = TRUE + /// The ai behavior who owns us + var/datum/ai_behavior/find_potential_targets/owning_behavior + /// The ai controller we're using + var/datum/ai_controller/controller + /// The target key we're trying to fill + var/target_key + /// The targeting strategy KEY we're using + var/targeting_strategy_key + /// The hiding location key we're using + var/hiding_location_key + + /// The targeting strategy we're using + var/datum/targeting_strategy/filter + /// If we've built our field yet + /// Prevents wasted work on the first build (since the behavior did it) + var/first_build = TRUE + +// Initially, run the check manually +// If that fails, set up a field and have it manage the behavior fully +/datum/proximity_monitor/advanced/ai_target_tracking/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, datum/ai_behavior/find_potential_targets/owning_behavior, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + src.owning_behavior = owning_behavior + src.controller = controller + src.target_key = target_key + src.targeting_strategy_key = targeting_strategy_key + src.hiding_location_key = hiding_location_key + src.filter = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key]) + RegisterSignal(controller, COMSIG_QDELETING, PROC_REF(controller_deleted)) + RegisterSignal(controller, COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS, PROC_REF(controller_think)) + RegisterSignal(controller, COMSIG_AI_CONTROLLER_POSSESSED_PAWN, PROC_REF(pawn_changed)) + RegisterSignal(controller, AI_CONTROLLER_BEHAVIOR_QUEUED(owning_behavior.type), PROC_REF(behavior_requeued)) + RegisterSignal(controller, COMSIG_AI_BLACKBOARD_KEY_SET(targeting_strategy_key), PROC_REF(targeting_datum_changed)) + RegisterSignal(controller, COMSIG_AI_BLACKBOARD_KEY_CLEARED(targeting_strategy_key), PROC_REF(targeting_datum_cleared)) + recalculate_field(full_recalc = TRUE) + +/datum/proximity_monitor/advanced/ai_target_tracking/Destroy() + . = ..() + if(!QDELETED(controller) && owning_behavior) + controller.modify_cooldown(owning_behavior, owning_behavior.get_cooldown(controller)) + owning_behavior = null + controller = null + target_key = null + targeting_strategy_key = null + hiding_location_key = null + filter = null + +/datum/proximity_monitor/advanced/ai_target_tracking/recalculate_field(full_recalc = FALSE) + . = ..() + first_build = FALSE + +/datum/proximity_monitor/advanced/ai_target_tracking/setup_field_turf(turf/target) + . = ..() + if(first_build) + return + owning_behavior.new_turf_found(target, controller, filter) + +/datum/proximity_monitor/advanced/ai_target_tracking/field_turf_crossed(atom/movable/movable, turf/location, turf/old_location) + . = ..() + + if(!owning_behavior.atom_allowed(movable, filter, controller.pawn)) + return + + owning_behavior.new_atoms_found(list(movable), controller, target_key, filter, hiding_location_key) + +/// React to controller planning +/datum/proximity_monitor/advanced/ai_target_tracking/proc/controller_deleted(datum/source) + SIGNAL_HANDLER + qdel(src) + +/// React to the pawn goin byebye +/datum/proximity_monitor/advanced/ai_target_tracking/proc/pawn_changed(datum/source) + SIGNAL_HANDLER + qdel(src) + +/// React to controller planning +/datum/proximity_monitor/advanced/ai_target_tracking/proc/controller_think(datum/ai_controller/source, list/datum/ai_behavior/old_behaviors, list/datum/ai_behavior/new_behaviors) + SIGNAL_HANDLER + // If our parent was forgotten, nuke ourselves + if(!new_behaviors[owning_behavior]) + qdel(src) + +/datum/proximity_monitor/advanced/ai_target_tracking/proc/behavior_requeued(datum/source, list/new_arguments) + SIGNAL_HANDLER + check_new_args(arglist(new_arguments)) + +/// Ensure our args and locals are up to date +/datum/proximity_monitor/advanced/ai_target_tracking/proc/check_new_args(target_key, targeting_strategy_key, hiding_location_key) + var/update_filter = FALSE + if(src.target_key != target_key) + src.target_key = target_key + if(src.targeting_strategy_key != targeting_strategy_key) + src.targeting_strategy_key = targeting_strategy_key + update_filter = TRUE + if(src.hiding_location_key != hiding_location_key) + src.hiding_location_key = hiding_location_key + if(update_filter) + targeting_datum_changed(null) + +/datum/proximity_monitor/advanced/ai_target_tracking/proc/targeting_datum_changed(datum/source) + SIGNAL_HANDLER + filter = controller.blackboard[targeting_strategy_key] + // Filter changed, need to do a full reparse + // Fucking 9 * 9 out here I stg + for(var/turf/in_field as anything in field_turfs + edge_turfs) + owning_behavior.new_turf_found(in_field, controller, filter) + +/datum/proximity_monitor/advanced/ai_target_tracking/proc/targeting_datum_cleared(datum/source) + SIGNAL_HANDLER + // Go fuckin home bros + qdel(src) diff --git a/code/datums/proximity_monitor/fields/gravity.dm b/code/datums/proximity_monitor/fields/gravity.dm index e2604267657..b7e22840041 100644 --- a/code/datums/proximity_monitor/fields/gravity.dm +++ b/code/datums/proximity_monitor/fields/gravity.dm @@ -7,7 +7,7 @@ /datum/proximity_monitor/advanced/gravity/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, gravity) . = ..() gravity_value = gravity - recalculate_field() + recalculate_field(full_recalc = TRUE) /datum/proximity_monitor/advanced/gravity/setup_field_turf(turf/target) . = ..() @@ -42,15 +42,15 @@ for(var/mob/living/guy in target) warn_mob(guy, target) -/datum/proximity_monitor/advanced/gravity/warns_on_entrance/field_edge_crossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/gravity/warns_on_entrance/field_edge_crossed(atom/movable/movable, turf/old_location, turf/new_location) . = ..() if(isliving(movable)) - warn_mob(movable, location) + warn_mob(movable, new_location) -/datum/proximity_monitor/advanced/gravity/warns_on_entrance/field_edge_uncrossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/gravity/warns_on_entrance/field_edge_uncrossed(atom/movable/movable, turf/old_location, turf/new_location) . = ..() if(isliving(movable)) - warn_mob(movable, location) + warn_mob(movable, old_location) /datum/proximity_monitor/advanced/gravity/warns_on_entrance/proc/warn_mob(mob/living/to_warn, turf/location) var/mob_ref_key = REF(to_warn) diff --git a/code/datums/proximity_monitor/fields/projectile_dampener.dm b/code/datums/proximity_monitor/fields/projectile_dampener.dm index 705ac6bface..3e696e5fb13 100644 --- a/code/datums/proximity_monitor/fields/projectile_dampener.dm +++ b/code/datums/proximity_monitor/fields/projectile_dampener.dm @@ -19,7 +19,7 @@ /datum/proximity_monitor/advanced/projectile_dampener/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, atom/projector) ..() RegisterSignal(projector, COMSIG_QDELETING, PROC_REF(on_projector_del)) - recalculate_field() + recalculate_field(full_recalc = TRUE) START_PROCESSING(SSfastprocess, src) /datum/proximity_monitor/advanced/projectile_dampener/Destroy() @@ -48,7 +48,7 @@ LAZYSET(edgeturf_effects, target, effect) /datum/proximity_monitor/advanced/projectile_dampener/on_z_change(datum/source) - recalculate_field() + recalculate_field(full_recalc = TRUE) /datum/proximity_monitor/advanced/projectile_dampener/cleanup_edge_turf(turf/target) . = ..() @@ -90,16 +90,15 @@ /datum/proximity_monitor/advanced/projectile_dampener/proc/on_projector_del(datum/source) SIGNAL_HANDLER - qdel(src) -/datum/proximity_monitor/advanced/projectile_dampener/field_edge_uncrossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/projectile_dampener/field_edge_uncrossed(atom/movable/movable, turf/old_location, turf/new_location) if(isprojectile(movable) && get_dist(movable, host) > current_range) if(movable in tracked) release_projectile(movable) -/datum/proximity_monitor/advanced/projectile_dampener/field_edge_crossed(atom/movable/movable, turf/location) - if(isprojectile(movable) && !(movable in tracked)) +/datum/proximity_monitor/advanced/projectile_dampener/field_edge_crossed(atom/movable/movable, turf/location, turf/old_location) + if(isprojectile(movable)) capture_projectile(movable) /datum/proximity_monitor/advanced/projectile_dampener/peaceborg/process(seconds_per_tick) diff --git a/code/datums/proximity_monitor/fields/timestop.dm b/code/datums/proximity_monitor/fields/timestop.dm index c48759c1deb..a3e22483c7a 100644 --- a/code/datums/proximity_monitor/fields/timestop.dm +++ b/code/datums/proximity_monitor/fields/timestop.dm @@ -82,7 +82,7 @@ src.immune = immune src.antimagic_flags = antimagic_flags src.channelled = channelled - recalculate_field() + recalculate_field(full_recalc = TRUE) START_PROCESSING(SSfastprocess, src) /datum/proximity_monitor/advanced/timestop/Destroy() @@ -93,7 +93,7 @@ STOP_PROCESSING(SSfastprocess, src) return ..() -/datum/proximity_monitor/advanced/timestop/field_turf_crossed(atom/movable/movable, turf/location) +/datum/proximity_monitor/advanced/timestop/field_turf_crossed(atom/movable/movable, turf/old_location, turf/new_location) freeze_atom(movable) /datum/proximity_monitor/advanced/timestop/proc/freeze_atom(atom/movable/A) diff --git a/code/datums/proximity_monitor/proximity_monitor.dm b/code/datums/proximity_monitor/proximity_monitor.dm index fc28212202d..ec77ce2145a 100644 --- a/code/datums/proximity_monitor/proximity_monitor.dm +++ b/code/datums/proximity_monitor/proximity_monitor.dm @@ -11,7 +11,7 @@ var/static/list/loc_connections = list( COMSIG_ATOM_ENTERED = PROC_REF(on_entered), COMSIG_ATOM_EXITED = PROC_REF(on_uncrossed), - COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON = PROC_REF(on_entered), + COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON = PROC_REF(on_initialized), ) /datum/proximity_monitor/New(atom/_host, range, _ignore_if_not_on_turf = TRUE) @@ -78,7 +78,12 @@ SIGNAL_HANDLER return //Used by the advanced subtype for effect fields. -/datum/proximity_monitor/proc/on_entered(atom/source, atom/movable/arrived) +/datum/proximity_monitor/proc/on_entered(atom/source, atom/movable/arrived, turf/old_loc) SIGNAL_HANDLER if(source != host) hasprox_receiver?.HasProximity(arrived) + +/datum/proximity_monitor/proc/on_initialized(turf/location, atom/created, init_flags) + SIGNAL_HANDLER + if(location != host) + hasprox_receiver?.HasProximity(created) diff --git a/code/game/objects/effects/poster_motivational.dm b/code/game/objects/effects/poster_motivational.dm index 12c22a30fbf..bc20c551193 100644 --- a/code/game/objects/effects/poster_motivational.dm +++ b/code/game/objects/effects/poster_motivational.dm @@ -79,7 +79,7 @@ src.department = department RegisterSignal(host, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) -/datum/proximity_monitor/advanced/quirk_posters/field_turf_crossed(atom/movable/crossed, turf/location) +/datum/proximity_monitor/advanced/quirk_posters/field_turf_crossed(atom/movable/crossed, turf/old_location, turf/new_location) if (!isliving(crossed) || !can_see(crossed, host, current_range)) return on_seen(crossed) diff --git a/code/modules/antagonists/traitor/components/demoraliser.dm b/code/modules/antagonists/traitor/components/demoraliser.dm index 47cdae620f4..ee44527728c 100644 --- a/code/modules/antagonists/traitor/components/demoraliser.dm +++ b/code/modules/antagonists/traitor/components/demoraliser.dm @@ -13,7 +13,7 @@ src.moods = moods RegisterSignal(host, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) -/datum/proximity_monitor/advanced/demoraliser/field_turf_crossed(atom/movable/crossed, turf/location) +/datum/proximity_monitor/advanced/demoraliser/field_turf_crossed(atom/movable/crossed, turf/old_location, turf/new_location) if (!isliving(crossed)) return if (!can_see(crossed, host, current_range)) diff --git a/code/modules/transport/elevator/elev_music_zone.dm b/code/modules/transport/elevator/elev_music_zone.dm index 1f09a00a68b..045407fe781 100644 --- a/code/modules/transport/elevator/elev_music_zone.dm +++ b/code/modules/transport/elevator/elev_music_zone.dm @@ -67,7 +67,7 @@ GLOBAL_LIST_EMPTY(elevator_music) QDEL_LIST_ASSOC_VAL(tracked_mobs) return ..() -/datum/proximity_monitor/advanced/elevator_music_area/field_turf_crossed(mob/entered, turf/location) +/datum/proximity_monitor/advanced/elevator_music_area/field_turf_crossed(mob/entered, turf/old_location, turf/new_location) if (!istype(entered) || !entered.mind) return @@ -80,7 +80,7 @@ GLOBAL_LIST_EMPTY(elevator_music) tracked_mobs[entered] = null // Still add it to the list so we don't keep making this check RegisterSignal(entered, COMSIG_QDELETING, PROC_REF(mob_destroyed)) -/datum/proximity_monitor/advanced/elevator_music_area/field_turf_uncrossed(mob/exited, turf/location) +/datum/proximity_monitor/advanced/elevator_music_area/field_turf_uncrossed(mob/exited, turf/old_location, turf/new_location) if (!(exited in tracked_mobs)) return if (exited.z == host.z && get_dist(exited, host) <= current_range) diff --git a/tgstation.dme b/tgstation.dme index 25aaeb30c87..4c011a1c3d9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1749,6 +1749,7 @@ #include "code\datums\mutations\webbing.dm" #include "code\datums\proximity_monitor\field.dm" #include "code\datums\proximity_monitor\proximity_monitor.dm" +#include "code\datums\proximity_monitor\fields\ai_target_tracking.dm" #include "code\datums\proximity_monitor\fields\gravity.dm" #include "code\datums\proximity_monitor\fields\projectile_dampener.dm" #include "code\datums\proximity_monitor\fields\timestop.dm"