AI actions won't unassign each other's movement targets & Mice stop being scared of people if fed cheese (#72130)

## About The Pull Request

Fixes #72116 
I've had a persistent issue with basic mob actions reporting this error
and think I finally cracked it
When replanning with `AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION` it can run
`Setup` on one action leading to the plan changing, meaning that it runs
`finishCommand` to cancel all other existing commands
If you triggered a replan by setting up a movement action in the middle
of another movement action, cancelling the existing action would remove
the target already set by the current one.
We want actions to be able to remove _their own_ movement target but not
if it has been changed by something else in the intervening time.

I fixed this by passing a source every time you set a movement target
and adding a proc which only clears it if you are the source... but this
feels kind of ugly. I couldn't think of anything but if you have a
better idea let me know.

Also while I was doing this I turned it into a feature because I'm
crazy.
If you feed a mouse cheese by hand it will stop being scared of humans
and so will any other mice it attracts from eating more cheese. This is
mostly because I think industrial mouse farming to pass cargo bounties
is funny.
Mice controlled by a Regal Rat lose this behaviour and forget any past
loyalties they may have had.


https://user-images.githubusercontent.com/7483112/208779368-3bd1da0f-4191-4405-86e5-b55a58c2cd00.mp4

Oh also I removed a block about cancelling if you have another target
from the "hunt" behaviour, everywhere using this already achieves that
simply by ordering the actions in expected priority order and it was
messing with how I expected mice to work.
Now if they happen to stop by some cheese they will correctly stop
fleeing in order to eat it before continuing to run away.

## Why It's Good For The Game

Fixes a bug I kept running into.
Makes it possible to set up a mouse farm without them screaming
constantly.
Lets people more easily domesticate mice to support Ratatouille
gameplay.

## Changelog

🆑
add: Mice who are fed cheese by hand will accept humans as friends, at
least until reminded otherwise by their rightful lord.
fix: Fixed a runtime preventing mice from acting correctly when trying
to flee and also eat cheese at the same time.
/🆑
This commit is contained in:
Jacquerel
2022-12-22 00:41:36 -08:00
committed by GitHub
parent 18d7faac3a
commit 8cb4947084
19 changed files with 98 additions and 46 deletions
+11 -1
View File
@@ -23,6 +23,16 @@
controller.behavior_args -= type
if(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //If this was a movement task, reset our movement target if necessary
if(!(behavior_flags & AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH))
controller.set_movement_target(null)
clear_movement_target(controller)
if(!(behavior_flags & AI_BEHAVIOR_KEEP_MOVING_TOWARDS_TARGET_ON_FINISH))
controller.ai_movement.stop_moving_towards(controller)
/// Helper proc to ensure consistency in setting the source of the movement target
/datum/ai_behavior/proc/set_movement_target(datum/ai_controller/controller, atom/target, datum/ai_movement/new_movement)
controller.set_movement_target(type, target, new_movement)
/// Clear the controller's movement target only if it was us who last set it
/datum/ai_behavior/proc/clear_movement_target(datum/ai_controller/controller)
if (controller.movement_target_source != type)
return
controller.set_movement_target(type, null)
+4 -1
View File
@@ -19,6 +19,8 @@ multiple modular subtrees with behaviors
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
///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.
var/list/blackboard = list()
///Stored arguments for behaviors given during their initial creation
@@ -64,7 +66,8 @@ multiple modular subtrees with behaviors
return ..()
///Sets the current movement target, with an optional param to override the movement behavior
/datum/ai_controller/proc/set_movement_target(atom/target, datum/ai_movement/new_movement)
/datum/ai_controller/proc/set_movement_target(source, atom/target, datum/ai_movement/new_movement)
movement_target_source = source
current_movement_target = target
if(new_movement)
change_ai_movement_type(new_movement)
+4 -1
View File
@@ -25,7 +25,10 @@
/datum/ai_behavior/item_move_close_and_attack/setup(datum/ai_controller/controller, target_key, throw_count_key)
. = ..()
var/datum/weakref/target_ref = controller.blackboard[target_key]
controller.set_movement_target(target_ref?.resolve())
var/atom/target = target_ref?.resolve()
if (isnull(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/item_move_close_and_attack/perform(delta_time, datum/ai_controller/controller, target_key, throw_count_key)
. = ..()
@@ -12,7 +12,7 @@
var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
if (!targetting_datum)
return
controller.set_movement_target(target)
set_movement_target(controller, target)
/datum/ai_behavior/basic_melee_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
. = ..()
@@ -52,7 +52,7 @@
var/atom/target = weak_target?.resolve()
if(!target)
return FALSE
controller.set_movement_target(target)
set_movement_target(controller, target)
/datum/ai_behavior/basic_ranged_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
. = ..()
@@ -21,7 +21,7 @@
var/atom/target = weak_target?.resolve()
var/escaped = !target || !can_see(controller.pawn, target, run_distance) // If we can't see it we got away
if (escaped)
finish_action(controller, succeeded = TRUE)
finish_action(controller, succeeded = TRUE, target_key = target_key, hiding_location_key = hiding_location_key)
return
if (!in_range(controller.pawn, controller.current_movement_target))
return
@@ -30,4 +30,8 @@
/datum/ai_behavior/run_away_from_target/proc/plot_path_away_from(datum/ai_controller/controller, atom/target)
var/run_direction = get_dir(controller.pawn, get_step_away(controller.pawn, target))
var/turf/target_destination = get_ranged_target_turf(controller.pawn, run_direction, run_distance)
controller.set_movement_target(target_destination)
set_movement_target(controller, target_destination)
/datum/ai_behavior/run_away_from_target/finish_action(datum/ai_controller/controller, succeeded, target_key, hiding_location_key)
. = ..()
controller.blackboard[target_key] = null
+1 -2
View File
@@ -11,5 +11,4 @@
if(get_dist(curse_target, item_pawn) > CURSED_VIEW_RANGE)
controller.blackboard[BB_CURSE_TARGET] = null
return
controller.set_movement_target(curse_target)
controller.queue_behavior(/datum/ai_behavior/item_move_close_and_attack/ghostly/cursed)
controller.queue_behavior(/datum/ai_behavior/item_move_close_and_attack/ghostly/cursed, BB_CURSE_TARGET)
+2 -2
View File
@@ -18,7 +18,7 @@
if(!controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_TARGET])
var/atom/movable/interact_target = controller.blackboard[BB_FETCH_TARGET]
if(in_range(living_pawn, interact_target) && (isturf(interact_target.loc)))
controller.set_movement_target(interact_target)
controller.set_movement_target(type, interact_target)
if(IS_EDIBLE(interact_target))
controller.queue_behavior(/datum/ai_behavior/eat_snack)
else if(isitem(interact_target))
@@ -35,6 +35,6 @@
// if the return target isn't in sight, we'll just forget about it and carry the thing around
controller.blackboard[BB_FETCH_DELIVER_TO] = null
return
controller.set_movement_target(return_target)
controller.set_movement_target(type, return_target)
controller.queue_behavior(/datum/ai_behavior/deliver_item)
return
+6 -6
View File
@@ -31,7 +31,7 @@
/datum/ai_behavior/break_spine/setup(datum/ai_controller/controller, target_key)
. = ..()
controller.set_movement_target(controller.blackboard[target_key])
set_movement_target(controller, controller.blackboard[target_key])
/datum/ai_behavior/break_spine/perform(delta_time, datum/ai_controller/controller, target_key)
var/mob/living/batman = controller.blackboard[target_key]
@@ -90,7 +90,7 @@
var/target = target_ref?.resolve()
if(!target)
return FALSE
controller.set_movement_target(target)
set_movement_target(controller, target)
/datum/ai_behavior/use_on_object/perform(delta_time, datum/ai_controller/controller, target_key)
. = ..()
@@ -119,7 +119,7 @@
/datum/ai_behavior/give/setup(datum/ai_controller/controller, target_key)
. = ..()
var/datum/weakref/target_ref = controller.blackboard[target_key]
controller.set_movement_target(target_ref?.resolve())
set_movement_target(controller, target_ref?.resolve())
/datum/ai_behavior/give/perform(delta_time, datum/ai_controller/controller, target_key)
. = ..()
@@ -186,7 +186,7 @@
/datum/ai_behavior/consume/setup(datum/ai_controller/controller, target_key)
. = ..()
var/datum/weakref/target_ref = controller.blackboard[target_key]
controller.set_movement_target(target_ref?.resolve())
set_movement_target(controller, target_ref?.resolve())
/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, target_key, hunger_timer_key)
. = ..()
@@ -245,7 +245,7 @@
finish_action(controller, TRUE)
return
controller.set_movement_target(living_target)
set_movement_target(controller, living_target)
attack(controller, living_target)
/datum/ai_behavior/attack/finish_action(datum/ai_controller/controller, succeeded)
@@ -281,7 +281,7 @@
finish_action(controller, TRUE)
return
controller.set_movement_target(living_target)
set_movement_target(controller, living_target)
/datum/ai_behavior/follow/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
@@ -31,16 +31,10 @@
if(HAS_TRAIT(controller.pawn, TRAIT_HANDS_BLOCKED) || living_pawn.stat != CONSCIOUS)
return
// We're targeting something else for another reason
var/datum/weakref/target_weakref = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
var/atom/target = target_weakref?.resolve()
if(!QDELETED(target))
return
var/datum/weakref/hunting_weakref = controller.blackboard[target_key]
var/atom/hunted = hunting_weakref?.resolve()
// We're not hunting anything, look around for something
if(QDELETED(hunted))
if(isnull(hunted))
controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range)
// We ARE hunting something, execute the hunt.
@@ -85,7 +79,10 @@
/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
. = ..()
var/datum/weakref/hunting_weakref = controller.blackboard[hunting_target_key]
controller.set_movement_target(hunting_weakref?.resolve())
var/atom/hunt_target = hunting_weakref?.resolve()
if (isnull(hunt_target))
return FALSE
set_movement_target(controller, hunt_target)
/datum/ai_behavior/hunt_target/perform(delta_time, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
. = ..()
@@ -93,7 +90,7 @@
var/datum/weakref/hunting_weakref = controller.blackboard[hunting_target_key]
var/atom/hunted = hunting_weakref?.resolve()
if(QDELETED(hunted))
if(isnull(hunted))
//Target is gone for some reason. forget about this task!
controller[hunting_target_key] = null
finish_action(controller, FALSE, hunting_target_key)
+4 -4
View File
@@ -143,7 +143,7 @@
/datum/ai_behavior/monkey_attack_mob/setup(datum/ai_controller/controller, target_key)
. = ..()
var/datum/weakref/target_ref = controller.blackboard[target_key]
controller.set_movement_target(target_ref?.resolve())
set_movement_target(controller, target_ref?.resolve())
/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller, target_key)
. = ..()
@@ -241,7 +241,7 @@
/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]
controller.set_movement_target(target_ref?.resolve())
set_movement_target(controller, target_ref?.resolve())
/datum/ai_behavior/disposal_mob/finish_action(datum/ai_controller/controller, succeeded, attack_target_key, disposal_target_key)
. = ..()
@@ -259,7 +259,7 @@
var/mob/living/target = target_ref?.resolve()
var/mob/living/living_pawn = controller.pawn
controller.set_movement_target(target)
set_movement_target(controller, target)
if(!target)
finish_action(controller, FALSE)
@@ -272,7 +272,7 @@
var/datum/weakref/disposal_ref = controller.blackboard[disposal_target_key]
var/obj/machinery/disposal/disposal = disposal_ref.resolve()
controller.set_movement_target(disposal)
set_movement_target(controller, disposal)
if(!disposal)
finish_action(controller, FALSE)
+1 -1
View File
@@ -134,7 +134,7 @@ have ways of interacting with a specific mob and control it.
return FALSE
blackboard[BB_MONKEY_PICKUPTARGET] = weapon
set_movement_target(weapon)
set_movement_target(type, weapon)
if(pickpocket)
queue_behavior(/datum/ai_behavior/monkey_equip/pickpocket)
else
@@ -7,7 +7,7 @@
/datum/ai_behavior/vendor_crush/setup(datum/ai_controller/controller, target_key)
. = ..()
controller.set_movement_target(controller.blackboard[target_key])
set_movement_target(controller, controller.blackboard[target_key])
/datum/ai_behavior/vendor_crush/perform(delta_time, datum/ai_controller/controller)
@@ -180,7 +180,7 @@
CancelActions()
blackboard[BB_HOSTILE_ORDER_MODE] = HOSTILE_COMMAND_FOLLOW
blackboard[BB_FOLLOW_TARGET] = WEAKREF(commander)
set_movement_target(commander)
set_movement_target(type, commander)
var/mob/living/living_pawn = pawn
if(living_pawn.buckled)
queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something
@@ -211,7 +211,7 @@
if(blackboard[BB_HOSTILE_ORDER_MODE] == HOSTILE_COMMAND_ATTACK)
pawn.visible_message(span_notice("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and [blackboard[BB_HOSTILE_ATTACK_WORD]] intensely!"))
set_movement_target(pointed_movable)
set_movement_target(type, pointed_movable)
blackboard[BB_ATTACK_TARGET] = WEAKREF(pointed_movable)
if(living_pawn.buckled)
queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something
@@ -122,7 +122,7 @@
/datum/ai_behavior/leave_venue/setup(datum/ai_controller/controller, venue_key)
. = ..()
var/datum/venue/attending_venue = controller.blackboard[venue_key]
controller.set_movement_target(attending_venue.restaurant_portal)
set_movement_target(controller, attending_venue.restaurant_portal)
/datum/ai_behavior/leave_venue/perform(delta_time, datum/ai_controller/controller, venue_key)
. = ..()
@@ -15,7 +15,7 @@
controller.queue_behavior(/datum/ai_behavior/find_seat)
return SUBTREE_RETURN_FINISH_PLANNING
controller.set_movement_target(seat_marker)
controller.set_movement_target(type, seat_marker)
if(!controller.blackboard[BB_CUSTOMER_CURRENT_ORDER]) //We haven't ordered yet even ordered yet. go on! go over there and go do it!
controller.queue_behavior(/datum/ai_behavior/order_food)
+4 -4
View File
@@ -40,21 +40,21 @@
return COMPONENT_CANCEL_ATTACK_CHAIN
attacker.visible_message(span_notice("[attacker] hand-feeds [food] to [parent]."), span_notice("You hand-feed [food] to [parent]."))
qdel(food)
if(tame)
return COMPONENT_CANCEL_ATTACK_CHAIN
if (prob(tame_chance)) //note: lack of feedback message is deliberate, keep them guessing!
on_tame(attacker)
on_tame(attacker, food)
else
tame_chance += bonus_tame_chance
qdel(food)
return COMPONENT_CANCEL_ATTACK_CHAIN
///Ran once taming succeeds
/datum/component/tameable/proc/on_tame(mob/living/tamer)
/datum/component/tameable/proc/on_tame(mob/living/tamer, atom/food)
SIGNAL_HANDLER
tame = TRUE
after_tame?.Invoke(tamer)//Run custom behavior if needed
after_tame?.Invoke(tamer, food)//Run custom behavior if needed
if (isliving(parent) && isliving(tamer))
var/mob/living/tamed = parent
+40 -5
View File
@@ -29,6 +29,8 @@
ai_controller = /datum/ai_controller/basic_controller/mouse
/// Whether this rat is friendly to players
var/tame = FALSE
/// What color our mouse is. Brown, gray and white - leave blank for random.
var/body_color
/// Does this mouse contribute to the ratcap?
@@ -36,23 +38,30 @@
/// Probability that, if we successfully bite a shocked cable, that we will die to it.
var/cable_zap_prob = 85
/mob/living/basic/mouse/Initialize(mapload)
/mob/living/basic/mouse/Initialize(mapload, tame = FALSE)
. = ..()
if(contributes_to_ratcap)
SSmobs.cheeserats |= src
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
src.tame = tame
if(isnull(body_color))
body_color = pick("brown", "gray", "white")
held_state = "mouse_[body_color]" // not handled by variety element
AddElement(/datum/element/animal_variety, "mouse", body_color, FALSE)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOUSE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 10)
AddComponent(/datum/component/squeak, list('sound/effects/mousesqueek.ogg' = 1), 100, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) //as quiet as a mouse or whatever
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
make_tameable()
/mob/living/basic/mouse/proc/make_tameable()
if (tame)
faction |= FACTION_NEUTRAL
else
AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/cheese), tame_chance = 100, after_tame = CALLBACK(src, PROC_REF(tamed)))
/mob/living/basic/mouse/Destroy()
SSmobs.cheeserats -= src
@@ -137,6 +146,14 @@
if(ishuman(entered) && stat == CONSCIOUS)
to_chat(entered, span_notice("[icon2html(src, entered)] Squeak!"))
/// Called when a mouse is hand-fed some cheese, it will stop being afraid of humans
/mob/living/basic/mouse/proc/tamed(mob/living/tamer, obj/item/food/cheese/cheese)
new /obj/effect/temp_visual/heart(loc)
faction |= FACTION_NEUTRAL
tame = TRUE
try_consume_cheese(cheese)
ai_controller.CancelActions() // Interrupt any current fleeing
/// Attempts to consume a piece of cheese, causing a few effects.
/mob/living/basic/mouse/proc/try_consume_cheese(obj/item/food/cheese/cheese)
// Royal cheese will evolve us into a regal rat
@@ -183,7 +200,7 @@
/// Creates a new mouse based on this mouse's subtype.
/mob/living/basic/mouse/proc/create_a_new_rat()
new /mob/living/basic/mouse(loc)
new /mob/living/basic/mouse(loc, /* tame = */ tame)
/// Biting into a cable will cause a mouse to get shocked and die if applicable. Or do nothing if they're lucky.
/mob/living/basic/mouse/proc/try_bite_cable(obj/structure/cable/cable)
@@ -234,7 +251,10 @@
response_harm_continuous = "splats"
response_harm_simple = "splat"
gold_core_spawnable = NO_SPAWN
faction = list(FACTION_RAT, FACTION_MAINT_CREATURES, FACTION_NEUTRAL)
/mob/living/basic/mouse/brown/tom/make_tameable()
tame = TRUE
return ..()
/mob/living/basic/mouse/brown/tom/Initialize(mapload)
. = ..()
@@ -243,7 +263,7 @@
AddElement(/datum/element/pet_bonus, "squeaks happily!")
/mob/living/basic/mouse/brown/tom/create_a_new_rat()
new /mob/living/basic/mouse/brown(loc) // dominant gene
new /mob/living/basic/mouse/brown(loc, /* tame = */ tame) // dominant gene
/mob/living/basic/mouse/rat
name = "rat"
@@ -257,6 +277,9 @@
ai_controller = /datum/ai_controller/basic_controller/mouse/rat
/mob/living/basic/mouse/rat/make_tameable()
return // Unlike in real life, space rats are horrible creatures who don't like you
/mob/living/basic/mouse/rat/create_a_new_rat()
new /mob/living/basic/mouse/rat(loc)
@@ -349,9 +372,21 @@
/datum/ai_planning_subtree/find_and_hunt_target/look_for_cables,
)
/// Don't look for anything to run away from if you are distracted by being adjacent to cheese
/datum/ai_planning_subtree/flee_target/mouse
flee_behaviour = /datum/ai_behavior/run_away_from_target/mouse
/datum/ai_planning_subtree/flee_target/mouse
/datum/ai_planning_subtree/flee_target/mouse/SelectBehaviors(datum/ai_controller/controller, delta_time)
var/datum/weakref/hunting_weakref = controller.blackboard[BB_CURRENT_HUNTING_TARGET]
var/atom/hunted_cheese = hunting_weakref?.resolve()
if (!isnull(hunted_cheese))
return // We see some cheese, which is more important than our life
return ..()
/datum/ai_planning_subtree/flee_target/mouse/select
/datum/ai_behavior/run_away_from_target/mouse
run_distance = 3 // Mostly exists in small tunnels, don't get ahead of yourself
@@ -305,6 +305,7 @@
/datum/action/cooldown/riot/proc/make_minion(mob/living/new_minion, minion_desc, list/command_list = mouse_commands)
if (isbasicmob(new_minion)) // One day this will work for frogs too
new_minion.AddComponent(/datum/component/obeys_commands, command_list)
qdel(new_minion.GetComponent(/datum/component/tameable)) // Rats don't share
new_minion.befriend(owner)
new_minion.faction = owner.faction.Copy()
// Give a hint in description too
@@ -109,7 +109,7 @@
return FALSE
var/datum/ai_controller/mod_ai = new /datum/ai_controller/mod(module.mod)
module.mod.ai_controller = mod_ai
mod_ai.set_movement_target(imp_in)
mod_ai.set_movement_target(type, imp_in)
mod_ai.blackboard[BB_MOD_TARGET] = imp_in
mod_ai.blackboard[BB_MOD_IMPLANT] = src
module.mod.interaction_flags_item &= ~INTERACT_ITEM_ATTACK_HAND_PICKUP