Skip to content

Commit 81b3a20

Browse files
/tg/ AI controllers, part 1: core implementation. (ParadiseSS13#28065)
* /tg/ AI controllers, part 1: core implementation. * lewc review * remove unused arg * lewc review 2 * lint fix
1 parent d7c4822 commit 81b3a20

23 files changed

+1486
-14
lines changed

code/__DEFINES/ai/ai_defines.dm

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#define GET_AI_BEHAVIOR(behavior_type) SSai_behaviors.ai_behaviors[behavior_type]
2+
#define GET_TARGETING_STRATEGY(targeting_type) SSai_behaviors.targeting_strategies[targeting_type]
3+
#define HAS_AI_CONTROLLER_TYPE(thing, type) istype(thing?.ai_controller, type)
4+
5+
//AI controller flags
6+
//If you add a new status, be sure to add it to the ai_controllers subsystem's ai_controllers_by_status list.
7+
/// The AI is currently active.
8+
#define AI_STATUS_ON "ai_on"
9+
/// The AI is currently offline for any reason.
10+
#define AI_STATUS_OFF "ai_off"
11+
/// The AI is currently in idle mode.
12+
#define AI_STATUS_IDLE "ai_idle"
13+
14+
// How far should we, by default, be looking for interesting things to de-idle?
15+
#define AI_DEFAULT_INTERESTING_DIST 10
16+
17+
/// Cooldown on planning if planning failed last time
18+
19+
#define AI_FAILED_PLANNING_COOLDOWN (1.5 SECONDS)
20+
21+
/// Flags for ai_behavior new()
22+
#define AI_CONTROLLER_INCOMPATIBLE (1<<0)
23+
24+
// Return flags for ai_behavior/perform()
25+
26+
/// Update this behavior's cooldown
27+
#define AI_BEHAVIOR_DELAY (1<<0)
28+
/// Finish the behavior successfully
29+
#define AI_BEHAVIOR_SUCCEEDED (1<<1)
30+
/// Finish the behavior unsuccessfully
31+
#define AI_BEHAVIOR_FAILED (1<<2)
32+
33+
#define AI_BEHAVIOR_INSTANT (NONE)
34+
35+
/// Does this task require movement from the AI before it can be performed?
36+
#define AI_BEHAVIOR_REQUIRE_MOVEMENT (1<<0)
37+
/// Does this require the current_movement_target to be adjacent and in reach?
38+
#define AI_BEHAVIOR_REQUIRE_REACH (1<<1)
39+
/// Does this task let you perform the action while you move closer? (Things like moving and shooting)
40+
#define AI_BEHAVIOR_MOVE_AND_PERFORM (1<<2)
41+
/// Does finishing this task not null the current movement target?
42+
#define AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH (1<<3)
43+
/// Does this behavior NOT block planning?
44+
#define AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION (1<<4)
45+
46+
// AI flags
47+
48+
/// Don't move if being pulled
49+
#define AI_FLAG_STOP_MOVING_WHEN_PULLED (1<<0)
50+
/// Continue processing even if dead
51+
#define AI_FLAG_CAN_ACT_WHILE_DEAD (1<<1)
52+
/// Stop processing while in a progress bar
53+
#define AI_FLAG_PAUSE_DURING_DO_AFTER (1<<2)
54+
/// Continue processing while in stasis
55+
#define AI_FLAG_CAN_ACT_IN_STASIS (1<<3)
56+
57+
// Base Subtree defines
58+
59+
/// This subtree should cancel any further planning, (Including from other subtrees)
60+
#define SUBTREE_RETURN_FINISH_PLANNING 1
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Generic blackboard keys.
2+
// This file will get a lot larger as AI subtrees are added.
3+
4+
#define BB_CURRENT_MIN_MOVE_DISTANCE "min_move_distance"

code/__DEFINES/dcs/ai_signals.dm

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
///sent from ai controllers when a behavior is inserted into the queue: (list/new_arguments)
2+
#define AI_CONTROLLER_BEHAVIOR_QUEUED(type) "ai_controller_behavior_queued_[type]"
3+
4+
/// Signal sent when a blackboard key is set to a new value
5+
#define COMSIG_AI_BLACKBOARD_KEY_SET(blackboard_key) "ai_blackboard_key_set_[blackboard_key]"
6+
7+
///Signal sent before a blackboard key is cleared
8+
#define COMSIG_AI_BLACKBOARD_KEY_PRECLEAR(blackboard_key) "ai_blackboard_key_pre_clear_[blackboard_key]"
9+
10+
/// Signal sent when a blackboard key is cleared
11+
#define COMSIG_AI_BLACKBOARD_KEY_CLEARED(blackboard_key) "ai_blackboard_key_clear_[blackboard_key]"
12+
13+
//from base of atom/attack_basic_mob(): (/mob/user)
14+
#define COMSIG_ATOM_ATTACK_BASIC_MOB "attack_basic_mob"
15+
16+
///sent from ai controllers when they possess a pawn: (datum/ai_controller/source_controller)
17+
#define COMSIG_AI_CONTROLLER_POSSESSED_PAWN "ai_controller_possessed_pawn"
18+
///sent from ai controllers when they pick behaviors: (list/datum/ai_behavior/old_behaviors, list/datum/ai_behavior/new_behaviors)
19+
#define COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS "ai_controller_picked_behaviors"

code/__DEFINES/subsystems.dm

+16-12
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,20 @@
5050
#define INIT_ORDER_PROFILER 101
5151
#define INIT_ORDER_QUEUE 100 // Load this quickly so people cant queue skip
5252
#define INIT_ORDER_TITLE 99 // Load this quickly so people dont see a blank lobby screen
53-
#define INIT_ORDER_GARBAGE 22
54-
#define INIT_ORDER_DBCORE 21
55-
#define INIT_ORDER_REDIS 20 // Make sure we dont miss any events
56-
#define INIT_ORDER_BLACKBOX 19
57-
#define INIT_ORDER_CLEANUP 18
58-
#define INIT_ORDER_INPUT 17
59-
#define INIT_ORDER_SOUNDS 16
60-
#define INIT_ORDER_INSTRUMENTS 15
61-
#define INIT_ORDER_RESEARCH 14 // SoonTM
62-
#define INIT_ORDER_STATION 13 //This is high priority because it manipulates a lot of the subsystems that will initialize after it.
63-
#define INIT_ORDER_EVENTS 12
64-
#define INIT_ORDER_JOBS 11
53+
#define INIT_ORDER_GARBAGE 24
54+
#define INIT_ORDER_DBCORE 23
55+
#define INIT_ORDER_REDIS 22 // Make sure we dont miss any events
56+
#define INIT_ORDER_BLACKBOX 21
57+
#define INIT_ORDER_CLEANUP 20
58+
#define INIT_ORDER_INPUT 19
59+
#define INIT_ORDER_SOUNDS 18
60+
#define INIT_ORDER_INSTRUMENTS 17
61+
#define INIT_ORDER_RESEARCH 16 // SoonTM
62+
#define INIT_ORDER_STATION 15 //This is high priority because it manipulates a lot of the subsystems that will initialize after it.
63+
#define INIT_ORDER_EVENTS 14
64+
#define INIT_ORDER_JOBS 13
65+
#define INIT_ORDER_AI_MOVEMENT 12
66+
#define INIT_ORDER_AI_CONTROLLERS 11
6567
#define INIT_ORDER_TICKER 10
6668
#define INIT_ORDER_MAPPING 9
6769
#define INIT_ORDER_EARLY_ASSETS 8
@@ -102,6 +104,8 @@
102104
#define FIRE_PRIORITY_AIR 20
103105
#define FIRE_PRIORITY_NPC 20
104106
#define FIRE_PRIORITY_CAMERA 20
107+
#define FIRE_PRIORITY_NPC_MOVEMENT 21
108+
#define FIRE_PRIORITY_NPC_ACTIONS 22
105109
#define FIRE_PRIORITY_PATHFINDING 23
106110
#define FIRE_PRIORITY_PROCESS 25
107111
#define FIRE_PRIORITY_THROWING 25

code/__HELPERS/trait_helpers.dm

+3
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
255255
#define TRAIT_CRYO_DESPAWNING "cryo_despawning" // dont adminbus this please
256256
#define TRAIT_EXAMINE_HALLUCINATING "examine_hallucinating"
257257

258+
/// Trait that prevents AI controllers from planning detached from ai_status to prevent weird state stuff.
259+
#define TRAIT_AI_PAUSED "trait_ai_paused"
260+
258261
//***** MIND TRAITS *****/
259262
#define TRAIT_HOLY "is_holy" // The mob is holy in regards to religion
260263
#define TRAIT_TABLE_LEAP "table_leap" // Lets bartender and chef mount tables faster

code/_onclick/click.dm

+2-2
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@
183183
continue
184184

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

189189
closed[target] = TRUE
@@ -206,7 +206,7 @@
206206
/mob/living/direct_access(atom/target)
207207
return ..() + get_contents()
208208

209-
/proc/CheckToolReach(atom/movable/here, atom/movable/there, reach)
209+
/proc/check_tool_reach(atom/movable/here, atom/movable/there, reach)
210210
if(!here || !there)
211211
return FALSE
212212
switch(reach)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/// The subsystem used to tick [/datum/ai_controllers] instances. Handling the re-checking of plans.
2+
SUBSYSTEM_DEF(ai_controllers)
3+
name = "AI Controller Ticker"
4+
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND
5+
priority = FIRE_PRIORITY_NPC
6+
init_order = INIT_ORDER_AI_CONTROLLERS
7+
wait = 0.5 SECONDS //Plan every half second if required, not great not terrible.
8+
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
9+
10+
///List of all ai_subtree singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_subtrees()
11+
var/list/datum/ai_planning_subtree/ai_subtrees = list()
12+
///Assoc List of all AI statuses and all AI controllers with that status.
13+
var/list/ai_controllers_by_status = list(
14+
AI_STATUS_ON = list(),
15+
AI_STATUS_OFF = list(),
16+
AI_STATUS_IDLE = list(),
17+
)
18+
///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.
19+
var/list/ai_controllers_by_zlevel = list()
20+
/// The tick cost of all active AI, calculated on fire.
21+
var/cost_on
22+
/// The tick cost of all idle AI, calculated on fire.
23+
var/cost_idle
24+
25+
26+
/datum/controller/subsystem/ai_controllers/Initialize()
27+
setup_subtrees()
28+
29+
/datum/controller/subsystem/ai_controllers/stat_entry(msg)
30+
var/list/active_list = ai_controllers_by_status[AI_STATUS_ON]
31+
var/list/inactive_list = ai_controllers_by_status[AI_STATUS_OFF]
32+
var/list/idle_list = ai_controllers_by_status[AI_STATUS_IDLE]
33+
msg = "Active AIs:[length(active_list)]/[round(cost_on,1)]%|Inactive:[length(inactive_list)]|Idle:[length(idle_list)]/[round(cost_idle,1)]%"
34+
return ..()
35+
36+
/datum/controller/subsystem/ai_controllers/fire(resumed)
37+
var/timer = TICK_USAGE_REAL
38+
cost_idle = MC_AVERAGE(cost_idle, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))
39+
40+
timer = TICK_USAGE_REAL
41+
for(var/datum/ai_controller/ai_controller as anything in ai_controllers_by_status[AI_STATUS_ON])
42+
if(!COOLDOWN_FINISHED(ai_controller, failed_planning_cooldown))
43+
continue
44+
45+
if(!ai_controller.able_to_plan())
46+
continue
47+
ai_controller.select_behaviors(wait / (1 SECONDS))
48+
if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan
49+
COOLDOWN_START(ai_controller, failed_planning_cooldown, AI_FAILED_PLANNING_COOLDOWN)
50+
51+
cost_on = MC_AVERAGE(cost_on, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))
52+
53+
///Creates all instances of ai_subtrees and assigns them to the ai_subtrees list.
54+
/datum/controller/subsystem/ai_controllers/proc/setup_subtrees()
55+
for(var/subtree_type in subtypesof(/datum/ai_planning_subtree))
56+
var/datum/ai_planning_subtree/subtree = new subtree_type
57+
ai_subtrees[subtree_type] = subtree
58+
59+
///Called when the max Z level was changed, updating our coverage.
60+
/datum/controller/subsystem/ai_controllers/proc/on_max_z_changed()
61+
if(!islist(ai_controllers_by_zlevel))
62+
ai_controllers_by_zlevel = new /list(world.maxz, 0)
63+
while(SSai_controllers.ai_controllers_by_zlevel.len < world.maxz)
64+
SSai_controllers.ai_controllers_by_zlevel.len++
65+
SSai_controllers.ai_controllers_by_zlevel[ai_controllers_by_zlevel.len] = list()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// The subsystem used to tick [/datum/ai_movement] instances. Handling the movement of individual AI instances
2+
MOVEMENT_SUBSYSTEM_DEF(ai_movement)
3+
name = "AI movement"
4+
flags = SS_BACKGROUND|SS_TICKER
5+
priority = FIRE_PRIORITY_NPC_MOVEMENT
6+
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
7+
init_order = INIT_ORDER_AI_MOVEMENT
8+
9+
///an assoc list of all ai_movement types. Assoc type to instance
10+
var/list/movement_types
11+
12+
/datum/controller/subsystem/movement/ai_movement/Initialize()
13+
setup_ai_movement_instances()
14+
15+
/datum/controller/subsystem/movement/ai_movement/proc/setup_ai_movement_instances()
16+
movement_types = list()
17+
for(var/key as anything in subtypesof(/datum/ai_movement))
18+
var/datum/ai_movement/ai_movement = new key
19+
movement_types[key] = ai_movement
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// The subsystem used to tick [/datum/ai_behavior] instances.
2+
/// Handling the individual actions an AI can take like punching someone in the fucking NUTS
3+
PROCESSING_SUBSYSTEM_DEF(ai_behaviors)
4+
name = "AI Behavior Ticker"
5+
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND
6+
priority = FIRE_PRIORITY_NPC_ACTIONS
7+
runlevels = RUNLEVEL_GAME|RUNLEVEL_POSTGAME
8+
init_order = INIT_ORDER_AI_CONTROLLERS
9+
wait = 1
10+
/// List of all ai_behavior singletons, key is the typepath while assigned
11+
/// value is a newly created instance of the typepath. See setup_ai_behaviors().
12+
var/list/ai_behaviors
13+
/// List of all targeting_strategy singletons, key is the typepath while assigned
14+
/// value is a newly created instance of the typepath. See setup_targeting_strats().
15+
var/list/targeting_strategies
16+
17+
/datum/controller/subsystem/processing/ai_behaviors/Initialize()
18+
setup_ai_behaviors()
19+
setup_targeting_strats()
20+
21+
/datum/controller/subsystem/processing/ai_behaviors/proc/setup_ai_behaviors()
22+
ai_behaviors = list()
23+
for(var/behavior_type in subtypesof(/datum/ai_behavior))
24+
var/datum/ai_behavior/ai_behavior = new behavior_type
25+
ai_behaviors[behavior_type] = ai_behavior
26+
27+
/datum/controller/subsystem/processing/ai_behaviors/proc/setup_targeting_strats()
28+
targeting_strategies = list()
29+
for(var/target_type in subtypesof(/datum/targeting_strategy))
30+
var/datum/targeting_strategy/target_start = new target_type
31+
targeting_strategies[target_type] = target_start

