Skip to content

Commit

Permalink
/tg/ AI controllers, part 1: core implementation. (#28065)
Browse files Browse the repository at this point in the history
* /tg/ AI controllers, part 1: core implementation.

* lewc review

* remove unused arg

* lewc review 2

* lint fix
  • Loading branch information
warriorstar-orion authored Feb 12, 2025
1 parent d7c4822 commit 81b3a20
Show file tree
Hide file tree
Showing 23 changed files with 1,486 additions and 14 deletions.
60 changes: 60 additions & 0 deletions code/__DEFINES/ai/ai_defines.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#define GET_AI_BEHAVIOR(behavior_type) SSai_behaviors.ai_behaviors[behavior_type]
#define GET_TARGETING_STRATEGY(targeting_type) SSai_behaviors.targeting_strategies[targeting_type]
#define HAS_AI_CONTROLLER_TYPE(thing, type) istype(thing?.ai_controller, type)

//AI controller flags
//If you add a new status, be sure to add it to the ai_controllers subsystem's ai_controllers_by_status list.
/// The AI is currently active.
#define AI_STATUS_ON "ai_on"
/// The AI is currently offline for any reason.
#define AI_STATUS_OFF "ai_off"
/// The AI is currently in idle mode.
#define AI_STATUS_IDLE "ai_idle"

// How far should we, by default, be looking for interesting things to de-idle?
#define AI_DEFAULT_INTERESTING_DIST 10

/// Cooldown on planning if planning failed last time

#define AI_FAILED_PLANNING_COOLDOWN (1.5 SECONDS)

/// Flags for ai_behavior new()
#define AI_CONTROLLER_INCOMPATIBLE (1<<0)

// Return flags for ai_behavior/perform()

/// Update this behavior's cooldown
#define AI_BEHAVIOR_DELAY (1<<0)
/// Finish the behavior successfully
#define AI_BEHAVIOR_SUCCEEDED (1<<1)
/// Finish the behavior unsuccessfully
#define AI_BEHAVIOR_FAILED (1<<2)

#define AI_BEHAVIOR_INSTANT (NONE)

/// Does this task require movement from the AI before it can be performed?
#define AI_BEHAVIOR_REQUIRE_MOVEMENT (1<<0)
/// Does this require the current_movement_target to be adjacent and in reach?
#define AI_BEHAVIOR_REQUIRE_REACH (1<<1)
/// Does this task let you perform the action while you move closer? (Things like moving and shooting)
#define AI_BEHAVIOR_MOVE_AND_PERFORM (1<<2)
/// Does finishing this task not null the current movement target?
#define AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH (1<<3)
/// Does this behavior NOT block planning?
#define AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION (1<<4)

// AI flags

/// Don't move if being pulled
#define AI_FLAG_STOP_MOVING_WHEN_PULLED (1<<0)
/// Continue processing even if dead
#define AI_FLAG_CAN_ACT_WHILE_DEAD (1<<1)
/// Stop processing while in a progress bar
#define AI_FLAG_PAUSE_DURING_DO_AFTER (1<<2)
/// Continue processing while in stasis
#define AI_FLAG_CAN_ACT_IN_STASIS (1<<3)

// Base Subtree defines

/// This subtree should cancel any further planning, (Including from other subtrees)
#define SUBTREE_RETURN_FINISH_PLANNING 1
4 changes: 4 additions & 0 deletions code/__DEFINES/ai/blackboard_defines.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generic blackboard keys.
// This file will get a lot larger as AI subtrees are added.

#define BB_CURRENT_MIN_MOVE_DISTANCE "min_move_distance"
19 changes: 19 additions & 0 deletions code/__DEFINES/dcs/ai_signals.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
///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]"

/// Signal sent when a blackboard key is set to a new value
#define COMSIG_AI_BLACKBOARD_KEY_SET(blackboard_key) "ai_blackboard_key_set_[blackboard_key]"

///Signal sent before a blackboard key is cleared
#define COMSIG_AI_BLACKBOARD_KEY_PRECLEAR(blackboard_key) "ai_blackboard_key_pre_clear_[blackboard_key]"

/// Signal sent when a blackboard key is cleared
#define COMSIG_AI_BLACKBOARD_KEY_CLEARED(blackboard_key) "ai_blackboard_key_clear_[blackboard_key]"

//from base of atom/attack_basic_mob(): (/mob/user)
#define COMSIG_ATOM_ATTACK_BASIC_MOB "attack_basic_mob"

///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"
28 changes: 16 additions & 12 deletions code/__DEFINES/subsystems.dm
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,20 @@
#define INIT_ORDER_PROFILER 101
#define INIT_ORDER_QUEUE 100 // Load this quickly so people cant queue skip
#define INIT_ORDER_TITLE 99 // Load this quickly so people dont see a blank lobby screen
#define INIT_ORDER_GARBAGE 22
#define INIT_ORDER_DBCORE 21
#define INIT_ORDER_REDIS 20 // Make sure we dont miss any events
#define INIT_ORDER_BLACKBOX 19
#define INIT_ORDER_CLEANUP 18
#define INIT_ORDER_INPUT 17
#define INIT_ORDER_SOUNDS 16
#define INIT_ORDER_INSTRUMENTS 15
#define INIT_ORDER_RESEARCH 14 // SoonTM
#define INIT_ORDER_STATION 13 //This is high priority because it manipulates a lot of the subsystems that will initialize after it.
#define INIT_ORDER_EVENTS 12
#define INIT_ORDER_JOBS 11
#define INIT_ORDER_GARBAGE 24
#define INIT_ORDER_DBCORE 23
#define INIT_ORDER_REDIS 22 // Make sure we dont miss any events
#define INIT_ORDER_BLACKBOX 21
#define INIT_ORDER_CLEANUP 20
#define INIT_ORDER_INPUT 19
#define INIT_ORDER_SOUNDS 18
#define INIT_ORDER_INSTRUMENTS 17
#define INIT_ORDER_RESEARCH 16 // SoonTM
#define INIT_ORDER_STATION 15 //This is high priority because it manipulates a lot of the subsystems that will initialize after it.
#define INIT_ORDER_EVENTS 14
#define INIT_ORDER_JOBS 13
#define INIT_ORDER_AI_MOVEMENT 12
#define INIT_ORDER_AI_CONTROLLERS 11
#define INIT_ORDER_TICKER 10
#define INIT_ORDER_MAPPING 9
#define INIT_ORDER_EARLY_ASSETS 8
Expand Down Expand Up @@ -102,6 +104,8 @@
#define FIRE_PRIORITY_AIR 20
#define FIRE_PRIORITY_NPC 20
#define FIRE_PRIORITY_CAMERA 20
#define FIRE_PRIORITY_NPC_MOVEMENT 21
#define FIRE_PRIORITY_NPC_ACTIONS 22
#define FIRE_PRIORITY_PATHFINDING 23
#define FIRE_PRIORITY_PROCESS 25
#define FIRE_PRIORITY_THROWING 25
Expand Down
3 changes: 3 additions & 0 deletions code/__HELPERS/trait_helpers.dm
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_CRYO_DESPAWNING "cryo_despawning" // dont adminbus this please
#define TRAIT_EXAMINE_HALLUCINATING "examine_hallucinating"

/// Trait that prevents AI controllers from planning detached from ai_status to prevent weird state stuff.
#define TRAIT_AI_PAUSED "trait_ai_paused"

//***** MIND TRAITS *****/
#define TRAIT_HOLY "is_holy" // The mob is holy in regards to religion
#define TRAIT_TABLE_LEAP "table_leap" // Lets bartender and chef mount tables faster
Expand Down
4 changes: 2 additions & 2 deletions code/_onclick/click.dm
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
continue

if(isturf(target) || isturf(target.loc) || (target in direct_access) || !(target.IsObscured()) || istype(target.loc, /obj/item/storage)) //Directly accessible atoms
if(target.Adjacent(src) || (tool && CheckToolReach(src, target, tool.reach))) //Adjacent or reaching attacks
if(target.Adjacent(src) || (tool && check_tool_reach(src, target, tool.reach))) //Adjacent or reaching attacks
return TRUE

closed[target] = TRUE
Expand All @@ -206,7 +206,7 @@
/mob/living/direct_access(atom/target)
return ..() + get_contents()

/proc/CheckToolReach(atom/movable/here, atom/movable/there, reach)
/proc/check_tool_reach(atom/movable/here, atom/movable/there, reach)
if(!here || !there)
return FALSE
switch(reach)
Expand Down
65 changes: 65 additions & 0 deletions code/controllers/subsystem/SSai_controllers.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/// The subsystem used to tick [/datum/ai_controllers] instances. Handling the re-checking of plans.
SUBSYSTEM_DEF(ai_controllers)
name = "AI Controller Ticker"
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND
priority = FIRE_PRIORITY_NPC
init_order = INIT_ORDER_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()
/// 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


/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)]%"
return ..()

