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 <orion@snowfrost.garden>

* 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 <orion@snowfrost.garden>
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>
This commit is contained in:
warriorstar-orion
2025-12-09 23:46:38 +00:00
committed by GitHub
co-authored by DGamerL PollardTheDragon Burzah
parent 9e87289bd8
commit be48d7598f
17 changed files with 351 additions and 127 deletions
+18
View File
@@ -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
+2
View File
@@ -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"
+1
View File
@@ -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
+18
View File
@@ -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(),
))
+29 -34
View File
@@ -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()
@@ -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
@@ -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
@@ -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
@@ -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
+1 -1
View File
@@ -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))
+210 -84
View File
@@ -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)
+1 -1
View File
@@ -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.
+3
View File
@@ -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
@@ -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
+2 -2
View File
@@ -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())
@@ -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]"
+5
View File
@@ -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"