mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-01-01 13:12:23 +00:00
838 lines
31 KiB
Plaintext
838 lines
31 KiB
Plaintext
RESTRICT_TYPE(/datum/ai_controller)
|
|
|
|
/**
|
|
* 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 actions
|
|
* it will try to perform 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/list/blackboard = list()
|
|
|
|
/// 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
|
|
/// Current actions being performed by the AI.
|
|
var/list/current_behaviors
|
|
/// Current actions and their respective last time ran as an assoc list.
|
|
var/list/behavior_cooldowns = list()
|
|
/// Current status of AI (OFF/ON)
|
|
var/ai_status
|
|
/// Current movement target of the AI, generally set by decision making.
|
|
var/atom/current_movement_target
|
|
/// Identifier for what last touched our movement target, so it can be cleared conditionally
|
|
var/movement_target_source
|
|
/// Stored arguments for behaviors given during their initial creation
|
|
var/list/behavior_args = list()
|
|
/// 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
|
|
/// Cooldown for new plans, to prevent AI from going nuts if it can't think of new plans and looping on end
|
|
COOLDOWN_DECLARE(failed_planning_cooldown)
|
|
/// All subtrees this AI has available. Will run them in order, so make sure
|
|
/// they're in the order you want them to run. On initialization of this
|
|
/// type, it will start as a typepath(s) and get converted to references of
|
|
/// ai_subtrees found in SSai_controllers when init_subtrees() is called
|
|
var/list/planning_subtrees
|
|
|
|
/// The idle behavior this AI performs when it has no actions.
|
|
var/datum/idle_behavior/idle_behavior = null
|
|
|
|
// 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
|
|
/// Delay between movements. This is on the controller so we can keep the movement datum singleton
|
|
var/movement_delay = 0.2 SECONDS
|
|
|
|
// TODO: Move the variables below into the blackboard at some point.
|
|
|
|
/// AI paused time
|
|
var/paused_until = 0
|
|
/// Can this AI idle?
|
|
var/can_idle = TRUE
|
|
/// 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
|
|
/// 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()
|
|
|
|
if(idle_behavior)
|
|
idle_behavior = new idle_behavior()
|
|
|
|
if(!isnull(new_pawn)) // unit tests need the ai_controller to exist in isolation due to list schenanigans i hate it here
|
|
possess_pawn(new_pawn)
|
|
|
|
/datum/ai_controller/Destroy(force)
|
|
set_ai_status(AI_STATUS_OFF)
|
|
unpossess_pawn(FALSE)
|
|
set_movement_target(type, null)
|
|
if(ai_movement.moving_controllers[src])
|
|
ai_movement.stop_moving_towards(src)
|
|
|
|
LAZYCLEARLIST(planned_behaviors)
|
|
LAZYCLEARLIST(planning_subtrees)
|
|
LAZYCLEARLIST(current_behaviors)
|
|
|
|
return ..()
|
|
|
|
/// Sets the current movement target, with an optional param to override the movement behavior
|
|
/datum/ai_controller/proc/set_movement_target(source, atom/target, datum/ai_movement/new_movement)
|
|
if(current_movement_target)
|
|
UnregisterSignal(current_movement_target, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_PREQDELETED))
|
|
if(!isnull(target) && !isatom(target))
|
|
stack_trace("[pawn]'s current movement target is [target.type], not an atom")
|
|
cancel_actions()
|
|
return
|
|
movement_target_source = source
|
|
current_movement_target = target
|
|
if(!isnull(current_movement_target))
|
|
RegisterSignal(current_movement_target, COMSIG_MOVABLE_MOVED, PROC_REF(on_movement_target_move))
|
|
RegisterSignal(current_movement_target, COMSIG_PARENT_PREQDELETED, PROC_REF(on_movement_target_delete))
|
|
if(new_movement)
|
|
change_ai_movement_type(new_movement)
|
|
|
|
/// 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 planning_subtrees with a new set based on argument provided.
|
|
/// List provided must contain specifically typepaths
|
|
/datum/ai_controller/proc/replace_planning_subtrees(list/typepaths_of_new_subtrees)
|
|
planning_subtrees = typepaths_of_new_subtrees
|
|
init_subtrees()
|
|
|
|
/// Loops over the subtrees in planning_subtrees and looks at the ai_controllers to grab a reference
|
|
/// Ensure planning_subtrees are typepaths and not instances/references before executing this!
|
|
/datum/ai_controller/proc/init_subtrees()
|
|
if(!LAZYLEN(planning_subtrees))
|
|
return
|
|
var/list/temp_subtree_list = list()
|
|
for(var/subtree in planning_subtrees)
|
|
var/subtree_instance = SSai_controllers.ai_subtrees[subtree]
|
|
temp_subtree_list += subtree_instance
|
|
planning_subtrees = temp_subtree_list
|
|
|
|
/// Proc to move from one pawn to another. This will destroy the target's existing controller.
|
|
/datum/ai_controller/proc/possess_pawn(atom/new_pawn)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
|
|
if(!istype(new_pawn))
|
|
qdel(src)
|
|
CRASH("[src] attempted to attach to null pawn!")
|
|
|
|
if(pawn) // Reset any old signals
|
|
unpossess_pawn(FALSE)
|
|
|
|
if(istype(new_pawn.ai_controller)) // Existing AI, kill it.
|
|
QDEL_NULL(new_pawn.ai_controller)
|
|
|
|
if(try_possess_pawn(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
|
|
|
|
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_PARENT_QDELETING, PROC_REF(on_pawn_qdeleted))
|
|
update_able_to_run()
|
|
setup_able_to_run()
|
|
|
|
/datum/ai_controller/proc/on_movement_target_move(datum/source)
|
|
SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
|
|
check_target_max_distance()
|
|
|
|
/datum/ai_controller/proc/on_movement_target_delete(atom/source)
|
|
SIGNAL_HANDLER // COMSIG_PARENT_PREQDELETED
|
|
set_movement_target(source = type, target = null)
|
|
|
|
/datum/ai_controller/proc/check_target_max_distance()
|
|
if(get_dist(current_movement_target, pawn) > max_target_distance)
|
|
cancel_actions()
|
|
|
|
/// 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())
|
|
|
|
/**
|
|
* Gets the AI status we expect the AI controller to be on at this current
|
|
* moment. Returns` AI_STATUS_OFF` if it's inhabited by a client and shouldn't be,
|
|
* if it's dead and cannot act while dead, or is on a z level without clients.
|
|
* Returns AI_STATUS_ON otherwise.
|
|
*/
|
|
/datum/ai_controller/proc/get_expected_ai_status()
|
|
. = AI_STATUS_ON
|
|
|
|
if(!ismob(pawn))
|
|
return
|
|
|
|
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
|
|
|
|
if(mob_pawn.stat == DEAD)
|
|
. = AI_STATUS_OFF
|
|
|
|
var/turf/pawn_turf = get_turf(mob_pawn)
|
|
#ifdef GAME_TESTS
|
|
if(!pawn_turf)
|
|
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
|
|
|
|
/// 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 // COMSIG_MOVABLE_Z_CHANGED
|
|
|
|
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(new_turf)
|
|
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/try_possess_pawn(atom/new_pawn)
|
|
return
|
|
|
|
/// Proc for deinitializing the pawn to the old controller
|
|
/datum/ai_controller/proc/unpossess_pawn(destroy)
|
|
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))
|
|
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
|
|
pawn.ai_controller = null
|
|
pawn = null
|
|
if(destroy)
|
|
qdel(src)
|
|
|
|
/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
|
|
GLOB.move_manager.stop_looping(pawn) //stop moving
|
|
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
|
|
|
|
/datum/ai_controller/proc/ai_can_interact()
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
return !QDELETED(pawn)
|
|
|
|
///Interact with objects
|
|
/datum/ai_controller/proc/ai_interact(target, intent, list/modifiers)
|
|
if(!ai_can_interact())
|
|
return FALSE
|
|
|
|
var/atom/final_target = isdatum(target) ? target : blackboard[target] //incase we got a blackboard key instead
|
|
|
|
if(QDELETED(final_target))
|
|
return FALSE
|
|
var/params = list2params(modifiers)
|
|
var/mob/living/living_pawn = pawn
|
|
if(isnull(intent))
|
|
living_pawn.ClickOn(final_target, params)
|
|
return TRUE
|
|
|
|
var/old_intent = living_pawn.a_intent
|
|
living_pawn.a_intent = intent
|
|
living_pawn.ClickOn(final_target, params)
|
|
living_pawn.a_intent = old_intent
|
|
return TRUE
|
|
|
|
/// Runs any actions that are currently running
|
|
/datum/ai_controller/process(seconds_per_tick)
|
|
// AI controllers were implemented on /tg/ after the deltatime conversion:
|
|
// https://github.com/tgstation/tgstation/pull/52981
|
|
// The practical side-effect of this is that the values we call "seconds_per_tick"
|
|
// 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(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
|
|
|
|
/// 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
|
|
|
|
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_behavior/current_behavior as anything in current_behaviors)
|
|
if(LAZYACCESS(planned_behaviors, current_behavior))
|
|
continue
|
|
var/list/arguments = list(src, FALSE)
|
|
var/list/stored_arguments = behavior_args[type]
|
|
if(stored_arguments)
|
|
arguments += stored_arguments
|
|
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)
|
|
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
|
|
ai_status = new_ai_status
|
|
SSai_controllers.ai_controllers_by_status[new_ai_status] += src
|
|
switch(ai_status)
|
|
if(AI_STATUS_ON)
|
|
START_PROCESSING(SSai_behaviors, src)
|
|
if(AI_STATUS_OFF, AI_STATUS_IDLE)
|
|
STOP_PROCESSING(SSai_behaviors, src)
|
|
cancel_actions()
|
|
|
|
/datum/ai_controller/proc/pause_ai(time)
|
|
paused_until = world.time + time
|
|
|
|
/datum/ai_controller/proc/modify_cooldown(datum/ai_behavior/behavior, new_cooldown)
|
|
behavior_cooldowns[behavior] = new_cooldown
|
|
|
|
/// Call this to add a behavior to the stack.
|
|
/datum/ai_controller/proc/queue_behavior(behavior_type, ...)
|
|
var/datum/ai_behavior/behavior = GET_AI_BEHAVIOR(behavior_type)
|
|
if(!behavior)
|
|
CRASH("Behavior [behavior_type] not found.")
|
|
var/list/arguments = args.Copy()
|
|
arguments[1] = src
|
|
|
|
// 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)
|
|
return
|
|
|
|
if(!behavior.setup(arglist(arguments)))
|
|
return
|
|
LAZYADDASSOC(current_behaviors, behavior, TRUE)
|
|
LAZYADDASSOC(planned_behaviors, behavior, TRUE)
|
|
arguments.Cut(1, 2)
|
|
if(length(arguments))
|
|
behavior_args[behavior_type] = arguments
|
|
else
|
|
behavior_args -= behavior_type
|
|
SEND_SIGNAL(src, AI_CONTROLLER_BEHAVIOR_QUEUED(behavior_type), arguments)
|
|
|
|
/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]
|
|
if(stored_arguments)
|
|
arguments += stored_arguments
|
|
|
|
var/process_flags = behavior.perform(arglist(arguments))
|
|
if(process_flags & AI_BEHAVIOR_DELAY)
|
|
behavior_cooldowns[behavior] = world.time + behavior.get_cooldown(src)
|
|
if(process_flags & AI_BEHAVIOR_FAILED)
|
|
arguments[1] = src
|
|
arguments[2] = FALSE
|
|
behavior.finish_action(arglist(arguments))
|
|
else if(process_flags & AI_BEHAVIOR_SUCCEEDED)
|
|
arguments[1] = src
|
|
arguments[2] = TRUE
|
|
behavior.finish_action(arglist(arguments))
|
|
|
|
/datum/ai_controller/proc/cancel_actions()
|
|
if(!LAZYLEN(current_behaviors))
|
|
return
|
|
for(var/datum/ai_behavior/current_behavior as anything in current_behaviors)
|
|
fail_behavior(current_behavior)
|
|
|
|
/datum/ai_controller/proc/fail_behavior(datum/ai_behavior/current_behavior)
|
|
var/list/arguments = list(src, FALSE)
|
|
var/list/stored_arguments = behavior_args[current_behavior.type]
|
|
if(stored_arguments)
|
|
arguments += stored_arguments
|
|
current_behavior.finish_action(arglist(arguments))
|
|
|
|
/// 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 // COMSIG_MOB_STATCHANGE
|
|
reset_ai_status()
|
|
update_able_to_run()
|
|
|
|
/datum/ai_controller/proc/on_sentience_gained()
|
|
SIGNAL_HANDLER // COMSIG_MOB_LOGIN
|
|
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 // COMSIG_MOB_LOGOUT
|
|
UnregisterSignal(pawn, COMSIG_MOB_LOGOUT)
|
|
set_ai_status(AI_STATUS_IDLE)
|
|
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()
|
|
SIGNAL_HANDLER // COMSIG_PARENT_QDELETING
|
|
set_ai_status(AI_STATUS_OFF)
|
|
set_movement_target(type, null)
|
|
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()
|
|
return
|
|
|
|
/// Returns the minimum required distance to preform one of our current behaviors.
|
|
/// Honestly this should just be cached or something but fuck you
|
|
/datum/ai_controller/proc/get_minimum_distance()
|
|
var/minimum_distance = max_target_distance
|
|
// right now I'm just taking the shortest minimum distance of our current behaviors, at some point in the future
|
|
// we should let whatever sets the current_movement_target also set the min distance and max path length
|
|
// (or at least cache it on the controller)
|
|
for(var/datum/ai_behavior/iter_behavior as anything in current_behaviors)
|
|
if(iter_behavior.required_distance < minimum_distance)
|
|
minimum_distance = iter_behavior.required_distance
|
|
return minimum_distance
|
|
|
|
/datum/ai_controller/proc/planning_failed()
|
|
on_failed_planning_timeout = TRUE
|
|
set_ai_status(get_expected_ai_status())
|
|
addtimer(CALLBACK(src, PROC_REF(resume_planning)), AI_FAILED_PLANNING_COOLDOWN)
|
|
|
|
/datum/ai_controller/proc/resume_planning()
|
|
on_failed_planning_timeout = FALSE
|
|
set_ai_status(get_expected_ai_status())
|
|
|
|
/// 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(isdatum(tracked_datum)) { \
|
|
var/datum/_tracked_datum = tracked_datum; \
|
|
if(!HAS_TRAIT_FROM(_tracked_datum, TRAIT_AI_TRACKING, "[UID()]_[key]")) { \
|
|
RegisterSignal(_tracked_datum, COMSIG_PARENT_QDELETING, PROC_REF(sig_remove_from_blackboard), override = TRUE); \
|
|
ADD_TRAIT(_tracked_datum, TRAIT_AI_TRACKING, "[UID()]_[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, "[UID()]_[key]"); \
|
|
if(!HAS_TRAIT(_tracked_datum, TRAIT_AI_TRACKING)) { \
|
|
UnregisterSignal(_tracked_datum, COMSIG_PARENT_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.
|
|
*/
|
|
/datum/ai_controller/proc/set_blackboard_key(key, thing)
|
|
// 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)
|
|
|
|
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
|
|
|
|
/**
|
|
* 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))
|
|
|
|
/**
|
|
* 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(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 // COMSIG_PARENT_QDELETING
|
|
|
|
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)
|
|
var/associated_value = next_to_clear[inner_value]
|
|
// 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
|
|
|
|
// 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
|
|
|
|
#undef TRACK_AI_DATUM_TARGET
|
|
#undef CLEAR_AI_DATUM_TARGET
|
|
#undef TRAIT_AI_TRACKING
|