From be48d7598fc4d741e4b79a7eb08c36f79ca3353f Mon Sep 17 00:00:00 2001 From: warriorstar-orion Date: Tue, 9 Dec 2025 18:46:38 -0500 Subject: [PATCH] wire up ai idling to spatial grid (#30711) * wire up ai idling to spatial grid * lint * Apply suggestions from code review Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com> Signed-off-by: warriorstar-orion * stupid loadbearing src * some spatial grid cleanup * make sure to remove controllers in the GLOB * immediately set targets and state for hiveloord brood * properly fail behaviors when movement targets are lost * Apply suggestions from code review Comment formatting Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/datums/ai/ai_controller.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/datums/ai/ai_controller.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Apply suggestions from code review Comment formatting Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/datums/ai/ai_controller.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> --------- Signed-off-by: warriorstar-orion Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com> Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> --- code/__DEFINES/MC.dm | 18 ++ code/__DEFINES/dcs/ai_signals.dm | 2 + code/__DEFINES/subsystems.dm | 1 + code/_globalvars/lists/basic_ai_lists.dm | 18 ++ .../controllers/subsystem/SSai_controllers.dm | 63 ++-- .../SSunplanned_ai_idle_controllers.dm | 4 + .../subsystem/SSunplanned_controllers.dm | 34 ++ .../subsystem/ai_idle_controllers.dm | 8 + .../processing/SSai_idle_behaviors.dm | 6 + code/datums/ai/ai_behavior.dm | 2 +- code/datums/ai/ai_controller.dm | 294 +++++++++++++----- code/datums/proximity/proximity_monitor.dm | 2 +- code/game/atoms_movable.dm | 3 + .../mob/living/basic/mining/hivelord_ai.dm | 10 +- code/modules/mob/living/living.dm | 4 +- .../tgui/modules/ai_controller_debugger.dm | 4 +- paradise.dme | 5 + 17 files changed, 351 insertions(+), 127 deletions(-) create mode 100644 code/_globalvars/lists/basic_ai_lists.dm create mode 100644 code/controllers/subsystem/SSunplanned_ai_idle_controllers.dm create mode 100644 code/controllers/subsystem/SSunplanned_controllers.dm create mode 100644 code/controllers/subsystem/ai_idle_controllers.dm create mode 100644 code/controllers/subsystem/processing/SSai_idle_behaviors.dm diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm index 828192403d9..7965d1c9916 100644 --- a/code/__DEFINES/MC.dm +++ b/code/__DEFINES/MC.dm @@ -104,3 +104,21 @@ }\ /datum/controller/subsystem/movement/##X/fire() {..() /*just so it shows up on the profiler*/} \ /datum/controller/subsystem/movement/##X + +#define AI_CONTROLLER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/ai_controllers/##X);\ +/datum/controller/subsystem/ai_controllers/##X/New(){\ + NEW_SS_GLOBAL(SS##X);\ + PreInit();\ + ss_id="ai_controller_[#X]";\ +}\ +/datum/controller/subsystem/ai_controllers/##X/fire() {..() /*just so it shows up on the profiler*/} \ +/datum/controller/subsystem/ai_controllers/##X + +#define UNPLANNED_CONTROLLER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/unplanned_controllers/##X);\ +/datum/controller/subsystem/unplanned_controllers/##X/New(){\ + NEW_SS_GLOBAL(SS##X);\ + PreInit();\ + ss_id="unplanned_controller_[#X]";\ +}\ +/datum/controller/subsystem/unplanned_controllers/##X/fire() {..() /*just so it shows up on the profiler*/} \ +/datum/controller/subsystem/unplanned_controllers/##X diff --git a/code/__DEFINES/dcs/ai_signals.dm b/code/__DEFINES/dcs/ai_signals.dm index b9d52316d7c..66e44037e3d 100644 --- a/code/__DEFINES/dcs/ai_signals.dm +++ b/code/__DEFINES/dcs/ai_signals.dm @@ -18,3 +18,5 @@ #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 they stop possessing a pawn: (datum/ai_controller/source_controller) +#define COMSIG_AI_CONTROLLER_UNPOSSESSED_PAWN "ai_controller_unpossessed_pawn" diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 1a1a54cd1e6..d92a12d42d4 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -99,6 +99,7 @@ #define FIRE_PRIORITY_PING 10 #define FIRE_PRIORITY_NIGHTSHIFT 10 #define FIRE_PRIORITY_IDLE_NPC 10 +#define FIRE_PRIORITY_UNPLANNED_NPC 10 #define FIRE_PRIORITY_CLEANUP 10 #define FIRE_PRIORITY_TICKETS 10 #define FIRE_PRIORITY_RESEARCH 10 // SoonTM diff --git a/code/_globalvars/lists/basic_ai_lists.dm b/code/_globalvars/lists/basic_ai_lists.dm new file mode 100644 index 00000000000..6c28bfd03a0 --- /dev/null +++ b/code/_globalvars/lists/basic_ai_lists.dm @@ -0,0 +1,18 @@ +/// All basic AI subtrees +GLOBAL_LIST_EMPTY(ai_subtrees) + +/// Basic AI controllers based on status +GLOBAL_LIST_INIT(ai_controllers_by_status, list( + AI_STATUS_ON = list(), + AI_STATUS_OFF = list(), + AI_STATUS_IDLE = list(), +)) + +/// Basic AI controllers based on their z level +GLOBAL_LIST_EMPTY(ai_controllers_by_zlevel) + +/// Basic AI controllers that are currently performing idled behaviors +GLOBAL_LIST_INIT_TYPED(unplanned_controllers, /list/datum/ai_controller, list( + AI_STATUS_ON = list(), + AI_STATUS_IDLE = list(), +)) diff --git a/code/controllers/subsystem/SSai_controllers.dm b/code/controllers/subsystem/SSai_controllers.dm index 8e2b1953227..e3b58caddcd 100644 --- a/code/controllers/subsystem/SSai_controllers.dm +++ b/code/controllers/subsystem/SSai_controllers.dm @@ -7,59 +7,54 @@ SUBSYSTEM_DEF(ai_controllers) wait = 0.5 SECONDS //Plan every half second if required, not great not terrible. runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME - ///List of all ai_subtree singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_subtrees() - var/list/datum/ai_planning_subtree/ai_subtrees = list() - ///Assoc List of all AI statuses and all AI controllers with that status. - var/list/ai_controllers_by_status = list( - AI_STATUS_ON = list(), - AI_STATUS_OFF = list(), - AI_STATUS_IDLE = list(), - ) - ///Assoc List of all AI controllers and the Z level they are on, which we check when someone enters/leaves a Z level to turn them on/off. - var/list/ai_controllers_by_zlevel = list() + /// Type of status we are interested in running + var/planning_status = AI_STATUS_ON /// The tick cost of all active AI, calculated on fire. - var/cost_on - /// The tick cost of all idle AI, calculated on fire. - var/cost_idle - + var/our_cost + /// The tick cost of all currently processed AI, being summed together + var/summing_cost /datum/controller/subsystem/ai_controllers/Initialize() setup_subtrees() /datum/controller/subsystem/ai_controllers/stat_entry(msg) - var/list/active_list = ai_controllers_by_status[AI_STATUS_ON] - var/list/inactive_list = ai_controllers_by_status[AI_STATUS_OFF] - var/list/idle_list = ai_controllers_by_status[AI_STATUS_IDLE] - msg = "Active AIs:[length(active_list)]/[round(cost_on,1)]%|Inactive:[length(inactive_list)]|Idle:[length(idle_list)]/[round(cost_idle,1)]%" + var/list/planning_list = GLOB.ai_controllers_by_status[planning_status] + msg = "Planning AIs:[length(planning_list)]/[round(our_cost,1)]%" return ..() /datum/controller/subsystem/ai_controllers/fire(resumed) + if(!resumed) + summing_cost = 0 var/timer = TICK_USAGE_REAL - cost_idle = MC_AVERAGE(cost_idle, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer)) - - timer = TICK_USAGE_REAL - for(var/datum/ai_controller/ai_controller as anything in ai_controllers_by_status[AI_STATUS_ON]) - if(!ai_controller.able_to_plan()) + for(var/datum/ai_controller/ai_controller as anything in GLOB.ai_controllers_by_status[planning_status]) + if(!ai_controller.able_to_plan) continue - ai_controller.select_behaviors(wait / (1 SECONDS)) - if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan + ai_controller.select_behaviors(wait * 0.1) + + if(!length(ai_controller.current_behaviors)) // Still no plan ai_controller.planning_failed() if(MC_TICK_CHECK) break - cost_on = MC_AVERAGE(cost_on, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer)) + summing_cost += TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer) + if(MC_TICK_CHECK) + return -///Creates all instances of ai_subtrees and assigns them to the ai_subtrees list. + our_cost = MC_AVERAGE(our_cost, summing_cost) + +/// Creates all instances of ai_subtrees and assigns them to the ai_subtrees list. /datum/controller/subsystem/ai_controllers/proc/setup_subtrees() + if(length(GLOB.ai_subtrees)) + return for(var/subtree_type in subtypesof(/datum/ai_planning_subtree)) var/datum/ai_planning_subtree/subtree = new subtree_type - ai_subtrees[subtree_type] = subtree + GLOB.ai_subtrees[subtree_type] = subtree -///Called when the max Z level was changed, updating our coverage. +/// Called when the max Z level was changed, updating our coverage. /datum/controller/subsystem/ai_controllers/proc/on_max_z_changed() - if(!islist(ai_controllers_by_zlevel)) - ai_controllers_by_zlevel = new /list(world.maxz, 0) - while(SSai_controllers.ai_controllers_by_zlevel.len < world.maxz) - SSai_controllers.ai_controllers_by_zlevel.len++ - SSai_controllers.ai_controllers_by_zlevel[ai_controllers_by_zlevel.len] = list() + if(!length(GLOB.ai_controllers_by_zlevel)) + GLOB.ai_controllers_by_zlevel = new /list(world.maxz,0) + while(GLOB.ai_controllers_by_zlevel.len < world.maxz) + GLOB.ai_controllers_by_zlevel.len++ + GLOB.ai_controllers_by_zlevel[GLOB.ai_controllers_by_zlevel.len] = list() diff --git a/code/controllers/subsystem/SSunplanned_ai_idle_controllers.dm b/code/controllers/subsystem/SSunplanned_ai_idle_controllers.dm new file mode 100644 index 00000000000..6385239e18c --- /dev/null +++ b/code/controllers/subsystem/SSunplanned_ai_idle_controllers.dm @@ -0,0 +1,4 @@ +UNPLANNED_CONTROLLER_SUBSYSTEM_DEF(idle_unplanned_controllers) + name = "Unplanned AI Idle Controllers" + wait = 2.5 SECONDS + target_status = AI_STATUS_IDLE diff --git a/code/controllers/subsystem/SSunplanned_controllers.dm b/code/controllers/subsystem/SSunplanned_controllers.dm new file mode 100644 index 00000000000..1fefae3ae46 --- /dev/null +++ b/code/controllers/subsystem/SSunplanned_controllers.dm @@ -0,0 +1,34 @@ +GLOBAL_LIST_EMPTY(unplanned_controller_subsystems) +/// Handles making mobs perform lightweight "idle" behaviors such as wandering around when they have nothing planned +SUBSYSTEM_DEF(unplanned_controllers) + name = "AI Unplanned Controllers" + flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + priority = FIRE_PRIORITY_UNPLANNED_NPC + wait = 0.25 SECONDS + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + /// What ai status are we interested in + var/target_status = AI_STATUS_ON + var/list/current_run = list() + +/datum/controller/subsystem/unplanned_controllers/Initialize() + GLOB.unplanned_controller_subsystems += src + +/datum/controller/subsystem/unplanned_controllers/Destroy() + GLOB.unplanned_controller_subsystems -= src + return ..() + +/datum/controller/subsystem/unplanned_controllers/stat_entry(msg) + msg = "Planning AIs:[length(GLOB.unplanned_controllers[target_status])]" + return ..() + +/datum/controller/subsystem/unplanned_controllers/fire(resumed) + if(!resumed) + src.current_run = GLOB.unplanned_controllers[target_status].Copy() + var/list/current_run = src.current_run // cache for sonic speed + while(length(current_run)) + var/datum/ai_controller/unplanned = current_run[current_run.len] + current_run.len-- + if(!QDELETED(unplanned)) + unplanned.idle_behavior.perform_idle_behavior(wait * 0.1, unplanned) + if(MC_TICK_CHECK) + return diff --git a/code/controllers/subsystem/ai_idle_controllers.dm b/code/controllers/subsystem/ai_idle_controllers.dm new file mode 100644 index 00000000000..367a2c82ffc --- /dev/null +++ b/code/controllers/subsystem/ai_idle_controllers.dm @@ -0,0 +1,8 @@ +AI_CONTROLLER_SUBSYSTEM_DEF(ai_idle_controllers) + name = "AI Idle Controllers" + flags = SS_POST_FIRE_TIMING | SS_BACKGROUND + priority = FIRE_PRIORITY_IDLE_NPC + init_order = INIT_ORDER_AI_IDLE_CONTROLLERS + wait = 5 SECONDS + runlevels = RUNLEVEL_GAME + planning_status = AI_STATUS_IDLE diff --git a/code/controllers/subsystem/processing/SSai_idle_behaviors.dm b/code/controllers/subsystem/processing/SSai_idle_behaviors.dm new file mode 100644 index 00000000000..e92ab3ef400 --- /dev/null +++ b/code/controllers/subsystem/processing/SSai_idle_behaviors.dm @@ -0,0 +1,6 @@ +PROCESSING_SUBSYSTEM_DEF(idle_ai_behaviors) + name = "AI Idle Behaviors" + flags = SS_NO_INIT | SS_BACKGROUND + wait = 1.5 SECONDS + priority = FIRE_PRIORITY_IDLE_NPC + init_order = INIT_ORDER_AI_IDLE_CONTROLLERS // must execute only after ai behaviors are initialized diff --git a/code/datums/ai/ai_behavior.dm b/code/datums/ai/ai_behavior.dm index a00c55aeb94..32245a5c60d 100644 --- a/code/datums/ai/ai_behavior.dm +++ b/code/datums/ai/ai_behavior.dm @@ -44,7 +44,7 @@ /// [AI_BEHAVIOR_FAILED]. /datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded, ...) SHOULD_CALL_PARENT(TRUE) - LAZYREMOVE(controller.current_behaviors, src) + controller.dequeue_behavior(src) controller.behavior_args -= type // If this was a movement task, reset our movement target if necessary if(!(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT)) diff --git a/code/datums/ai/ai_controller.dm b/code/datums/ai/ai_controller.dm index 68d0998b145..16818475739 100644 --- a/code/datums/ai/ai_controller.dm +++ b/code/datums/ai/ai_controller.dm @@ -26,9 +26,9 @@ RESTRICT_TYPE(/datum/ai_controller) /// Bitfield of traits for this AI to handle extra behavior. var/ai_traits = NONE /// Current actions planned to be performed by the AI in the upcoming plan. - var/list/planned_behaviors + var/list/planned_behaviors = list() /// Current actions being performed by the AI. - var/list/current_behaviors + var/list/current_behaviors = list() /// Current actions and their respective last time ran as an assoc list. var/list/behavior_cooldowns = list() /// Current status of AI (OFF/ON) @@ -55,6 +55,8 @@ RESTRICT_TYPE(/datum/ai_controller) /// The idle behavior this AI performs when it has no actions. var/datum/idle_behavior/idle_behavior = null + /// Our current cell grid. + var/datum/cell_tracker/our_cells // Movement related things here /// Reference to the movement datum we use. Is a type on initialize but becomes a ref afterwards. @@ -75,10 +77,11 @@ RESTRICT_TYPE(/datum/ai_controller) /// Make sure you hook update_able_to_run() in setup_able_to_run() to whatever parameters changing that you added /// Otherwise we will not pay attention to them changing var/able_to_run = FALSE + /// are we even able to plan? + var/able_to_plan = TRUE /// are we currently on failed planning timeout? var/on_failed_planning_timeout = FALSE - /datum/ai_controller/New(atom/new_pawn) change_ai_movement_type(ai_movement) init_subtrees() @@ -90,15 +93,19 @@ RESTRICT_TYPE(/datum/ai_controller) possess_pawn(new_pawn) /datum/ai_controller/Destroy(force) - set_ai_status(AI_STATUS_OFF) unpossess_pawn(FALSE) + + if(ai_status) + GLOB.ai_controllers_by_status[ai_status] -= src + + our_cells = null set_movement_target(type, null) if(ai_movement.moving_controllers[src]) ai_movement.stop_moving_towards(src) - LAZYCLEARLIST(planned_behaviors) + planned_behaviors.Cut() LAZYCLEARLIST(planning_subtrees) - LAZYCLEARLIST(current_behaviors) + current_behaviors.Cut() return ..() @@ -135,7 +142,7 @@ RESTRICT_TYPE(/datum/ai_controller) return var/list/temp_subtree_list = list() for(var/subtree in planning_subtrees) - var/subtree_instance = SSai_controllers.ai_subtrees[subtree] + var/subtree_instance = GLOB.ai_subtrees[subtree] temp_subtree_list += subtree_instance planning_subtrees = temp_subtree_list @@ -162,7 +169,7 @@ RESTRICT_TYPE(/datum/ai_controller) var/turf/pawn_turf = get_turf(pawn) if(pawn_turf) - SSai_controllers.ai_controllers_by_zlevel[pawn_turf.z] += src + GLOB.ai_controllers_by_zlevel[pawn_turf.z] += src SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_POSSESSED_PAWN) @@ -174,6 +181,18 @@ RESTRICT_TYPE(/datum/ai_controller) update_able_to_run() setup_able_to_run() + our_cells = new(interesting_dist, interesting_dist, 1) + set_new_cells() + + RegisterSignal(pawn, COMSIG_MOVABLE_MOVED, PROC_REF(update_grid)) + +/datum/ai_controller/proc/update_grid(datum/source, datum/spatial_grid_cell/new_cell) + SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED + + set_new_cells() + if(current_movement_target) + check_target_max_distance() + /datum/ai_controller/proc/on_movement_target_move(datum/source) SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED check_target_max_distance() @@ -186,6 +205,66 @@ RESTRICT_TYPE(/datum/ai_controller) if(get_dist(current_movement_target, pawn) > max_target_distance) cancel_actions() +/datum/ai_controller/proc/set_new_cells() + if(isnull(our_cells)) + return + + var/turf/our_turf = get_turf(pawn) + + if(isnull(our_turf)) + return + + var/list/cell_collections = our_cells.recalculate_cells(our_turf) + + for(var/datum/old_grid as anything in cell_collections[2]) + UnregisterSignal(old_grid, list(SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS))) + + for(var/datum/spatial_grid_cell/new_grid as anything in cell_collections[1]) + RegisterSignal(new_grid, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_client_enter)) + RegisterSignal(new_grid, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_client_exit)) + + recalculate_idle() + +/datum/ai_controller/proc/should_idle() + if(!can_idle || isnull(our_cells)) + return FALSE + for(var/datum/spatial_grid_cell/grid as anything in our_cells.member_cells) + if(locate(/mob/living) in grid.client_contents) + return FALSE + return TRUE + +/datum/ai_controller/proc/recalculate_idle(datum/exited) + if(ai_status == AI_STATUS_OFF) + return + + var/distance = INFINITY + if(islist(exited)) + var/list/exited_list = exited + distance = get_dist(pawn, exited_list[1]) + else if(isatom(exited)) + var/atom/exited_atom = exited + distance = get_dist(pawn, exited_atom) + + if(distance <= interesting_dist) // is our target in between interesting cells? + return + + if(should_idle()) + set_ai_status(AI_STATUS_IDLE) + +/datum/ai_controller/proc/on_client_enter(datum/source, list/target_list) + SIGNAL_HANDLER // COMSIG_CLIENT_ENTER + + if(!(locate(/mob/living) in target_list)) + return + + if(ai_status == AI_STATUS_IDLE) + set_ai_status(AI_STATUS_ON) + +/datum/ai_controller/proc/on_client_exit(datum/source, datum/exited) + SIGNAL_HANDLER // COMSIG_CLIENT_EXIT + + recalculate_idle(exited) + /// Sets the AI on or off based on current conditions, call to reset after you've manually disabled it somewhere /datum/ai_controller/proc/reset_ai_status() set_ai_status(get_expected_ai_status()) @@ -197,20 +276,20 @@ RESTRICT_TYPE(/datum/ai_controller) * Returns AI_STATUS_ON otherwise. */ /datum/ai_controller/proc/get_expected_ai_status() - . = AI_STATUS_ON + if(isnull(get_turf(pawn))) + return AI_STATUS_OFF if(!ismob(pawn)) - return + return AI_STATUS_ON var/mob/living/mob_pawn = pawn if(!continue_processing_when_client && mob_pawn.client) - . = AI_STATUS_OFF - - if(ai_traits & AI_FLAG_CAN_ACT_WHILE_DEAD) - return + return AI_STATUS_OFF if(mob_pawn.stat == DEAD) - . = AI_STATUS_OFF + if(ai_traits & AI_FLAG_CAN_ACT_WHILE_DEAD) + return AI_STATUS_ON + return AI_STATUS_OFF var/turf/pawn_turf = get_turf(mob_pawn) #ifdef GAME_TESTS @@ -218,7 +297,11 @@ RESTRICT_TYPE(/datum/ai_controller) CRASH("AI controller [src] controlling pawn ([pawn]) is not on a turf.") #endif if(!SSmobs.clients_by_zlevel || !length(SSmobs.clients_by_zlevel[pawn_turf.z]) || on_failed_planning_timeout) - . = AI_STATUS_OFF + return AI_STATUS_OFF + if(should_idle()) + return AI_STATUS_IDLE + + return AI_STATUS_ON /// Called when the AI controller pawn changes z levels. /// We check if there's any clients on the new one and wake up the AI if there is. @@ -230,9 +313,9 @@ RESTRICT_TYPE(/datum/ai_controller) if(mob_pawn?.client && !continue_processing_when_client) return if(old_turf) - SSai_controllers.ai_controllers_by_zlevel[old_turf.z] -= src + GLOB.ai_controllers_by_zlevel[old_turf.z] -= src if(new_turf) - SSai_controllers.ai_controllers_by_zlevel[new_turf.z] += src + GLOB.ai_controllers_by_zlevel[new_turf.z] += src reset_ai_status() /// Abstract proc for initializing the pawn to the new controller @@ -241,17 +324,20 @@ RESTRICT_TYPE(/datum/ai_controller) /// Proc for deinitializing the pawn to the old controller /datum/ai_controller/proc/unpossess_pawn(destroy) + SHOULD_CALL_PARENT(TRUE) if(isnull(pawn)) return // instantiated without an applicable pawn, fine - UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE, COMSIG_PARENT_QDELETING)) + SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_UNPOSSESSED_PAWN) + set_ai_status(AI_STATUS_OFF) + UnregisterSignal(pawn, list(COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE, COMSIG_PARENT_QDELETING)) + clear_able_to_run() if(ai_movement.moving_controllers[src]) ai_movement.stop_moving_towards(src) var/turf/pawn_turf = get_turf(pawn) if(pawn_turf) - SSai_controllers.ai_controllers_by_zlevel[pawn_turf.z] -= src - if(ai_status) - SSai_controllers.ai_controllers_by_status[ai_status] -= src + GLOB.ai_controllers_by_zlevel[pawn_turf.z] -= src + remove_from_unplanned_controllers() pawn.ai_controller = null pawn = null if(destroy) @@ -315,77 +401,55 @@ RESTRICT_TYPE(/datum/ai_controller) // in the AI controller implementation are actually the managing subsystem's `wait`. seconds_per_tick /= (1 SECONDS) - if(!able_to_run) - GLOB.move_manager.stop_looping(pawn) //stop moving - return //this should remove them from processing in the future through event-based stuff. - - if(!LAZYLEN(current_behaviors) && idle_behavior) - idle_behavior.perform_idle_behavior(seconds_per_tick, src) //Do some stupid shit while we have nothing to do - return - for(var/datum/ai_behavior/current_behavior as anything in current_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.get_cooldown(src) * 0.1, seconds_per_tick) - if(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //Might need to move closer - if(isnull(current_movement_target)) - fail_behavior(current_behavior) - return - - // Stops pawns from performing such actions that should require the target to be adjacent. - var/atom/movable/moving_pawn = pawn - var/can_reach = !(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_REACH) || moving_pawn.can_reach_nested_adjacent(current_movement_target) - if(can_reach && current_behavior.required_distance >= get_dist(moving_pawn, current_movement_target)) // Are we close enough to engage? - if(ai_movement.moving_controllers[src] == current_movement_target) // We are close enough, if we're moving stop. - ai_movement.stop_moving_towards(src) - - if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown - continue - process_behavior(action_seconds_per_tick, current_behavior) - return - - else if(ai_movement.moving_controllers[src] != current_movement_target) // We're too far, if we're not already moving start doing it. - ai_movement.start_moving_towards(src, current_movement_target, current_behavior.required_distance) // Then start moving - - if(current_behavior.behavior_flags & AI_BEHAVIOR_MOVE_AND_PERFORM) // If we can move and perform then do so. - if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown - continue - process_behavior(action_seconds_per_tick, current_behavior) - return - else // No movement required + if(!(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT)) if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown continue process_behavior(action_seconds_per_tick, current_behavior) return -/// Determines whether the AI can currently make a new plan. -/datum/ai_controller/proc/able_to_plan() - . = TRUE - if(QDELETED(pawn)) - return FALSE - for(var/datum/ai_behavior/current_behavior as anything in current_behaviors) - if(!(current_behavior.behavior_flags & AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION)) // We have a behavior that blocks planning - return FALSE + if(isnull(current_movement_target)) + fail_behavior(current_behavior) + return + + /// Stops pawns from performing such actions that should require the target to be adjacent. + var/atom/movable/moving_pawn = pawn + var/can_reach = !(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_REACH) || moving_pawn.can_reach(current_movement_target) + if(can_reach && current_behavior.required_distance >= get_dist(moving_pawn, current_movement_target)) // Are we close enough to engage? + if(ai_movement.moving_controllers[src] == current_movement_target) // We are close enough, if we're moving stop. + ai_movement.stop_moving_towards(src) + + if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown + continue + process_behavior(action_seconds_per_tick, current_behavior) + return + + if(ai_movement.moving_controllers[src] != current_movement_target) // We're too far, if we're not already moving start doing it. + ai_movement.start_moving_towards(src, current_movement_target, current_behavior.required_distance) // Then start moving + + if(current_behavior.behavior_flags & AI_BEHAVIOR_MOVE_AND_PERFORM) // If we can move and perform then do so. + if(behavior_cooldowns[current_behavior] > world.time) // Still on cooldown + continue + process_behavior(action_seconds_per_tick, current_behavior) + return /// This is where you decide what actions are taken by the AI. /datum/ai_controller/proc/select_behaviors(seconds_per_tick) SHOULD_NOT_SLEEP(TRUE) - if(!COOLDOWN_FINISHED(src, failed_planning_cooldown)) - return FALSE + planned_behaviors.Cut() - LAZYINITLIST(current_behaviors) - LAZYCLEARLIST(planned_behaviors) - - if(LAZYLEN(planning_subtrees)) - for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) - if(subtree.select_behaviors(src, seconds_per_tick) == SUBTREE_RETURN_FINISH_PLANNING) - break + for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) + if(subtree.select_behaviors(src, seconds_per_tick) == SUBTREE_RETURN_FINISH_PLANNING) + break for(var/datum/ai_behavior/current_behavior as anything in current_behaviors) - if(LAZYACCESS(planned_behaviors, current_behavior)) + if(current_behavior in planned_behaviors) continue var/list/arguments = list(src, FALSE) var/list/stored_arguments = behavior_args[type] @@ -394,24 +458,57 @@ RESTRICT_TYPE(/datum/ai_controller) current_behavior.finish_action(arglist(arguments)) /// This proc handles changing AI status, and starts/stops processing if required. -/datum/ai_controller/proc/set_ai_status(new_ai_status) +/datum/ai_controller/proc/set_ai_status(new_ai_status, additional_flags = NONE) 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 + GLOB.ai_controllers_by_status[ai_status] -= src + remove_from_unplanned_controllers() + stop_previous_processing() ai_status = new_ai_status - SSai_controllers.ai_controllers_by_status[new_ai_status] += src + GLOB.ai_controllers_by_status[new_ai_status] += src + if(ai_status == AI_STATUS_OFF) + if(!(additional_flags & AI_PREVENT_CANCEL_ACTIONS)) + cancel_actions() + return + if(!length(current_behaviors)) + add_to_unplanned_controllers() + return + start_ai_processing() + +/datum/ai_controller/proc/start_ai_processing() switch(ai_status) if(AI_STATUS_ON) START_PROCESSING(SSai_behaviors, src) - if(AI_STATUS_OFF, AI_STATUS_IDLE) + if(AI_STATUS_IDLE) + START_PROCESSING(SSidle_ai_behaviors, src) + +/datum/ai_controller/proc/stop_previous_processing() + switch(ai_status) + if(AI_STATUS_ON) STOP_PROCESSING(SSai_behaviors, src) - cancel_actions() + if(AI_STATUS_IDLE) + STOP_PROCESSING(SSidle_ai_behaviors, src) /datum/ai_controller/proc/pause_ai(time) paused_until = world.time + time + update_able_to_run() + addtimer(CALLBACK(src, PROC_REF(update_able_to_run)), time) + +/datum/ai_controller/proc/add_to_unplanned_controllers() + if(isnull(ai_status) || ai_status == AI_STATUS_OFF || isnull(idle_behavior)) + return + GLOB.unplanned_controllers[ai_status][src] = TRUE + +/datum/ai_controller/proc/remove_from_unplanned_controllers() + if(isnull(ai_status) || ai_status == AI_STATUS_OFF) + return + GLOB.unplanned_controllers[ai_status] -= src + for(var/datum/controller/subsystem/unplanned_controllers/potential_holder as anything in GLOB.unplanned_controller_subsystems) + if(potential_holder.target_status == ai_status) + potential_holder.current_run -= src /datum/ai_controller/proc/modify_cooldown(datum/ai_behavior/behavior, new_cooldown) behavior_cooldowns[behavior] = new_cooldown @@ -426,21 +523,50 @@ RESTRICT_TYPE(/datum/ai_controller) // It's still in the plan, don't add it again to current_behaviors // but do keep it in the planned behavior list so its not cancelled - if(LAZYACCESS(current_behaviors, behavior)) - LAZYADDASSOC(planned_behaviors, behavior, TRUE) + if(current_behaviors[behavior]) + planned_behaviors[behavior] = TRUE return if(!behavior.setup(arglist(arguments))) return - LAZYADDASSOC(current_behaviors, behavior, TRUE) - LAZYADDASSOC(planned_behaviors, behavior, TRUE) + + var/should_exit_unplanned = !length(current_behaviors) + planned_behaviors[behavior] = TRUE + current_behaviors[behavior] = TRUE arguments.Cut(1, 2) if(length(arguments)) behavior_args[behavior_type] = arguments else behavior_args -= behavior_type + + if(!(behavior.behavior_flags & AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION)) // this one blocks planning! + able_to_plan = FALSE + + if(should_exit_unplanned) + exit_unplanned_mode() + SEND_SIGNAL(src, AI_CONTROLLER_BEHAVIOR_QUEUED(behavior_type), arguments) +/datum/ai_controller/proc/check_able_to_plan() + for(var/datum/ai_behavior/current_behavior as anything in current_behaviors) + if(!(current_behavior.behavior_flags & AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION)) // We have a behavior that blocks planning + return FALSE + return TRUE + +/datum/ai_controller/proc/dequeue_behavior(datum/ai_behavior/behavior) + current_behaviors -= behavior + able_to_plan = check_able_to_plan() + if(!length(current_behaviors)) + enter_unplanned_mode() + +/datum/ai_controller/proc/exit_unplanned_mode() + remove_from_unplanned_controllers() + start_ai_processing() + +/datum/ai_controller/proc/enter_unplanned_mode() + add_to_unplanned_controllers() + stop_previous_processing() + /datum/ai_controller/proc/process_behavior(seconds_per_tick, datum/ai_behavior/behavior) var/list/arguments = list(seconds_per_tick, src) var/list/stored_arguments = behavior_args[behavior.type] @@ -460,7 +586,7 @@ RESTRICT_TYPE(/datum/ai_controller) behavior.finish_action(arglist(arguments)) /datum/ai_controller/proc/cancel_actions() - if(!LAZYLEN(current_behaviors)) + if(!length(current_behaviors)) return for(var/datum/ai_behavior/current_behavior as anything in current_behaviors) fail_behavior(current_behavior) diff --git a/code/datums/proximity/proximity_monitor.dm b/code/datums/proximity/proximity_monitor.dm index 09a7d32f916..57e713c8c65 100644 --- a/code/datums/proximity/proximity_monitor.dm +++ b/code/datums/proximity/proximity_monitor.dm @@ -74,7 +74,7 @@ //Update the ignore_if_not_on_turf AddComponent(/datum/component/connect_range, host, loc_connections, current_range, ignore_if_not_on_turf) -/datum/proximity_monitor/proc/on_uncrossed() +/datum/proximity_monitor/proc/on_uncrossed(atom/source, atom/movable/exited) SIGNAL_HANDLER // COMSIG_ATOM_EXITED return //Used by the advanced subtype for effect fields. diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 9d53f7abfa7..ec738232081 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -149,6 +149,9 @@ moveToNullspace() + if(spatial_grid_key) + SSspatial_grid.force_remove_from_grid(src) + // This absolutely must be after moveToNullspace() // We rely on Entered and Exited to manage this list, and the copy of this list that is on any /atom/movable "Containers" // If we clear this before the nullspace move, a ref to this object will be hung in any of its movable containers diff --git a/code/modules/mob/living/basic/mining/hivelord_ai.dm b/code/modules/mob/living/basic/mining/hivelord_ai.dm index 3c914b2ce30..9721fc70a78 100644 --- a/code/modules/mob/living/basic/mining/hivelord_ai.dm +++ b/code/modules/mob/living/basic/mining/hivelord_ai.dm @@ -44,8 +44,8 @@ ai_movement = /datum/ai_movement/jps // Not gonna get stuck on walls now, are ya? idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( - /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/simple_find_target, ) /// Same as hivelords, but go after corpses too @@ -118,17 +118,21 @@ if(!target) // Only summon if we have a hostile target return - controller.queue_behavior(/datum/ai_behavior/summon_brood) + controller.queue_behavior(/datum/ai_behavior/summon_brood, target_key) /datum/ai_behavior/summon_brood action_cooldown = 3 SECONDS behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM required_distance = 0 -/datum/ai_behavior/summon_brood/perform(seconds_per_tick, datum/ai_controller/controller) +/datum/ai_behavior/summon_brood/perform(seconds_per_tick, datum/ai_controller/controller, target_key) . = ..() var/mob/living/basic/mining/hivelord/summoner = controller.pawn var/mob/living/basic/mining/hivelordbrood/A = new summoner.brood_type(summoner.loc) A.admin_spawned = summoner.admin_spawned A.faction = summoner.faction.Copy() + var/mob/living/target = controller.blackboard[target_key] + if(istype(target) && istype(A.ai_controller)) + A.ai_controller.set_ai_status(AI_STATUS_ON) + A.ai_controller.set_blackboard_key(target_key, target) return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index ab2dd4fa319..6f723564555 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1084,7 +1084,7 @@ var/old_level_new_clients = (registered_z ? length(SSmobs.clients_by_zlevel[registered_z]) : null) // No one is left after we're gone, shut off inactive ones if(registered_z && old_level_new_clients == 0) - for(var/datum/ai_controller/controller as anything in SSai_controllers.ai_controllers_by_zlevel[registered_z]) + for(var/datum/ai_controller/controller as anything in GLOB.ai_controllers_by_zlevel[registered_z]) controller.set_ai_status(AI_STATUS_OFF) @@ -1097,7 +1097,7 @@ if(new_level_old_clients == 0) // No one was here before, wake up all the AIs. // Basic mob AI - for(var/datum/ai_controller/controller as anything in SSai_controllers.ai_controllers_by_zlevel[new_z]) + for(var/datum/ai_controller/controller as anything in GLOB.ai_controllers_by_zlevel[new_z]) // We don't set them directly on, for instances like AIs acting while dead and other cases that may exist in the future. // This isn't a problem for AIs with a client since the client will prevent this from being called anyway. controller.set_ai_status(controller.get_expected_ai_status()) diff --git a/code/modules/tgui/modules/ai_controller_debugger.dm b/code/modules/tgui/modules/ai_controller_debugger.dm index 109a441d452..69b7760d2e0 100644 --- a/code/modules/tgui/modules/ai_controller_debugger.dm +++ b/code/modules/tgui/modules/ai_controller_debugger.dm @@ -49,11 +49,11 @@ "uid" = "[movement_target.UID()]", "source" = "[controller.movement_target_source]", ) - if(LAZYLEN(controller.current_behaviors)) + if(length(controller.current_behaviors)) for(var/datum/ai_behavior/behavior in controller.current_behaviors) data["controller"]["current_behaviors"] += "[behavior.type]" - if(LAZYLEN(controller.planned_behaviors)) + if(length(controller.planned_behaviors)) for(var/datum/ai_behavior/behavior in controller.planned_behaviors) data["controller"]["planned_behaviors"] += "[behavior.type]" diff --git a/paradise.dme b/paradise.dme index f080269edea..06dcb8fd639 100644 --- a/paradise.dme +++ b/paradise.dme @@ -239,6 +239,7 @@ #include "code\_globalvars\mapping_vars.dm" #include "code\_globalvars\misc_globals.dm" #include "code\_globalvars\traits.dm" +#include "code\_globalvars\lists\basic_ai_lists.dm" #include "code\_globalvars\lists\dye_registry.dm" #include "code\_globalvars\lists\flavor_misc.dm" #include "code\_globalvars\lists\fortunes.dm" @@ -322,6 +323,7 @@ #include "code\controllers\configuration\sections\tgui_configuration.dm" #include "code\controllers\configuration\sections\url_configuration.dm" #include "code\controllers\configuration\sections\vote_configuration.dm" +#include "code\controllers\subsystem\ai_idle_controllers.dm" #include "code\controllers\subsystem\SSacid.dm" #include "code\controllers\subsystem\SSafk.dm" #include "code\controllers\subsystem\SSai_controllers.dm" @@ -369,6 +371,8 @@ #include "code\controllers\subsystem\SSticker.dm" #include "code\controllers\subsystem\SStime_track.dm" #include "code\controllers\subsystem\SStimer.dm" +#include "code\controllers\subsystem\SSunplanned_ai_idle_controllers.dm" +#include "code\controllers\subsystem\SSunplanned_controllers.dm" #include "code\controllers\subsystem\SSverb_manager.dm" #include "code\controllers\subsystem\SSvis_overlays.dm" #include "code\controllers\subsystem\SSvote.dm" @@ -393,6 +397,7 @@ #include "code\controllers\subsystem\non_firing\SSspatial_grid.dm" #include "code\controllers\subsystem\non_firing\SStitlescreen.dm" #include "code\controllers\subsystem\processing\SSai_behaviors.dm" +#include "code\controllers\subsystem\processing\SSai_idle_behaviors.dm" #include "code\controllers\subsystem\processing\SSdcs.dm" #include "code\controllers\subsystem\processing\SSfastprocess.dm" #include "code\controllers\subsystem\processing\SSfields.dm"