/datum/controller/subsystem/ai_controllers/fire(resumed)
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(!COOLDOWN_FINISHED(ai_controller, failed_planning_cooldown))
continue

if(!ai_controller.able_to_plan())
continue
ai_controller.select_behaviors(wait / (1 SECONDS))
if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan
COOLDOWN_START(ai_controller, failed_planning_cooldown, AI_FAILED_PLANNING_COOLDOWN)

cost_on = MC_AVERAGE(cost_on, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))

///Creates all instances of ai_subtrees and assigns them to the ai_subtrees list.
/datum/controller/subsystem/ai_controllers/proc/setup_subtrees()
for(var/subtree_type in subtypesof(/datum/ai_planning_subtree))
var/datum/ai_planning_subtree/subtree = new subtree_type
ai_subtrees[subtree_type] = subtree

///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()
19 changes: 19 additions & 0 deletions code/controllers/subsystem/movement/SSai_movement.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// The subsystem used to tick [/datum/ai_movement] instances. Handling the movement of individual AI instances
MOVEMENT_SUBSYSTEM_DEF(ai_movement)
name = "AI movement"
flags = SS_BACKGROUND|SS_TICKER
priority = FIRE_PRIORITY_NPC_MOVEMENT
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
init_order = INIT_ORDER_AI_MOVEMENT

