Experiment with replacing weakrefs in AI blackboard with deleting signals, ideally making it easier to work with and harder to cause hard deletes (#74791)

## About The Pull Request

Replaces weakref usage in AI blackboards with deleting signals

All blackboard var setting must go through setters rather than directly

## Why It's Good For The Game

This both makes it a ton easier to develop AI for, and also makes it
harder for hard deletes to sneak in, as has been seen with recent 515
prs showing hard deletes in AI blackboards

(To quantify "making it easier to develop AI", I found multiple bugs in
existing AI code due to the usage of weakrefs.)

I'm looking for `@Jacquerel` `@tralezab` 's opinions on the matter, also
maybe `@LemonInTheDark` if they're interested

## Changelog

🆑 Melbert
refactor: Mob ai refactored once again
/🆑
This commit is contained in:
MrMelbert
2023-04-23 17:07:17 -06:00
committed by GitHub
parent 23b09f8a14
commit ed2f04f486
92 changed files with 688 additions and 592 deletions
+49 -57
View File
@@ -8,14 +8,9 @@
. = ..()
if(!success) //Don't try again on this item if we failed
var/list/item_blacklist = controller.blackboard[BB_MONKEY_BLACKLISTITEMS]
var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET]
controller.set_blackboard_key_assoc(BB_MONKEY_BLACKLISTITEMS, controller.blackboard[BB_MONKEY_PICKUPTARGET], TRUE)
item_blacklist[target] = TRUE
if(istype(controller, /datum/ai_controller/monkey)) //What the fuck
controller.RegisterSignal(target, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/ai_controller/monkey,target_del))
controller.blackboard[BB_MONKEY_PICKUPTARGET] = null
controller.clear_blackboard_key(BB_MONKEY_PICKUPTARGET)
/datum/ai_behavior/monkey_equip/proc/equip_item(datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
@@ -39,7 +34,7 @@
else if(target.force > best_force)
living_pawn.drop_all_held_items()
living_pawn.put_in_hands(target)
controller.blackboard[BB_MONKEY_BEST_FORCE_FOUND] = target.force
controller.set_blackboard_key(BB_MONKEY_BEST_FORCE_FOUND, target.force)
finish_action(controller, TRUE)
return
@@ -77,7 +72,6 @@
/datum/ai_behavior/monkey_equip/pickpocket/proc/attempt_pickpocket(datum/ai_controller/controller)
var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET]
var/mob/living/victim = target.loc
var/mob/living/living_pawn = controller.pawn
@@ -89,7 +83,7 @@
victim.visible_message(span_warning("[living_pawn] starts trying to take [target] from [victim]!"), span_danger("[living_pawn] tries to take [target]!"))
controller.blackboard[BB_MONKEY_PICKPOCKETING] = TRUE
controller.set_blackboard_key(BB_MONKEY_PICKPOCKETING, TRUE)
var/success = FALSE
@@ -110,8 +104,8 @@
/datum/ai_behavior/monkey_equip/pickpocket/finish_action(datum/ai_controller/controller, success)
. = ..()
controller.blackboard[BB_MONKEY_PICKPOCKETING] = FALSE
controller.blackboard[BB_MONKEY_PICKUPTARGET] = null
controller.set_blackboard_key(BB_MONKEY_PICKPOCKETING, FALSE)
controller.clear_blackboard_key(BB_MONKEY_PICKUPTARGET)
/datum/ai_behavior/monkey_flee
@@ -128,7 +122,7 @@
// flee from anyone who attacked us and we didn't beat down
for(var/mob/living/L in view(living_pawn, MONKEY_FLEE_VISION))
if(controller.blackboard[BB_MONKEY_ENEMIES][WEAKREF(L)] && L.stat == CONSCIOUS)
if(controller.blackboard[BB_MONKEY_ENEMIES][L] && L.stat == CONSCIOUS)
target = L
break
@@ -142,14 +136,12 @@
/datum/ai_behavior/monkey_attack_mob/setup(datum/ai_controller/controller, target_key)
. = ..()
var/datum/weakref/target_ref = controller.blackboard[target_key]
set_movement_target(controller, target_ref?.resolve())
set_movement_target(controller, controller.blackboard[target_key])
/datum/ai_behavior/monkey_attack_mob/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
. = ..()
var/datum/weakref/target_ref = controller.blackboard[target_key]
var/mob/living/target = target_ref?.resolve()
var/mob/living/target = controller.blackboard[target_key]
var/mob/living/living_pawn = controller.pawn
if(!target || target.stat != CONSCIOUS)
@@ -175,7 +167,7 @@
. = ..()
var/mob/living/living_pawn = controller.pawn
SSmove_manager.stop_looping(living_pawn)
controller.blackboard[target_key] = null
controller.clear_blackboard_key(target_key)
/// attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little
/datum/ai_behavior/monkey_attack_mob/proc/monkey_attack(datum/ai_controller/controller, mob/living/target, seconds_per_tick, disarm)
@@ -193,7 +185,7 @@
living_pawn.set_combat_mode(TRUE)
if(isnull(controller.blackboard[BB_MONKEY_GUN_WORKED]))
controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE
controller.set_blackboard_key(BB_MONKEY_GUN_WORKED, TRUE)
// attack with weapon if we have one
if(living_pawn.CanReach(target, weapon))
@@ -201,7 +193,7 @@
weapon.melee_attack_chain(living_pawn, target)
else
living_pawn.UnarmedAttack(target, null, disarm ? list("right" = TRUE) : null) //Fake a right click if we're disarmin
controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // We reset their memory of the gun being 'broken' if they accomplish some other attack
controller.set_blackboard_key(BB_MONKEY_GUN_WORKED, TRUE) // We reset their memory of the gun being 'broken' if they accomplish some other attack
else if(weapon)
var/atom/real_target = target
if(prob(10)) // Artificial miss
@@ -212,27 +204,32 @@
if(gun && controller.blackboard[BB_MONKEY_GUN_WORKED] && prob(95))
// We attempt to attack even if we can't shoot so we get the effects of pulling the trigger
gun.afterattack(real_target, living_pawn, FALSE)
controller.blackboard[BB_MONKEY_GUN_WORKED] = can_shoot ? TRUE : prob(80) // Only 20% likely to notice it didn't work
controller.set_blackboard_key(BB_MONKEY_GUN_WORKED, can_shoot ? TRUE : prob(80)) // Only 20% likely to notice it didn't work
if(can_shoot)
controller.blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] = TRUE
controller.set_blackboard_key(BB_MONKEY_GUN_NEURONS_ACTIVATED, TRUE)
else
living_pawn.throw_item(real_target)
controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // 'worked'
controller.set_blackboard_key(BB_MONKEY_GUN_WORKED, TRUE) // 'worked'
// no de-aggro
if(controller.blackboard[BB_MONKEY_AGGRESSIVE])
return
// we've queued up a monkey attack on a mob which isn't already an enemy, so give them 1 threat to start
// note they might immediately reduce threat and drop from the list.
// this is fine, we're just giving them a love tap then leaving them alone.
// unless they fight back, then we retaliate
if(isnull(controller.blackboard[BB_MONKEY_ENEMIES][target]))
controller.set_blackboard_key_assoc(BB_MONKEY_ENEMIES, target, 1)
/// mob refs are uids, so this is safe
var/datum/weakref/target_ref = WEAKREF(target)
if(SPT_PROB(MONKEY_HATRED_REDUCTION_PROB, seconds_per_tick))
controller.blackboard[BB_MONKEY_ENEMIES][target_ref]--
controller.add_blackboard_key_assoc(BB_MONKEY_ENEMIES, target, -1)
// if we are not angry at our target, go back to idle
if(controller.blackboard[BB_MONKEY_ENEMIES][target_ref] <= 0)
var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES]
enemies.Remove(target_ref)
if(controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] == WEAKREF(target))
if(controller.blackboard[BB_MONKEY_ENEMIES][target] <= 0)
controller.remove_thing_from_blackboard_key(BB_MONKEY_ENEMIES, target)
if(controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] == target)
finish_action(controller, TRUE)
/datum/ai_behavior/disposal_mob
@@ -240,14 +237,13 @@
/datum/ai_behavior/disposal_mob/setup(datum/ai_controller/controller, attack_target_key, disposal_target_key)
. = ..()
var/datum/weakref/target_ref = controller.blackboard[attack_target_key]
set_movement_target(controller, target_ref?.resolve())
set_movement_target(controller, controller.blackboard[attack_target_key])
/datum/ai_behavior/disposal_mob/finish_action(datum/ai_controller/controller, succeeded, attack_target_key, disposal_target_key)
. = ..()
controller.blackboard[attack_target_key] = null //Reset attack target
controller.blackboard[BB_MONKEY_DISPOSING] = FALSE //No longer disposing
controller.blackboard[disposal_target_key] = null //No target disposal
controller.clear_blackboard_key(attack_target_key) //Reset attack target
controller.set_blackboard_key(BB_MONKEY_DISPOSING, FALSE) //No longer disposing
controller.clear_blackboard_key(disposal_target_key) //No target disposal
/datum/ai_behavior/disposal_mob/perform(seconds_per_tick, datum/ai_controller/controller, attack_target_key, disposal_target_key)
. = ..()
@@ -255,8 +251,7 @@
if(controller.blackboard[BB_MONKEY_DISPOSING]) //We are disposing, don't do ANYTHING!!!!
return
var/datum/weakref/target_ref = controller.blackboard[attack_target_key]
var/mob/living/target = target_ref?.resolve()
var/mob/living/target = controller.blackboard[attack_target_key]
var/mob/living/living_pawn = controller.pawn
set_movement_target(controller, target)
@@ -270,8 +265,7 @@
target.grabbedby(living_pawn)
return //Do the rest next turn
var/datum/weakref/disposal_ref = controller.blackboard[disposal_target_key]
var/obj/machinery/disposal/disposal = disposal_ref.resolve()
var/obj/machinery/disposal/disposal = controller.blackboard[disposal_target_key]
set_movement_target(controller, disposal)
if(!disposal)
@@ -285,12 +279,10 @@
/datum/ai_behavior/disposal_mob/proc/try_disposal_mob(datum/ai_controller/controller, attack_target_key, disposal_target_key)
var/mob/living/living_pawn = controller.pawn
var/datum/weakref/target_ref = controller.blackboard[attack_target_key]
var/mob/living/target = target_ref?.resolve()
var/datum/weakref/disposal_ref = controller.blackboard[disposal_target_key]
var/obj/machinery/disposal/disposal = disposal_ref?.resolve()
var/mob/living/target = controller.blackboard[attack_target_key]
var/obj/machinery/disposal/disposal = controller.blackboard[disposal_target_key]
controller.blackboard[BB_MONKEY_DISPOSING] = TRUE
controller.set_blackboard_key(BB_MONKEY_DISPOSING, TRUE)
if(target && disposal?.stuff_mob_in(target, living_pawn))
disposal.flush()
@@ -300,20 +292,19 @@
/datum/ai_behavior/recruit_monkeys/perform(seconds_per_tick, datum/ai_controller/controller)
. = ..()
controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN
controller.set_blackboard_key(BB_MONKEY_RECRUIT_COOLDOWN, world.time + MONKEY_RECRUIT_COOLDOWN)
var/mob/living/living_pawn = controller.pawn
for(var/mob/living/L in view(living_pawn, MONKEY_ENEMY_VISION))
if(!HAS_AI_CONTROLLER_TYPE(L, /datum/ai_controller/monkey))
for(var/mob/living/nearby_monkey in view(living_pawn, MONKEY_ENEMY_VISION))
if(!HAS_AI_CONTROLLER_TYPE(nearby_monkey, /datum/ai_controller/monkey))
continue
if(!SPT_PROB(MONKEY_RECRUIT_PROB, seconds_per_tick))
continue
var/datum/ai_controller/monkey/monkey_ai = L.ai_controller
var/datum/weakref/enemy_ref = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
var/list/enemies = L.ai_controller.blackboard[BB_MONKEY_ENEMIES]
enemies[enemy_ref] = MONKEY_RECRUIT_HATED_AMOUNT
monkey_ai.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN
// Recruited a monkey to our side
controller.set_blackboard_key(BB_MONKEY_RECRUIT_COOLDOWN, world.time + MONKEY_RECRUIT_COOLDOWN)
// Other monkeys now also hate the guy we're currently targeting
nearby_monkey.ai_controller.add_blackboard_key_assoc(BB_MONKEY_ENEMIES, controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET], MONKEY_RECRUIT_HATED_AMOUNT)
finish_action(controller, TRUE)
/datum/ai_behavior/monkey_set_combat_target/perform(seconds_per_tick, datum/ai_controller/controller, set_key, enemies_key)
@@ -322,16 +313,17 @@
for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, controller.pawn))
if(possible_enemy == controller.pawn)
continue // don't target ourselves
var/datum/weakref/enemy_ref = WEAKREF(possible_enemy)
if(!enemies[enemy_ref]) //We don't hate this creature! But we might still attack it!
if(!enemies[possible_enemy]) //We don't hate this creature! But we might still attack it!
if(!controller.blackboard[BB_MONKEY_AGGRESSIVE]) //We are not aggressive either, so we won't attack!
continue
if(HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey) && !controller.blackboard[BB_MONKEY_TARGET_MONKEYS]) //Do not target poor monkes
continue
// Weighted list, so the closer they are the more likely they are to be chosen as the enemy
valids[enemy_ref] = CEILING(100 / (get_dist(controller.pawn, possible_enemy) || 1), 1)
valids[possible_enemy] = CEILING(100 / (get_dist(controller.pawn, possible_enemy) || 1), 1)
if(!valids.len)
if(!length(valids))
finish_action(controller, FALSE)
controller.blackboard[set_key] = pick_weight(valids)
return
controller.set_blackboard_key(set_key, pick_weight(valids))
finish_action(controller, TRUE)
+5 -10
View File
@@ -52,7 +52,7 @@ have ways of interacting with a specific mob and control it.
. = ..()
if(. & AI_CONTROLLER_INCOMPATIBLE)
return
blackboard[BB_MONKEY_AGGRESSIVE] = TRUE //Angry cunt
set_blackboard_key(BB_MONKEY_AGGRESSIVE, TRUE) //Angry cunt
/datum/ai_controller/monkey/TryPossessPawn(atom/new_pawn)
if(!isliving(new_pawn))
@@ -103,8 +103,8 @@ have ways of interacting with a specific mob and control it.
/datum/ai_controller/monkey/proc/TryFindWeapon()
var/mob/living/living_pawn = pawn
if(!locate(/obj/item) in living_pawn.held_items)
blackboard[BB_MONKEY_BEST_FORCE_FOUND] = 0
if(!(locate(/obj/item) in living_pawn.held_items))
set_blackboard_key(BB_MONKEY_BEST_FORCE_FOUND, 0)
if(blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] && (locate(/obj/item/gun) in living_pawn.held_items))
// We have a gun, what could we possibly want?
@@ -135,7 +135,7 @@ have ways of interacting with a specific mob and control it.
if(weapon.force < 2) // our bite does 2 damage on avarage, no point in settling for anything less
return FALSE
blackboard[BB_MONKEY_PICKUPTARGET] = weapon
set_blackboard_key(BB_MONKEY_PICKUPTARGET, weapon)
set_movement_target(type, weapon)
if(pickpocket)
queue_behavior(/datum/ai_behavior/monkey_equip/pickpocket)
@@ -145,8 +145,7 @@ have ways of interacting with a specific mob and control it.
///Reactive events to being hit
/datum/ai_controller/monkey/proc/retaliate(mob/living/L)
var/list/enemies = blackboard[BB_MONKEY_ENEMIES]
enemies[WEAKREF(L)] += MONKEY_HATRED_AMOUNT
add_blackboard_key_assoc(BB_MONKEY_ENEMIES, L, MONKEY_HATRED_AMOUNT)
/datum/ai_controller/monkey/proc/on_attacked(datum/source, mob/attacker)
SIGNAL_HANDLER
@@ -175,7 +174,3 @@ have ways of interacting with a specific mob and control it.
/datum/ai_controller/monkey/proc/update_movespeed(mob/living/pawn)
SIGNAL_HANDLER
movement_delay = pawn.cached_multiplicative_slowdown
/datum/ai_controller/monkey/proc/target_del(target)
SIGNAL_HANDLER
blackboard[BB_MONKEY_BLACKLISTITEMS] -= target
+2 -3
View File
@@ -37,10 +37,9 @@
living_pawn.set_combat_mode(FALSE)
return SUBTREE_RETURN_FINISH_PLANNING
var/datum/weakref/target_ref = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
var/mob/living/selected_enemy = target_ref?.resolve()
var/mob/living/selected_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
if(!selected_enemy)
if(QDELETED(selected_enemy))
living_pawn.set_combat_mode(FALSE)
return