code/datums/ai/ai_behavior.dm

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/// Abstract class for an action an AI can take. Can range from movement to grabbing a nearby weapon.
2+
/datum/ai_behavior
3+
/// What distance you need to be from the target to perform the action.
4+
var/required_distance = 1
5+
/// Flags for extra behavior
6+
var/behavior_flags = NONE
7+
/// Cooldown between actions performances, defaults to the value of
8+
/// CLICK_CD_MELEE because that seemed like a nice standard for the speed of
9+
/// AI behavior
10+
var/action_cooldown = CLICK_CD_MELEE
11+
12+
/// Called by the AI controller when first being added. Additional arguments
13+
/// depend on the behavior type. For example, if the behavior involves attacking
14+
/// a mob, you may require an argument naming the blackboard key which points to
15+
/// the target. Return FALSE to cancel.
16+
/datum/ai_behavior/proc/setup(datum/ai_controller/controller, ...)
17+
return TRUE
18+
19+
/// Returns the delay to use for this behavior in the moment. The default
20+
/// behavior cooldown is `CLICK_CD_MELEE`, but can be customized; for example,
21+
/// you may want a mob crawling through vents to move slowly and at a random
22+
/// pace between pipes.
23+
/datum/ai_behavior/proc/get_cooldown(datum/ai_controller/cooldown_for)
24+
return action_cooldown
25+
26+
/// Called by the AI controller when this action is performed. This will
27+
/// typically require consulting the blackboard for information on the specific
28+
/// actions desired from this behavior, by passing the relevant blackboard data
29+
/// keys to this proc. Returns a combination of [AI_BEHAVIOR_DELAY] or
30+
/// [AI_BEHAVIOR_INSTANT], determining whether or not a cooldown occurs, and
31+
/// [AI_BEHAVIOR_SUCCEEDED] or [AI_BEHAVIOR_FAILED]. The behavior's
32+
/// `finish_action` proc is given TRUE or FALSE depending on whether or not the
33+
/// return value of `perform` is marked as successful or unsuccessful.
34+
/datum/ai_behavior/proc/perform(seconds_per_tick, datum/ai_controller/controller, ...)
35+
controller.behavior_cooldowns[src] = world.time + action_cooldown
36+
37+
/// Called when the action is finished. This needs the same args as `perform`
38+
/// besides the default ones. This should be used to clear up the blackboard of
39+
/// any unnecessary or obsolete data, and update the state of the pawn if
40+
/// necessary once we know whether or not the AI action was successful.
41+
/// `succeeded` is `TRUE` or `FALSE` depending on whether
42+
/// [/datum/ai_behavior/proc/perform] returns [AI_BEHAVIOR_SUCCEEDED] or
43+
/// [AI_BEHAVIOR_FAILED].
44+
/datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded, ...)
45+
LAZYREMOVE(controller.current_behaviors, src)
46+
controller.behavior_args -= type
47+
// If this was a movement task, reset our movement target if necessary
48+
if(!(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT))
49+
return
50+
if(behavior_flags & AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH)
51+
return
52+
clear_movement_target(controller)
53+
controller.ai_movement.stop_moving_towards(controller)
54+
55+
/// Helper proc to ensure consistency in setting the source of the movement target
56+
/datum/ai_behavior/proc/set_movement_target(datum/ai_controller/controller, atom/target, datum/ai_movement/new_movement)
57+
controller.set_movement_target(type, target, new_movement)
58+
59+
/// Clear the controller's movement target only if it was us who last set it
60+
/datum/ai_behavior/proc/clear_movement_target(datum/ai_controller/controller)
61+
if(controller.movement_target_source != type)
62+
return
63+
controller.set_movement_target(type, null)

0 commit comments

Comments
 (0)