///an assoc list of all ai_movement types. Assoc type to instance
var/list/movement_types

/datum/controller/subsystem/movement/ai_movement/Initialize()
setup_ai_movement_instances()

/datum/controller/subsystem/movement/ai_movement/proc/setup_ai_movement_instances()
movement_types = list()
for(var/key as anything in subtypesof(/datum/ai_movement))
var/datum/ai_movement/ai_movement = new key
movement_types[key] = ai_movement
31 changes: 31 additions & 0 deletions code/controllers/subsystem/processing/SSai_behaviors.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// The subsystem used to tick [/datum/ai_behavior] instances.
/// Handling the individual actions an AI can take like punching someone in the fucking NUTS
PROCESSING_SUBSYSTEM_DEF(ai_behaviors)
name = "AI Behavior Ticker"
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND
priority = FIRE_PRIORITY_NPC_ACTIONS
runlevels = RUNLEVEL_GAME|RUNLEVEL_POSTGAME
init_order = INIT_ORDER_AI_CONTROLLERS
wait = 1
/// List of all ai_behavior singletons, key is the typepath while assigned
/// value is a newly created instance of the typepath. See setup_ai_behaviors().
var/list/ai_behaviors
/// List of all targeting_strategy singletons, key is the typepath while assigned
/// value is a newly created instance of the typepath. See setup_targeting_strats().
var/list/targeting_strategies

