mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 20:48:56 +01:00
This PR replaces our current NPC AI with a [behavior tree system](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)). Behavior trees are a common way of creating AI in which you place nodes in a tree structure to define what actions an AI should take. AI controllers defined a list of /datum/ai_planning_subtree types in behavior_nodes. Each subtree was a self-contained unit that could call queue_behavior() to fire off /datum/ai_behavior actions. The controller iterated subtrees in order, each one deciding independently whether to queue something and deciding whether the next subtree would run. This has a few issues: 1. There's no real structure; you are just defining a list of things to try in order. 2. There was a loooot of subtrees that were basically the same as another but with some slight modification 3. It was hard to understand. Controllers now define a single json file describing a tree of nodes. The tree is composed of structural composites: Sequence - do A, then B, then C (and so on) Selector - try A, if it fails try B, then C (and so on) Parallel - run A and B simultaneously, with configurable failure/success policies and or looping behavior Subplan - loop a child continiously Along that we also have "Decorators". These are nodes that basically check a condition (E.g.; do we have a combat target). These decorators can be used to gate behavior and are re-useable across behavior trees. They also have a concept known as "Observers". Which lets them cancel lower priority behavior in case their condition changes (Which we check whenever a signal fires that fits that specific decorator). This makes the AI much more responsive to change in environment. For behaviors, we still use the ai_behavior datums. These are the actual behaviors such as "Move to X", "Attack X". The only major change is that these can no longer sleep() since they now run in the ai_controller. Lastly, we now also have subtrees, except now they are essentially pieces of behavior tree that can be re-used, or even overriden at runtime or as a variable. Allowing for making modular AI made out of several smaller trees. You can set variables on these nodes directly via the extension (see below), which should reduce the need to make subtypes of behaviors by a lot. All of these vars are saved on the JSON and will be applied at runtime. If you are using subtrees, you can also assign "bindings" to these variables, which will allow instances of the subtree to override those variables. Since a tree structure with variables becomes hard to parse in a JSON, I've made a VSCode extension to edit these JSONs: https://marketplace.visualstudio.com/items?itemName=BehaviorTreeG.behaviortreeg https://github.com/CabinetOnFire/BehaviorTreeG <img width="1795" height="1268" alt="image" src="https://github.com/user-attachments/assets/56aa2f0b-3cf9-449f-bca4-8281fca82db6" /> This extension allows you to edit the behavior tree JSONs, and browse through all the behaviors/decorators/subtrees we have If you'd like more info on how to build these AI check out the learn_ai.md. I will also make a tutorial to go over more depth on what the system offers because I kind of suck at doing technical write-ups. Targetting has been changed to. I've made a new acquire_targets behavior that takes a target_source (what am I targetting) and targetting_strategy (what does the candidate need to fulfill to be considered a target). This allows us to make composites targetting combinations to reduce the amount of specific find_and_set esque behaviors we had before. Not everything is ported to this system but that would be a longer term goal. I've added a new build_bt script that converts all the behavior tree JSONs into compiled versions. Why is this needed? Because I wanted to keep using defines in behavior trees, so we need a way to convert this into literal values before we send it to DM. This script runs on compile and should also run in CI (If I didn't fuck that up!). This saves to a new build/ folder. I've ported every single AI in the game to this system (except raptors, Kobsa is working on those so should be in soon!), so I do expect some bugs to come out of this. But I also fixed some issues that have probably been in the game for a long time such as: - Fixed penguins being unable to fish - Fixed bileworms not being able to devour people - Fixes goldgrubs not grubbing gold (they could not mine!) - Lizards actually eat food they find Either way, I'd reccomend a long TM on this. 1. (Hopefully) a better development experience for making AI 2. Less copy-paste for behaviors, we should be able to re-use more pieces to make behavior 3. Behavior trees is a more common pattern in making AI, so it should be easier to find resources to find out how to do things. 🆑 CabinetOnFire, Iamgoofball, SmartKar, Ben10omintrix refactor: Replaces our AI system with behavior trees, porting all datum/ai to it /🆑 I will add this PR with more details down the line. I think I got the big picture but its a big PR, so sorry if I missed something important. --------- Co-authored-by: Iamgoofball <iamgoofball@gmail.com> Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com> Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com> Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
1101 lines
42 KiB
Plaintext
1101 lines
42 KiB
Plaintext
/*
|
|
AI controllers are a datumized form of AI that simulates the input a player would otherwise give to a atom. What this means is that these datums
|
|
have ways of interacting with a specific atom and control it. They posses a blackboard with the information the AI knows and has, and will plan behaviors it will try to execute through
|
|
multiple modular subtrees with behaviors
|
|
*/
|
|
|
|
/datum/ai_controller
|
|
///The atom this controller is controlling
|
|
var/atom/pawn
|
|
/**
|
|
* This is a list of variables the AI uses and can be mutated by actions.
|
|
*
|
|
* When an action is performed you pass this list and any relevant keys for the variables it can mutate.
|
|
*
|
|
* DO NOT set values in the blackboard directly, and especially not if you're adding a datum reference to this!
|
|
* Use the setters, this is important for reference handing.
|
|
*/
|
|
var/alist/blackboard = alist()
|
|
|
|
///Bitfield of traits for this AI to handle extra behavior
|
|
var/ai_traits = DEFAULT_AI_FLAGS
|
|
///Current status of AI (OFF/ON)
|
|
var/ai_status
|
|
///Set by force_ai_off() when an outside system deliberately disables this AI. While TRUE, get_expected_ai_status() always returns AI_STATUS_OFF, so status recalculations (stat changes, z changes, client login/logout) cannot re-enable us. Cleared via clear_forced_off().
|
|
var/forced_off = FALSE
|
|
///Tracks recent pathing attempts, if we fail too many in a row we fail our current plans.
|
|
var/consecutive_pathing_attempts
|
|
///Can the AI remain in control if there is a client?
|
|
var/continue_processing_when_client = FALSE
|
|
///distance to give up on target
|
|
var/max_target_distance = 14
|
|
/// Repo-relative path to the .bt.json source file for this controller (e.g. "code/datums/ai/basic_mobs/cleanbot.bt.json").
|
|
/// initialize_behavior_tree() derives the compiled path from this and loads the BT tree at runtime.
|
|
var/behavior_tree_json = null
|
|
///The root of our tree, which will contain
|
|
var/list/behavior_nodes
|
|
/// Execution index of the leaf node currently returning BT_RUNNING. 0 = nothing active.
|
|
var/active_execution_index = 0
|
|
/// Set to TRUE by cancel_current_plan() when it fires mid-tick. Checked by composites to abort the current tick loop early, preventing running_child_index from being re-established after a reset. Cleared at the start of SelectBehaviors().
|
|
var/cancelled_during_tick = FALSE
|
|
/// Draining log of all leaf execution indices that fired since the last bt_viewer poll. Null when no viewer is attached.
|
|
var/list/bt_execution_log = null
|
|
/// assoc list of override_id -> /datum/bt_node/subtree for runtime subtree replacement.
|
|
/// Populated by finalize_tree() when subtrees with override_id are found. Null until then.
|
|
var/list/override_slots = null
|
|
/// Decorators in polling mode (observer_abort set, no signal registered). Iterated after each SelectBehaviors tick so their condition is re-evaluated even when skipped by composite resume logic.
|
|
var/list/polling_observers = null
|
|
/// world.time of our last SelectBehaviors() tick from SSai_controllers. Used to derive the real seconds_per_tick under load; 0 means no tick since the last status change, so the first tick falls back to the subsystem wait.
|
|
var/last_bt_tick = 0
|
|
///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.
|
|
var/datum/ai_movement/ai_movement = /datum/ai_movement/dumb
|
|
///Delay between movements. This is on the controller so we can keep the movement datum singleton
|
|
var/movement_delay = 0.1 SECONDS
|
|
|
|
// The variables below are fucking stupid and should be put into the blackboard at some point.
|
|
///AI paused time
|
|
var/paused_until = 0
|
|
///What distance should we be checking for interesting things when considering idling/deidling? Defaults to AI_DEFAULT_INTERESTING_DIST
|
|
var/interesting_dist = AI_DEFAULT_INTERESTING_DIST
|
|
/// TRUE if we're able to run, FALSE if we aren't
|
|
/// Should not be set manually, override get_able_to_run() instead
|
|
/// 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
|
|
|
|
/datum/ai_controller/New(atom/new_pawn)
|
|
change_ai_movement_type(ai_movement)
|
|
initialize_behavior_tree()
|
|
|
|
if(!isnull(new_pawn)) // unit tests need the ai_controller to exist in isolation due to list schenanigans i hate it here
|
|
PossessPawn(new_pawn)
|
|
|
|
/datum/ai_controller/Destroy(force)
|
|
UnpossessPawn(FALSE)
|
|
if(ai_status)
|
|
SSai_controllers.ai_controllers_by_status[ai_status] -= src
|
|
for(var/datum/controller/subsystem/ai_controllers/controller_subsystem in Master.subsystems)
|
|
if(controller_subsystem.planning_status == ai_status)
|
|
controller_subsystem.currentrun -= src
|
|
break
|
|
our_cells = null
|
|
if(ai_movement.moving_controllers[src])
|
|
ai_movement.stop_moving_towards(src)
|
|
QDEL_LIST(behavior_nodes)
|
|
return ..()
|
|
|
|
///Overrides the current ai_movement of this controller with a new one
|
|
/datum/ai_controller/proc/change_ai_movement_type(datum/ai_movement/new_movement)
|
|
ai_movement = SSai_movement.movement_types[new_movement]
|
|
|
|
///Completely replaces the behavior_nodes with a new set based on argument provided.
|
|
/datum/ai_controller/proc/replace_behavior_nodes(list/typepaths_of_new_subtrees)
|
|
var/list/old_nodes = behavior_nodes
|
|
behavior_nodes = typepaths_of_new_subtrees
|
|
initialize_behavior_tree()
|
|
QDEL_LIST(old_nodes)
|
|
|
|
/// Resolves the children/child of a composite or decorator node, creating configured instances.
|
|
/// Safe to call on any node type; non-composite/non-decorator nodes are a no-op.
|
|
/datum/ai_controller/proc/resolve_node_children(datum/bt_node/node)
|
|
if(istype(node, /datum/bt_node/composite))
|
|
var/datum/bt_node/composite/comp = node
|
|
if(!LAZYLEN(comp.children_typepaths) || LAZYLEN(comp.children))
|
|
return
|
|
var/list/resolved_children = list()
|
|
for(var/child_type in comp.children_typepaths)
|
|
var/list/config = comp.children_typepaths[child_type]
|
|
var/datum/bt_node/child = resolve_child_node(child_type, config)
|
|
if(isnull(child))
|
|
stack_trace("BT composite [node.type] references unknown child type [child_type]")
|
|
continue
|
|
resolved_children += child
|
|
comp.children = resolved_children
|
|
else if(istype(node, /datum/bt_node/decorator))
|
|
var/datum/bt_node/decorator/dec = node
|
|
if(isnull(dec.child_typepath) || !isnull(dec.child))
|
|
return
|
|
dec.child = resolve_child_node(dec.child_typepath, null)
|
|
if(isnull(dec.child))
|
|
stack_trace("BT decorator [node.type] references unknown child type [dec.child_typepath]")
|
|
else if(istype(node, /datum/bt_node/subtree))
|
|
var/datum/bt_node/subtree/sub = node
|
|
if(!isnull(sub.behavior_nodes) && isnull(sub.root))
|
|
sub.root = build_node_from_descriptor(sub.behavior_nodes)
|
|
else if(!isnull(sub.behavior_tree_json) && isnull(sub.root))
|
|
var/file = file(BT_COMPILED_PATH(sub.behavior_tree_json))
|
|
var/list/raw_desc = json_decode(file2text(file))
|
|
if(LAZYLEN(sub.bindings) || !isnull(raw_desc[BT_DESC_BINDINGS]))
|
|
raw_desc = apply_bindings_to_descriptor(raw_desc, sub.bindings)
|
|
sub.root = build_node_from_descriptor(raw_desc)
|
|
|
|
// Always creates a fresh instance regardless of whether config is provided.
|
|
/datum/ai_controller/proc/resolve_child_node(child_type, list/config)
|
|
if(!ispath(child_type, /datum/bt_node))
|
|
return null
|
|
var/datum/bt_node/child = new child_type
|
|
if(config)
|
|
child.configure(config)
|
|
resolve_node_children(child)
|
|
return child
|
|
|
|
/datum/ai_controller/proc/get_or_build_node(entry)
|
|
if(ispath(entry))
|
|
if(!ispath(entry, /datum/bt_node))
|
|
stack_trace("get_or_build_node() received non-BT typepath: [entry]")
|
|
return null
|
|
var/datum/bt_node/node = new entry
|
|
resolve_node_children(node)
|
|
return node
|
|
if(islist(entry))
|
|
return build_node_from_descriptor(entry)
|
|
stack_trace("get_or_build_node() received unexpected entry type: [entry]")
|
|
return null
|
|
|
|
///Loads and decodes a compiled BT JSON file into a node tree.
|
|
/datum/ai_controller/proc/load_tree_from_json(path)
|
|
var/file = file(path)
|
|
var/list/desc = json_decode(file2text(file))
|
|
return build_node_from_descriptor(desc)
|
|
|
|
/**
|
|
* Merges call-site binding overrides with the subtree's declared defaults,
|
|
* then substitutes all $name placeholders in the descriptor tree.
|
|
* Returns a new descriptor with BT_DESC_BINDINGS stripped and placeholders resolved.
|
|
*/
|
|
/datum/ai_controller/proc/apply_bindings_to_descriptor(list/desc, list/call_site_bindings)
|
|
var/list/merged = list()
|
|
var/list/declared = desc[BT_DESC_BINDINGS]
|
|
for(var/name in declared)
|
|
merged[name] = declared[name]["default"]
|
|
for(var/name in call_site_bindings)
|
|
merged[name] = call_site_bindings[name]
|
|
return _substitute_bindings(desc, merged)
|
|
|
|
/// Recursively walks a descriptor list, replacing "$name" strings with their bound values.
|
|
/datum/ai_controller/proc/_substitute_bindings(list/desc, list/merged)
|
|
var/list/out = list()
|
|
for(var/key in desc)
|
|
if(key == BT_DESC_BINDINGS)
|
|
continue
|
|
var/value = desc[key]
|
|
if(islist(value))
|
|
out[key] = _substitute_bindings_in_list(value, merged)
|
|
else if(istext(value) && copytext(value, 1, 2) == "$")
|
|
var/binding_name = copytext(value, 2)
|
|
out[key] = isnull(merged[binding_name]) ? value : merged[binding_name]
|
|
else
|
|
out[key] = value
|
|
return out
|
|
|
|
/// Substitutes bindings inside a descriptor value list, preserving assoc entries
|
|
/datum/ai_controller/proc/_substitute_bindings_in_list(list/input, list/merged)
|
|
var/list/resolved_list = list()
|
|
for(var/item in input)
|
|
var/assoc_value = isnum(item) ? null : input[item]
|
|
if(!isnull(assoc_value))
|
|
if(islist(assoc_value))
|
|
resolved_list[item] = _substitute_bindings_in_list(assoc_value, merged) //recursion baby
|
|
else if(istext(assoc_value) && copytext(assoc_value, 1, 2) == "$")
|
|
var/binding_name = copytext(assoc_value, 2)
|
|
resolved_list[item] = isnull(merged[binding_name]) ? assoc_value : merged[binding_name]
|
|
else
|
|
resolved_list[item] = assoc_value
|
|
else if(islist(item))
|
|
resolved_list += list(_substitute_bindings(item, merged))
|
|
else if(istext(item) && copytext(item, 1, 2) == "$")
|
|
var/binding_name = copytext(item, 2)
|
|
var/resolved = isnull(merged[binding_name]) ? item : merged[binding_name]
|
|
if(islist(resolved))
|
|
resolved_list += list(resolved)
|
|
else
|
|
resolved_list += resolved
|
|
else
|
|
resolved_list += item
|
|
return resolved_list
|
|
|
|
/**
|
|
* Recursively builds a BT node tree from a descriptor list.
|
|
* BT_DESC_TYPE and BT_DESC_CHILDREN are consumed internally; all other keys are written
|
|
* as vars onto the node. String values starting with "/" are resolved via text2path so
|
|
* typepath args (e.g. "/datum/ai_movement/basic_avoidance") arrive as actual types.
|
|
* If you put / in a string then yeah that might cause issues, should probably fix that later!
|
|
*/
|
|
/datum/ai_controller/proc/build_node_from_descriptor(list/desc)
|
|
var/raw_type = desc[BT_DESC_TYPE]
|
|
if(!raw_type) // This can happen if we have an overriden type with no binding. (e.g. subtrees not being overriden and default to null)
|
|
return null
|
|
var/node_type = ispath(raw_type) ? raw_type : text2path(raw_type)
|
|
if(isnull(node_type))
|
|
stack_trace("build_node_from_descriptor(): unknown typepath '[raw_type]'")
|
|
return null
|
|
var/datum/bt_node/node = new node_type
|
|
for(var/key in desc)
|
|
if(key == BT_DESC_TYPE || key == BT_DESC_CHILDREN || key == BT_DESC_BINDINGS)
|
|
continue
|
|
var/value = desc[key]
|
|
if(islist(value))
|
|
var/list/resolved = value
|
|
for(var/i in 1 to length(resolved))
|
|
if(istext(resolved[i]))
|
|
var/as_path = text2path(resolved[i])
|
|
if(!isnull(as_path))
|
|
resolved[i] = as_path
|
|
else if(istext(value))
|
|
var/as_path = text2path(value)
|
|
if(!isnull(as_path))
|
|
value = as_path
|
|
node.vars[key] = value
|
|
resolve_node_children(node)
|
|
var/list/children_descs = desc[BT_DESC_CHILDREN]
|
|
if(LAZYLEN(children_descs))
|
|
node.set_descriptor_children(children_descs, src)
|
|
return node
|
|
|
|
/// Builds the per-controller BT node tree from behavior_nodes typepaths or descriptors, then finalizes it.
|
|
/datum/ai_controller/proc/initialize_behavior_tree()
|
|
if(!isnull(behavior_tree_json) && !LAZYLEN(behavior_nodes))
|
|
var/compiled_path = BT_COMPILED_PATH(behavior_tree_json) //Find the compiled version of this BT
|
|
var/datum/bt_node/root = load_tree_from_json(compiled_path)
|
|
if(isnull(root))
|
|
stack_trace("[type] failed to load behavior tree from compiled JSON: [compiled_path]")
|
|
return
|
|
behavior_nodes = list(root)
|
|
finalize_tree()
|
|
return
|
|
if(!LAZYLEN(behavior_nodes))
|
|
return
|
|
var/list/temp_subtree_list = list()
|
|
if(!isnull(behavior_nodes[BT_DESC_TYPE]))
|
|
var/datum/bt_node/node_instance = get_or_build_node(behavior_nodes)
|
|
if(isnull(node_instance))
|
|
stack_trace("[type]'s behavior_nodes BT descriptor could not be built")
|
|
else
|
|
temp_subtree_list += node_instance
|
|
else
|
|
for(var/entry in behavior_nodes)
|
|
var/datum/bt_node/node_instance = get_or_build_node(entry)
|
|
if(isnull(node_instance))
|
|
stack_trace("[type]'s behavior_nodes contains unknown entry: [entry]")
|
|
continue
|
|
temp_subtree_list += node_instance
|
|
behavior_nodes = temp_subtree_list
|
|
finalize_tree()
|
|
|
|
/// Walks the resolved tree to set owning_controller and parent_node on all nodes, populates
|
|
/// override_slots, and assigns pre-order execution indices. Called after initialize_behavior_tree() and
|
|
/// after set_behavior_tree_override() installs or removes an override node.
|
|
/datum/ai_controller/proc/finalize_tree()
|
|
if(!LAZYLEN(behavior_nodes))
|
|
return
|
|
override_slots = null
|
|
var/list/to_visit = behavior_nodes.Copy()
|
|
for(var/datum/bt_node/root in behavior_nodes)
|
|
root.parent_node = null
|
|
var/index = 1
|
|
while(index <= length(to_visit)) //while loop so we can recursively keep populating this list
|
|
var/datum/bt_node/node = to_visit[index++]
|
|
node.finalize_node(src, to_visit)
|
|
var/counter = 1
|
|
for(var/datum/bt_node/root in behavior_nodes)
|
|
counter = root.assign_execution_indices(counter)
|
|
|
|
///Proc to move from one pawn to another, this will destroy the target's existing controller.
|
|
/datum/ai_controller/proc/PossessPawn(atom/new_pawn)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
if(pawn) //Reset any old signals
|
|
UnpossessPawn(FALSE)
|
|
|
|
if(istype(new_pawn.ai_controller)) //Existing AI, kill it.
|
|
QDEL_NULL(new_pawn.ai_controller)
|
|
|
|
if(TryPossessPawn(new_pawn) & AI_CONTROLLER_INCOMPATIBLE)
|
|
qdel(src)
|
|
CRASH("[src] attached to [new_pawn] but these are not compatible!")
|
|
|
|
pawn = new_pawn
|
|
pawn.ai_controller = src
|
|
set_blackboard_key(BB_MY_PAWN, pawn, FALSE) //Don't track the datum we already handle qdel of pawn here.
|
|
|
|
var/turf/pawn_turf = get_turf(pawn)
|
|
if(pawn_turf)
|
|
SSai_controllers.ai_controllers_by_zlevel[pawn_turf.z] += src
|
|
|
|
SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_POSSESSED_PAWN)
|
|
|
|
reset_ai_status()
|
|
RegisterSignal(pawn, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_changed_z_level))
|
|
RegisterSignal(pawn, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed))
|
|
RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained))
|
|
RegisterSignal(pawn, COMSIG_QDELETING, PROC_REF(on_pawn_qdeleted))
|
|
RegisterSignal(pawn, COMSIG_EVLOGGING_ENABLED, PROC_REF(on_pawn_evlogging_enabled))
|
|
RegisterSignal(pawn, COMSIG_EVLOGGING_DISABLED, PROC_REF(on_pawn_evlogging_disabled))
|
|
|
|
our_cells = new(interesting_dist, interesting_dist, 1)
|
|
set_new_cells()
|
|
|
|
update_able_to_run()
|
|
setup_able_to_run()
|
|
|
|
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
|
|
|
|
set_new_cells()
|
|
|
|
/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()
|
|
|
|
///Returns TRUE if a living mob with a client is in one of our tracked spatial grid cells
|
|
/datum/ai_controller/proc/has_nearby_client()
|
|
if(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 TRUE
|
|
return FALSE
|
|
|
|
///Check if mob should go into idle/low-priority (from spatial cells)
|
|
/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
|
|
|
|
reset_ai_status()
|
|
|
|
/datum/ai_controller/proc/on_client_enter(datum/source, list/target_list)
|
|
SIGNAL_HANDLER
|
|
|
|
if(ai_status == AI_STATUS_ON)
|
|
return
|
|
|
|
if (!(locate(/mob/living) in target_list))
|
|
return
|
|
|
|
reset_ai_status()
|
|
|
|
/datum/ai_controller/proc/on_client_exit(datum/source, datum/exited)
|
|
SIGNAL_HANDLER
|
|
|
|
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())
|
|
|
|
/**
|
|
* Deliberately disables this AI until clear_forced_off() is called.
|
|
* Unlike a bare set_ai_status(AI_STATUS_OFF), this survives status recalculations
|
|
* from stat changes, z-level changes, client login/logout and the like.
|
|
*/
|
|
/datum/ai_controller/proc/force_ai_off(additional_flags = NONE)
|
|
forced_off = TRUE
|
|
set_ai_status(AI_STATUS_OFF, additional_flags)
|
|
|
|
/// Undoes force_ai_off() and recalculates what status we should be in.
|
|
/datum/ai_controller/proc/clear_forced_off()
|
|
forced_off = FALSE
|
|
reset_ai_status()
|
|
|
|
/**
|
|
* Gets the AI status we expect the AI controller to be on at this current moment.
|
|
* Returns AI_STATUS_OFF if it has been forced off, is inhabited by a Client and shouldn't be, is dead and cannot act while dead,
|
|
* or is sleeping for performance (off-station with no nearby client and not flagged RUN_WHILE_UNWATCHED; client arrival wakes these automatically).
|
|
* Otherwise returns AI_STATUS_ON or AI_STATUS_ON_LOW, see get_active_ai_status().
|
|
*/
|
|
/datum/ai_controller/proc/get_expected_ai_status()
|
|
|
|
|
|
/*
|
|
#ifdef AI_PERFORMANCE_TESTING
|
|
return AI_STATUS_ON
|
|
#endif
|
|
*/
|
|
if (forced_off)
|
|
return AI_STATUS_OFF
|
|
|
|
if (isnull(get_turf(pawn)))
|
|
return AI_STATUS_OFF
|
|
|
|
if (!ismob(pawn))
|
|
return AI_STATUS_ON
|
|
|
|
var/mob/living/mob_pawn = pawn
|
|
if(!continue_processing_when_client && mob_pawn.client)
|
|
return AI_STATUS_OFF
|
|
|
|
if(mob_pawn.stat == DEAD)
|
|
if(ai_traits & CAN_ACT_WHILE_DEAD)
|
|
return AI_STATUS_ON
|
|
return AI_STATUS_OFF
|
|
|
|
#ifdef TESTING
|
|
var/turf/pawn_turf = get_turf(mob_pawn)
|
|
if(!pawn_turf)
|
|
CRASH("AI controller [src] controlling pawn ([pawn]) is not on a turf.")
|
|
#endif
|
|
if(!able_to_run)
|
|
return AI_STATUS_OFF
|
|
return get_active_ai_status()
|
|
|
|
/**
|
|
* Classifies an active AI controller into a priority tier.
|
|
* Returns AI_STATUS_ON for controllers on station/shuttle territory, with a nearby client,
|
|
* Returns AI_STATUS_ON_LOW for other active controllers unless they have ALWAYS_HIGH_PRIORITY.
|
|
* else its AI_STATUS_OFF
|
|
*/
|
|
/datum/ai_controller/proc/get_active_ai_status()
|
|
var/turf/pawn_turf = get_turf(pawn)
|
|
var/area/pawn_area = pawn_turf ? get_area(pawn_turf) : null
|
|
// AI actually standing on the station or a shuttle always stays high priority
|
|
if(istype(pawn_area, /area/station) || istype(pawn_area, /area/shuttle))
|
|
return AI_STATUS_ON
|
|
if(has_nearby_client())
|
|
return AI_STATUS_ON
|
|
if(ai_traits & RUN_WHILE_UNWATCHED)
|
|
if(ai_traits & ALWAYS_HIGH_PRIORITY)
|
|
return AI_STATUS_ON
|
|
else
|
|
return AI_STATUS_ON_LOW
|
|
return AI_STATUS_OFF
|
|
|
|
///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.
|
|
/datum/ai_controller/proc/on_changed_z_level(atom/source, turf/old_turf, turf/new_turf, same_z_layer, notify_contents)
|
|
SIGNAL_HANDLER
|
|
if (ismob(pawn))
|
|
var/mob/mob_pawn = pawn
|
|
if((mob_pawn?.client && !continue_processing_when_client))
|
|
return
|
|
if(old_turf)
|
|
SSai_controllers.ai_controllers_by_zlevel[old_turf.z] -= src
|
|
if(isnull(new_turf))
|
|
return
|
|
SSai_controllers.ai_controllers_by_zlevel[new_turf.z] += src
|
|
reset_ai_status()
|
|
|
|
///Abstract proc for initializing the pawn to the new controller
|
|
/datum/ai_controller/proc/TryPossessPawn(atom/new_pawn)
|
|
return
|
|
|
|
///Proc for deinitializing the pawn to the old controller
|
|
/datum/ai_controller/proc/UnpossessPawn(destroy)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
if(isnull(pawn))
|
|
return // instantiated without an applicable pawn, fine
|
|
|
|
SEND_SIGNAL(src, COMSIG_AI_CONTROLLER_UNPOSSESSED_PAWN)
|
|
reset_bt_tick_states()
|
|
set_ai_status(AI_STATUS_OFF)
|
|
UnregisterSignal(pawn, list(COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE, COMSIG_QDELETING, COMSIG_EVLOGGING_ENABLED))
|
|
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
|
|
pawn.ai_controller = null
|
|
pawn = null
|
|
if(destroy)
|
|
qdel(src)
|
|
|
|
///Call reset tick state on every node in the tree.
|
|
/datum/ai_controller/proc/reset_bt_tick_states()
|
|
if(!LAZYLEN(behavior_nodes))
|
|
return
|
|
var/list/to_visit = behavior_nodes.Copy()
|
|
var/index = 1
|
|
while(index <= length(to_visit))
|
|
var/datum/bt_node/node = to_visit[index++]
|
|
node.reset_tick_state()
|
|
node.collect_reset_children(to_visit)
|
|
|
|
/**
|
|
* Installs or removes a runtime override on the subtree slot registered with the given id.
|
|
*
|
|
* id - The ID for this slot
|
|
* override_subtree - actual subtree we're setting
|
|
*/
|
|
/datum/ai_controller/proc/set_behavior_tree_override(id, override_subtree)
|
|
var/datum/bt_node/subtree/slot = LAZYACCESS(override_slots, id)
|
|
if(isnull(slot))
|
|
return
|
|
|
|
var/current_type = isnull(slot.override_node) ? null : slot.override_node.type
|
|
if(current_type == override_subtree)
|
|
return
|
|
|
|
if(slot.override_node)
|
|
slot.override_node.reset_tick_state()
|
|
QDEL_NULL(slot.override_node)
|
|
|
|
if(isnull(override_subtree))
|
|
finalize_tree()
|
|
SEND_SIGNAL(pawn, COMSIG_AI_OVERRIDE_SLOT_CHANGED(id), null)
|
|
return
|
|
|
|
var/datum/bt_node/subtree/new_node = new override_subtree
|
|
resolve_node_children(new_node)
|
|
slot.override_node = new_node
|
|
finalize_tree()
|
|
cancel_current_plan() // Reset, not ideal; Maybe later on we can do this more gracefully.
|
|
SEND_SIGNAL(pawn, COMSIG_AI_OVERRIDE_SLOT_CHANGED(id), override_subtree)
|
|
|
|
/datum/ai_controller/proc/setup_able_to_run()
|
|
// paused_until is handled by PauseAi() manually
|
|
RegisterSignals(pawn, list(SIGNAL_ADDTRAIT(TRAIT_AI_PAUSED), SIGNAL_REMOVETRAIT(TRAIT_AI_PAUSED)), PROC_REF(update_able_to_run))
|
|
|
|
/datum/ai_controller/proc/clear_able_to_run()
|
|
UnregisterSignal(pawn, list(SIGNAL_ADDTRAIT(TRAIT_AI_PAUSED), SIGNAL_REMOVETRAIT(TRAIT_AI_PAUSED)))
|
|
|
|
/datum/ai_controller/proc/update_able_to_run()
|
|
SIGNAL_HANDLER
|
|
var/run_flags = get_able_to_run()
|
|
if(run_flags & AI_UNABLE_TO_RUN)
|
|
able_to_run = FALSE
|
|
ai_movement.fail_movement(src)
|
|
else
|
|
able_to_run = TRUE
|
|
set_ai_status(get_expected_ai_status(), run_flags)
|
|
|
|
///Returns TRUE if the ai controller can actually run at the moment, FALSE otherwise
|
|
/datum/ai_controller/proc/get_able_to_run()
|
|
if(HAS_TRAIT(pawn, TRAIT_AI_PAUSED))
|
|
return AI_UNABLE_TO_RUN
|
|
if(world.time < paused_until)
|
|
return AI_UNABLE_TO_RUN
|
|
return NONE
|
|
|
|
///Can this pawn interact with objects?
|
|
/datum/ai_controller/proc/ai_can_interact(atom/target)
|
|
return !QDELETED(pawn) && !QDELETED(target)
|
|
|
|
///Interact with objects
|
|
/datum/ai_controller/proc/ai_interact(target, combat_mode, list/modifiers)
|
|
var/atom/final_target = isdatum(target) ? target : blackboard[target] //incase we got a blackboard key instead
|
|
|
|
if(!ai_can_interact(final_target))
|
|
return FALSE
|
|
|
|
var/params = list2params(modifiers)
|
|
var/mob/living/living_pawn = pawn
|
|
if(isnull(combat_mode))
|
|
living_pawn.ClickOn(final_target, params)
|
|
return TRUE
|
|
|
|
var/old_combat_mode = living_pawn.combat_mode
|
|
living_pawn.set_combat_mode(combat_mode)
|
|
living_pawn.ClickOn(final_target, params)
|
|
living_pawn.set_combat_mode(old_combat_mode)
|
|
return TRUE
|
|
|
|
|
|
///This is where you decide what actions are taken by the AI.
|
|
/datum/ai_controller/proc/SelectBehaviors(seconds_per_tick)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
cancelled_during_tick = FALSE
|
|
if(LAZYLEN(polling_observers))
|
|
for(var/datum/bt_node/decorator/dec as anything in polling_observers.Copy())
|
|
dec.poll_condition(src)
|
|
for(var/datum/bt_node/node as anything in behavior_nodes)
|
|
if(node.tick(src, seconds_per_tick) == BT_RUNNING)
|
|
break
|
|
|
|
///This proc handles changing ai status and updates the planning subsystem list.
|
|
/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
|
|
for(var/datum/controller/subsystem/ai_controllers/controller_subsystem in Master.subsystems)
|
|
if(controller_subsystem.planning_status == ai_status)
|
|
controller_subsystem.currentrun -= src
|
|
break
|
|
ai_status = new_ai_status
|
|
last_bt_tick = 0 // don't count time spent in the previous status towards the next tick's seconds_per_tick
|
|
SSai_controllers.ai_controllers_by_status[new_ai_status] += src
|
|
if(ai_status == AI_STATUS_OFF)
|
|
if(!(additional_flags & AI_PREVENT_CANCEL_ACTIONS))
|
|
cancel_current_plan()
|
|
|
|
|
|
|
|
/datum/ai_controller/proc/cancel_current_plan()
|
|
active_execution_index = 0
|
|
cancelled_during_tick = TRUE
|
|
reset_bt_tick_states()
|
|
|
|
/// Turn the controller on or off based on if you're alive, we only register to this if the flag is present so don't need to check again
|
|
/datum/ai_controller/proc/on_stat_changed(mob/living/source, new_stat)
|
|
SIGNAL_HANDLER
|
|
reset_ai_status()
|
|
update_able_to_run()
|
|
|
|
/datum/ai_controller/proc/on_sentience_gained()
|
|
SIGNAL_HANDLER
|
|
UnregisterSignal(pawn, COMSIG_MOB_LOGIN)
|
|
if(!continue_processing_when_client)
|
|
set_ai_status(AI_STATUS_OFF) //Can't do anything while player is connected
|
|
RegisterSignal(pawn, COMSIG_MOB_LOGOUT, PROC_REF(on_sentience_lost))
|
|
|
|
/datum/ai_controller/proc/on_sentience_lost()
|
|
SIGNAL_HANDLER
|
|
UnregisterSignal(pawn, COMSIG_MOB_LOGOUT)
|
|
reset_ai_status() //resume AI control now that the client is gone
|
|
RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained))
|
|
|
|
// Turn the controller off if the pawn has been qdeleted
|
|
/datum/ai_controller/proc/on_pawn_qdeleted(datum/source)
|
|
SIGNAL_HANDLER
|
|
sig_remove_from_blackboard(source)
|
|
set_ai_status(AI_STATUS_OFF)
|
|
if(ai_movement.moving_controllers[src])
|
|
ai_movement.stop_moving_towards(src)
|
|
|
|
/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding. Return the access list you want to use
|
|
/datum/ai_controller/proc/get_access()
|
|
if(!isliving(pawn))
|
|
return
|
|
var/mob/living/living_pawn = pawn
|
|
return living_pawn.get_access()
|
|
|
|
/// Returns TRUE if the pawn can path to the target. minimum_distance is how close the path must get (0 = onto/adjacent to the target's turf); searches pass it from their own acquire_target leaf.
|
|
/datum/ai_controller/proc/can_reach_target(atom/target, distance = 10, minimum_distance = 0)
|
|
if(!isdatum(target)) //we dont need to check if its not a datum!
|
|
return TRUE
|
|
if(get_turf(pawn) == get_turf(target))
|
|
return TRUE
|
|
var/list/path = get_path_to(pawn, target, simulated_only = !HAS_TRAIT(pawn, TRAIT_SPACEWALK), mintargetdist = minimum_distance, max_distance = distance, access = get_access())
|
|
return (!!length(path))
|
|
|
|
|
|
/// Called when a target was found but couldn't be reached. Base no-op; override to record the target (e.g. add it to an ignore list).
|
|
/datum/ai_controller/proc/note_unreachable_target(atom/target)
|
|
return
|
|
|
|
/// Returns true if we have a blackboard key with the provided key and it is not qdeleting
|
|
/datum/ai_controller/proc/blackboard_key_exists(key)
|
|
var/datum/key_value = blackboard[key]
|
|
if (isdatum(key_value))
|
|
return !QDELETED(key_value)
|
|
if (islist(key_value))
|
|
return length(key_value) > 0
|
|
return !!key_value
|
|
|
|
/**
|
|
* Used to manage references to datum by AI controllers
|
|
*
|
|
* * tracked_datum - something being added to an ai blackboard
|
|
* * key - the associated key
|
|
*/
|
|
#define TRACK_AI_DATUM_TARGET(tracked_datum, key) do { \
|
|
if(isweakref(tracked_datum)) { \
|
|
var/datum/weakref/_bad_weakref = tracked_datum; \
|
|
stack_trace("Weakref (Actual datum: [_bad_weakref.resolve()]) found in ai datum blackboard! \
|
|
This is an outdated method of ai reference handling, please remove it."); \
|
|
}; \
|
|
else if(isdatum(tracked_datum)) { \
|
|
var/datum/_tracked_datum = tracked_datum; \
|
|
if(QDELETED(_tracked_datum)) { \
|
|
stack_trace("Tried to track a qdeleted datum ([_tracked_datum]) in ai datum blackboard (key: [key])! \
|
|
Please ensure that we are not doing this by adding handling where necessary."); \
|
|
return; \
|
|
}; \
|
|
else if(!HAS_TRAIT_FROM(_tracked_datum, TRAIT_AI_TRACKING, "[REF(src)]_[key]")) { \
|
|
RegisterSignal(_tracked_datum, COMSIG_QDELETING, PROC_REF(sig_remove_from_blackboard), override = TRUE); \
|
|
ADD_TRAIT(_tracked_datum, TRAIT_AI_TRACKING, "[REF(src)]_[key]"); \
|
|
}; \
|
|
}; \
|
|
} while(FALSE)
|
|
|
|
/**
|
|
* Used to clear previously set reference handing by AI controllers
|
|
*
|
|
* * tracked_datum - something being removed from an ai blackboard
|
|
* * key - the associated key
|
|
*/
|
|
#define CLEAR_AI_DATUM_TARGET(tracked_datum, key) do { \
|
|
if(isdatum(tracked_datum)) { \
|
|
var/datum/_tracked_datum = tracked_datum; \
|
|
REMOVE_TRAIT(_tracked_datum, TRAIT_AI_TRACKING, "[REF(src)]_[key]"); \
|
|
if(!HAS_TRAIT(_tracked_datum, TRAIT_AI_TRACKING)) { \
|
|
UnregisterSignal(_tracked_datum, COMSIG_QDELETING); \
|
|
}; \
|
|
}; \
|
|
} while(FALSE)
|
|
|
|
/// Used for above to track all the keys that have registered a signal
|
|
#define TRAIT_AI_TRACKING "tracked_by_ai"
|
|
|
|
/**
|
|
* Sets the key to the passed "thing".
|
|
*
|
|
* * key - A blackboard key
|
|
* * thing - a value to set the blackboard key to.
|
|
* * track_datum - whether we should track this ref for deletion, this should always be TRUE unless you really know wtf you're doing
|
|
*/
|
|
/datum/ai_controller/proc/set_blackboard_key(key, thing, track_datum = TRUE)
|
|
// Assume it is an error when trying to set a value overtop a list
|
|
if(islist(blackboard[key]))
|
|
CRASH("set_blackboard_key attempting to set a blackboard value to key [key] when it's a list!")
|
|
// Don't do anything if it's already got this value
|
|
if (blackboard[key] == thing)
|
|
return
|
|
|
|
// Clear existing values
|
|
if(!isnull(blackboard[key]))
|
|
clear_blackboard_key(key)
|
|
|
|
if(track_datum)
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
blackboard[key] = thing
|
|
post_blackboard_key_set(key)
|
|
|
|
/**
|
|
* Helper to force a key to be a certain thing no matter what's already there
|
|
*
|
|
* Useful for if you're overriding a list with a new list entirely,
|
|
* as otherwise it would throw a runtime error from trying to override a list
|
|
*
|
|
* Not necessary to use if you aren't dealing with lists, as set_blackboard_key will clear the existing value
|
|
* in that case already, but may be useful for clarity.
|
|
*
|
|
* * key - A blackboard key
|
|
* * thing - a value to set the blackboard key to.
|
|
*/
|
|
/datum/ai_controller/proc/override_blackboard_key(key, thing)
|
|
if(blackboard[key] == thing)
|
|
return
|
|
|
|
clear_blackboard_key(key)
|
|
set_blackboard_key(key, thing)
|
|
|
|
/**
|
|
* Sets the key at index thing to the passed value
|
|
*
|
|
* Assumes the key value is already a list, if not throws an error.
|
|
*
|
|
* * key - A blackboard key, with its value set to a list
|
|
* * thing - a value which becomes the inner list value's key
|
|
* * value - what to set the inner list's value to
|
|
*/
|
|
/datum/ai_controller/proc/set_blackboard_key_assoc(key, thing, value)
|
|
if(!islist(blackboard[key]))
|
|
CRASH("set_blackboard_key_assoc called on non-list key [key]!")
|
|
// Don't do anything if it's already got this value
|
|
if (blackboard[key][thing] == value)
|
|
return
|
|
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
TRACK_AI_DATUM_TARGET(value, key)
|
|
blackboard[key][thing] = value
|
|
post_blackboard_key_set(key)
|
|
|
|
/**
|
|
* Similar to [proc/set_blackboard_key_assoc] but operates under the assumption the key is a lazylist (so it will create a list)
|
|
* More dangerous / easier to override values, only use when you want to use a lazylist
|
|
*
|
|
* * key - A blackboard key, with its value set to a list
|
|
* * thing - a value which becomes the inner list value's key
|
|
* * value - what to set the inner list's value to
|
|
*/
|
|
/datum/ai_controller/proc/set_blackboard_key_assoc_lazylist(key, thing, value)
|
|
LAZYINITLIST(blackboard[key])
|
|
// Don't do anything if it's already got this value
|
|
if (blackboard[key][thing] == value)
|
|
return
|
|
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
TRACK_AI_DATUM_TARGET(value, key)
|
|
blackboard[key][thing] = value
|
|
post_blackboard_key_set(key)
|
|
|
|
/**
|
|
* Called after we set a blackboard key, forwards signal information.
|
|
*/
|
|
/datum/ai_controller/proc/post_blackboard_key_set(key)
|
|
if (isnull(pawn))
|
|
return
|
|
SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_SET(key), key)
|
|
|
|
/**
|
|
* Adds the passed "thing" to the associated key
|
|
*
|
|
* Works with lists or numbers, but not lazylists.
|
|
*
|
|
* * key - A blackboard key
|
|
* * thing - a value to set the blackboard key to.
|
|
*/
|
|
/datum/ai_controller/proc/add_blackboard_key(key, thing)
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
blackboard[key] += thing
|
|
|
|
/**
|
|
* Similar to [proc/add_blackboard_key], but performs an insertion rather than an add
|
|
* Throws an error if the key is not a list already, intended only for use with lists
|
|
*
|
|
* * key - A blackboard key, with its value set to a list
|
|
* * thing - a value to set the blackboard key to.
|
|
*/
|
|
/datum/ai_controller/proc/insert_blackboard_key(key, thing)
|
|
if(!islist(blackboard[key]))
|
|
CRASH("insert_blackboard_key called on non-list key [key]!")
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
blackboard[key] |= thing
|
|
|
|
/**
|
|
* Adds the passed "thing" to the associated key, assuming key is intended to be a lazylist (so it will create a list)
|
|
* More dangerous / easier to override values, only use when you want to use a lazylist
|
|
*
|
|
* * key - A blackboard key
|
|
* * thing - a value to set the blackboard key to.
|
|
*/
|
|
/datum/ai_controller/proc/add_blackboard_key_lazylist(key, thing)
|
|
LAZYINITLIST(blackboard[key])
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
blackboard[key] += thing
|
|
|
|
/**
|
|
* Similar to [proc/insert_blackboard_key_lazylist], but performs an insertion / or rather than an add
|
|
*
|
|
* * key - A blackboard key
|
|
* * thing - a value to set the blackboard key to.
|
|
*/
|
|
/datum/ai_controller/proc/insert_blackboard_key_lazylist(key, thing)
|
|
LAZYINITLIST(blackboard[key])
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
blackboard[key] |= thing
|
|
post_blackboard_key_set(key)
|
|
|
|
/**
|
|
* Adds the value to the inner list at key with the inner key set to "thing"
|
|
* Throws an error if the key is not a list already, intended only for use with lists
|
|
*
|
|
* * key - A blackboard key, with its value set to a list
|
|
* * thing - a value which becomes the inner list value's key
|
|
* * value - what to set the inner list's value to
|
|
*/
|
|
/datum/ai_controller/proc/add_blackboard_key_assoc(key, thing, value)
|
|
if(!islist(blackboard[key]))
|
|
CRASH("add_blackboard_key_assoc called on non-list key [key]!")
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
TRACK_AI_DATUM_TARGET(value, key)
|
|
blackboard[key][thing] += value
|
|
|
|
|
|
/**
|
|
* Similar to [proc/add_blackboard_key_assoc], assuming key is intended to be a lazylist (so it will create a list)
|
|
* More dangerous / easier to override values, only use when you want to use a lazylist
|
|
*
|
|
* * key - A blackboard key, with its value set to a list
|
|
* * thing - a value which becomes the inner list value's key
|
|
* * value - what to set the inner list's value to
|
|
*/
|
|
/datum/ai_controller/proc/add_blackboard_key_assoc_lazylist(key, thing, value)
|
|
LAZYINITLIST(blackboard[key])
|
|
TRACK_AI_DATUM_TARGET(thing, key)
|
|
TRACK_AI_DATUM_TARGET(value, key)
|
|
blackboard[key][thing] += value
|
|
|
|
/**
|
|
* Clears the passed key, resetting it to null
|
|
*
|
|
* Not intended for use with list keys - use [proc/remove_thing_from_blackboard_key] if you are removing a value from a list at a key
|
|
*
|
|
* * key - A blackboard key
|
|
*/
|
|
/datum/ai_controller/proc/clear_blackboard_key(key)
|
|
if(isnull(blackboard[key]))
|
|
return
|
|
if(pawn && (SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_PRECLEAR(key))))
|
|
return
|
|
CLEAR_AI_DATUM_TARGET(blackboard[key], key)
|
|
blackboard[key] = null
|
|
if(isnull(pawn))
|
|
return
|
|
SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(key), key)
|
|
|
|
/**
|
|
* Remove the passed thing from the associated blackboard key
|
|
*
|
|
* Intended for use with lists, if you're just clearing a reference from a key use [proc/clear_blackboard_key]
|
|
*
|
|
* * key - A blackboard key
|
|
* * thing - a value to set the blackboard key to.
|
|
*/
|
|
/datum/ai_controller/proc/remove_thing_from_blackboard_key(key, thing)
|
|
var/associated_value = blackboard[key]
|
|
if(isnull(associated_value))
|
|
return
|
|
if(thing == associated_value)
|
|
stack_trace("remove_thing_from_blackboard_key was called un-necessarily in a situation where clear_blackboard_key would suffice. ")
|
|
clear_blackboard_key(key)
|
|
return
|
|
|
|
if(!islist(associated_value))
|
|
CRASH("remove_thing_from_blackboard_key called with an invalid \"thing\" argument ([thing]). \
|
|
(The associated value of the passed key is not a list and is also not the passed thing, meaning it is clearing an unintended value.)")
|
|
|
|
for(var/inner_key in associated_value)
|
|
if(inner_key == thing)
|
|
// flat list
|
|
CLEAR_AI_DATUM_TARGET(thing, key)
|
|
associated_value -= thing
|
|
return
|
|
else if(associated_value[inner_key] == thing)
|
|
// assoc list
|
|
CLEAR_AI_DATUM_TARGET(thing, key)
|
|
associated_value -= inner_key
|
|
return
|
|
|
|
CRASH("remove_thing_from_blackboard_key called with an invalid \"thing\" argument ([thing]). \
|
|
(The passed value is not tracked in the passed list.)")
|
|
|
|
///removes a tracked object from a lazylist
|
|
/datum/ai_controller/proc/remove_from_blackboard_lazylist_key(key, thing)
|
|
var/lazylist = blackboard[key]
|
|
if(isnull(lazylist))
|
|
return
|
|
for(var/key_index in lazylist)
|
|
if(thing == key_index || lazylist[key_index] == thing)
|
|
CLEAR_AI_DATUM_TARGET(thing, key)
|
|
lazylist -= key_index
|
|
break
|
|
if(!LAZYLEN(lazylist))
|
|
clear_blackboard_key(key)
|
|
|
|
/// Signal proc to go through every key and remove the datum from all keys it finds
|
|
/datum/ai_controller/proc/sig_remove_from_blackboard(datum/source)
|
|
SIGNAL_HANDLER
|
|
|
|
var/list/list/remove_queue = list(blackboard)
|
|
var/index = 1
|
|
while(index <= length(remove_queue))
|
|
var/list/next_to_clear = remove_queue[index]
|
|
for(var/inner_value in next_to_clear)
|
|
// We are a lists of lists, add the next value to the queue so we can handle references in there
|
|
// (But we only need to bother checking the list if it's not empty.)
|
|
if(islist(inner_value) && length(inner_value))
|
|
UNTYPED_LIST_ADD(remove_queue, inner_value)
|
|
|
|
// We found the value that's been deleted. Clear it out from this list
|
|
else if(inner_value == source)
|
|
next_to_clear -= inner_value
|
|
|
|
//if this is the case stop here. This means the list isn't associative (because an assoc list couldnt have a key for a number!)
|
|
if(isnum(inner_value))
|
|
continue
|
|
|
|
var/associated_value = next_to_clear[inner_value]
|
|
if(!associated_value) //This wasn't an associated list! we lied! its all been a trick. Try again next time.
|
|
continue
|
|
// We are an assoc lists of lists, the list at the next value so we can handle references in there
|
|
// (But again, we only need to bother checking the list if it's not empty.)
|
|
if(islist(associated_value) && length(associated_value))
|
|
UNTYPED_LIST_ADD(remove_queue, associated_value)
|
|
|
|
// We found the value that's been deleted, it was an assoc value. Clear it out entirely
|
|
else if(associated_value == source)
|
|
next_to_clear -= inner_value
|
|
SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value))
|
|
|
|
index += 1
|
|
|
|
/// When the pawn gets DF_EVLOGGING, propagate it to this controller too.
|
|
/datum/ai_controller/proc/on_pawn_evlogging_enabled(datum/source)
|
|
SIGNAL_HANDLER
|
|
enable_evlogging(pawn)
|
|
|
|
/// When the pawn gets DF_EVLOGGING disabled, propagate it to this controller too.
|
|
/datum/ai_controller/proc/on_pawn_evlogging_disabled(datum/source)
|
|
SIGNAL_HANDLER
|
|
disable_evlogging(pawn)
|
|
|
|
///Register for an event being added so we can update track info
|
|
/datum/ai_controller/enable_evlogging()
|
|
. = ..()
|
|
RegisterSignal(src, COMSIG_EVLOG_EVENT_ADDED, PROC_REF(on_evlog_event_added))
|
|
|
|
///Unregister the evlog event added event, as we're no longer updating track info
|
|
/datum/ai_controller/disable_evlogging()
|
|
. = ..()
|
|
UnregisterSignal(src, COMSIG_EVLOG_EVENT_ADDED)
|
|
|
|
|
|
/// Called whenever an event is logged for this controller. Attaches a snapshot of current behaviors and blackboard state to the event via track_info.
|
|
/datum/ai_controller/proc/on_evlog_event_added(datum/source, datum/event_logger_track/track, list/event_data)
|
|
SIGNAL_HANDLER
|
|
var/list/track_info = list()
|
|
|
|
// Build full tree state view showing all nodes with status markers
|
|
var/list/tree_lines = list()
|
|
for(var/datum/bt_node/root_node as anything in behavior_nodes)
|
|
root_node.append_full_tree_state(tree_lines, "")
|
|
EVLOG_TRACK_INFO_ENTRY(track_info, "Behaviors", "Full Tree State", length(tree_lines) ? jointext(tree_lines, "\n") : "(none)")
|
|
|
|
// Add execution context section
|
|
var/active_node_label = "(none)"
|
|
if(active_execution_index)
|
|
for(var/datum/bt_node/root_node as anything in behavior_nodes)
|
|
var/datum/bt_node/found = root_node.find_by_index(active_execution_index)
|
|
if(found)
|
|
active_node_label = found.label
|
|
break
|
|
EVLOG_TRACK_INFO_ENTRY(track_info, "Execution Context", "Active Execution Index", "[active_execution_index] ([active_node_label])")
|
|
EVLOG_TRACK_INFO_ENTRY(track_info, "Execution Context", "AI Status", ai_status)
|
|
EVLOG_TRACK_INFO_ENTRY(track_info, "Execution Context", "Able to Run", able_to_run ? "TRUE" : "FALSE")
|
|
|
|
// Blackboard snapshot
|
|
for(var/blackboard_key_name, blackboard_value in blackboard)
|
|
var/value_string
|
|
if(isatom(blackboard_value))
|
|
value_string = "[blackboard_value]"
|
|
else if(islist(blackboard_value))
|
|
var/list/blackboard_list = blackboard_value
|
|
value_string = length(blackboard_list) ? jointext(blackboard_list, "\n") : "Empty List"
|
|
else if(isnull(blackboard_value))
|
|
value_string = "null"
|
|
else // I think I covered all cases?
|
|
value_string = "[blackboard_value]"
|
|
EVLOG_TRACK_INFO_ENTRY(track_info, "Blackboard", blackboard_key_name, value_string)
|
|
|
|
event_data["track_info"] = track_info
|
|
|
|
|
|
#undef TRACK_AI_DATUM_TARGET
|
|
#undef CLEAR_AI_DATUM_TARGET
|
|
#undef TRAIT_AI_TRACKING
|