/datum/controller/subsystem/processing/ai_behaviors/Initialize()
setup_ai_behaviors()
setup_targeting_strats()

/datum/controller/subsystem/processing/ai_behaviors/proc/setup_ai_behaviors()
ai_behaviors = list()
for(var/behavior_type in subtypesof(/datum/ai_behavior))
var/datum/ai_behavior/ai_behavior = new behavior_type
ai_behaviors[behavior_type] = ai_behavior

/datum/controller/subsystem/processing/ai_behaviors/proc/setup_targeting_strats()
targeting_strategies = list()
for(var/target_type in subtypesof(/datum/targeting_strategy))
var/datum/targeting_strategy/target_start = new target_type
targeting_strategies[target_type] = target_start
63 changes: 63 additions & 0 deletions code/datums/ai/ai_behavior.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// Abstract class for an action an AI can take. Can range from movement to grabbing a nearby weapon.
/datum/ai_behavior
/// What distance you need to be from the target to perform the action.
var/required_distance = 1
/// 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
var/action_cooldown = CLICK_CD_MELEE

/// Called by the AI controller when first being added. Additional arguments
/// depend on the behavior type. For example, if the behavior involves attacking
/// a mob, you may require an argument naming the blackboard key which points to
/// the target. Return FALSE to cancel.
/datum/ai_behavior/proc/setup(datum/ai_controller/controller, ...)
return TRUE

/// Returns the delay to use for this behavior in the moment. The default
/// behavior cooldown is `CLICK_CD_MELEE`, but can be customized; for example,
/// you may want a mob crawling through vents to move slowly and at a random
/// pace between pipes.
/datum/ai_behavior/proc/get_cooldown(datum/ai_controller/cooldown_for)
return action_cooldown

/// Called by the AI controller when this action is performed. This will
/// typically require consulting the blackboard for information on the specific
/// actions desired from this behavior, by passing the relevant blackboard data
/// keys to this proc. Returns a combination of [AI_BEHAVIOR_DELAY] or
/// [AI_BEHAVIOR_INSTANT], determining whether or not a cooldown occurs, and
/// [AI_BEHAVIOR_SUCCEEDED] or [AI_BEHAVIOR_FAILED]. The behavior's
/// `finish_action` proc is given TRUE or FALSE depending on whether or not the
/// return value of `perform` is marked as successful or unsuccessful.
/datum/ai_behavior/proc/perform(seconds_per_tick, datum/ai_controller/controller, ...)
controller.behavior_cooldowns[src] = world.time + action_cooldown

/// Called when the action is finished. This needs the same args as `perform`
/// besides the default ones. This should be used to clear up the blackboard of
/// any unnecessary or obsolete data, and update the state of the pawn if
/// necessary once we know whether or not the AI action was successful.
/// `succeeded` is `TRUE` or `FALSE` depending on whether
/// [/datum/ai_behavior/proc/perform] returns [AI_BEHAVIOR_SUCCEEDED] or
/// [AI_BEHAVIOR_FAILED].
/datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded, ...)
LAZYREMOVE(controller.current_behaviors, src)
controller.behavior_args -= type
// If this was a movement task, reset our movement target if necessary
if(!(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT))
return
if(behavior_flags & AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH)
return
clear_movement_target(controller)
controller.ai_movement.stop_moving_towards(controller)

/// Helper proc to ensure consistency in setting the source of the movement target
/datum/ai_behavior/proc/set_movement_target(datum/ai_controller/controller, atom/target, datum/ai_movement/new_movement)
controller.set_movement_target(type, target, new_movement)

/// Clear the controller's movement target only if it was us who last set it
/datum/ai_behavior/proc/clear_movement_target(datum/ai_controller/controller)
if(controller.movement_target_source != type)
return
controller.set_movement_target(type, null)
Loading

0 comments on commit 81b3a20

Please sign in to comment.