Files
Bubberstation/code/datums/ai/generic/generic_behaviors.dm
CabinetOnFire af8f69da13 Adds "Event Logging", or EVLogging, A new debug system that allows us to track individual datums and log events on a timeline (#96035)
## About The Pull Request

This Pull Request adds a new logging system that uses a timeline to
track and visualize important events for specific datums.

This is done via a new window in which you can select a datum for
tracking, which adds it to the timeline. If this datum implements the
EVLOGGING macros, it can track important events onto this timeline. As
an example, we can log whenever an AI is deciding to make a new path, if
it decides to generate a new decisionmaking plan, it finishes an action,
or it decides to target someone/something.

We can select these events to see more information, and optionally get a
snapshot of important variables at the time this event was logged (like
the blackboard and current plan for AI controllers).

You can also filter out specific events / track info, which is done via
categories. Each event / piece of track info is given a category and if
you disable a category all events / track info in that category is
hidden. This lets you filter out things you might not care about.

<img width="2346" height="1209" alt="image"
src="https://github.com/user-attachments/assets/0763077c-e349-4c7c-b017-23d29e1d089b"
/>

_whoever thinks we didnt need advanced cleanbot logging is a noob_


In the video below I showcase how this works;
https://file.house/7nsOiqdvmSTxlsk3fs-e8g==.mp4
A cleanbot is roaming the halls, I turn on the event logger, click the
"pick target" button and click on the datum I'd like to track (the
cleanbot). This results in the cleanbot now tracking its events. I spawn
some dirt and the cleanbot decides to clean it, and I go through the
events; You can see theres different events being listed, such as when
the cleanbot starts targetting the dirt, when it cleans plan, when it
makes it JPS path and every time it moves over it.


The macros I've currently implemented are as follows:

**EVLOG_TEXT(DATUM, CATEGORY, INFO)**
Only adds text to the event logger window, no world-visuals

EVLOG_LOCATION(DATUM, CATEGORY, INFO, TURF) 
Adds text to the event logger and adds an image to where that turf is.

EVLOG_TURFS(DATUM, CATEGORY, INFO, TURFS)
Adds text to the event logger and adds an image to each turf in the
TURFS list

EVLOG_LINES(DATUM, CATEGORY, INFO, TURF_A, TURF_B)
Adds text to the event logger and adds a line from turf_a to turf_B

EVLOG_PATH(DATUM, CATEGORY, INFO, TURFS)
Adds text to the event logger and visualizes a path from A to B (same
way as the pathfinding debugger, of which I moved the visualization
before to SSPathfinder)

In terms of performance, the logger is a singleton, and events are ONLY
logged if
1. The logger is running
2. The datum has the DF_EVLOGGING flag.

This means most of the time, logging an event is a single var lookup
(Since the runner is off by default). The DF_EVLOGGING flag is off by
default as well and has to be enabled by the event logger, or set
temporarily by a dev in code.

This system can easily be extended with more event types / visualization
types as well. (I'm thinking of datumizing the ones I have now)

The TGUI is still a bit of a mess, I would love some pointers because
I'm not really good at react so I just kind of hit it with a hammer
until it did what I wanted 😎

Also, all of this is based on VisLogging from Unreal Engine, so it will
have some likeness https://unreal-garden.com/tutorials/visual-logger/

## Why It's Good For The Game

This system allows us to debug more complex systems (like basic AI) in
an understandable and clear way. While the implementation cases are not
super common right now, extending this system could make debugging these
systems much more comprehensible, and hopefully lets more developers
help us with improving these systems. (plus, we LOVE timelines)

## Changelog

🆑 CabinetOnFire
refactor: Implements "Event Logging" an improved way for programmers to
debug specific datums.
/🆑

---------

Co-authored-by: Lucy <lucy@absolucy.moe>
2026-05-13 12:37:56 +00:00

378 lines
15 KiB
Plaintext

/datum/ai_behavior/resist/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
living_pawn.ai_controller.set_blackboard_key(BB_RESISTING, TRUE)
living_pawn.execute_resist()
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/battle_screech
///List of possible screeches the behavior has
var/list/screeches
/datum/ai_behavior/battle_screech/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick(screeches))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
///Moves to target then finishes
/datum/ai_behavior/move_to_target
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/move_to_target/perform(seconds_per_tick, datum/ai_controller/controller)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/break_spine
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
action_cooldown = 0.7 SECONDS
var/give_up_distance = 10
/datum/ai_behavior/break_spine/setup(datum/ai_controller/controller, target_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/break_spine/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/mob/living/batman = controller.blackboard[target_key]
var/mob/living/big_guy = controller.pawn //he was molded by the darkness
if(QDELETED(batman) || get_dist(batman, big_guy) >= give_up_distance)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
if(batman.stat != CONSCIOUS)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
big_guy.start_pulling(batman)
big_guy.face_atom(batman)
batman.visible_message(span_warning("[batman] gets a slightly too tight hug from [big_guy]!"), span_userdanger("You feel your body break as [big_guy] embraces you!"))
for(var/zone in GLOB.all_body_zones - BODY_ZONE_HEAD)
batman.apply_damage(15, BRUTE, zone, wound_bonus = 35)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/break_spine/finish_action(datum/ai_controller/controller, succeeded, target_key)
if(succeeded)
var/mob/living/bane = controller.pawn
if(QDELETED(bane)) // pawn can be null at this point
return ..()
bane.stop_pulling()
controller.clear_blackboard_key(target_key)
return ..()
/// Use in hand the currently held item
/datum/ai_behavior/use_in_hand
behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM
/datum/ai_behavior/use_in_hand/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/pawn = controller.pawn
var/obj/item/held = pawn.get_active_held_item()
if(!held)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
pawn.activate_hand()
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/// Use the currently held item, or unarmed, on a weakref to an object in the world
/datum/ai_behavior/use_on_object
required_distance = 1
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/use_on_object/setup(datum/ai_controller/controller, target_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
if(target == controller.pawn) // this can sometimes end up as ourselves, in which case there is no reason to move
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/use_on_object/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.ai_interact(target = target, combat_mode = FALSE)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/give
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/give/setup(datum/ai_controller/controller, target_key)
. = ..()
set_movement_target(controller, controller.blackboard[target_key])
/datum/ai_behavior/give/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/mob/living/pawn = controller.pawn
var/obj/item/held_item = pawn.get_active_held_item()
var/atom/target = controller.blackboard[target_key]
if(!held_item) //if held_item is null, we pretend that action was successful
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
if(QDELETED(target) || !target.IsReachableBy(pawn))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/living_target = target
if(!isliving(living_target)) // target should reasonably only ever be set to a living mob
stack_trace("Tried to give an item to a non-living target!")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/perform_flags = try_to_give_item(controller, living_target, held_item)
if(perform_flags & AI_BEHAVIOR_FAILED)
return perform_flags
controller.PauseAi(1.5 SECONDS)
living_target.visible_message(
span_info("[pawn] starts trying to give [held_item] to [living_target]!"),
span_warning("[pawn] tries to give you [held_item]!")
)
if(!do_after(pawn, 1 SECONDS, living_target))
return AI_BEHAVIOR_DELAY | perform_flags
perform_flags |= try_to_give_item(controller, living_target, held_item, actually_give = TRUE)
return AI_BEHAVIOR_DELAY | perform_flags
/datum/ai_behavior/give/proc/try_to_give_item(datum/ai_controller/controller, mob/living/target, obj/item/held_item, actually_give)
if(QDELETED(held_item) || QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/has_left_pocket = target.can_equip(held_item, ITEM_SLOT_LPOCKET)
var/has_right_pocket = target.can_equip(held_item, ITEM_SLOT_RPOCKET)
var/has_valid_hand
for(var/hand_index in target.get_empty_held_indexes())
if(target.can_put_in_hand(held_item, hand_index))
has_valid_hand = TRUE
break
if(!has_left_pocket && !has_right_pocket && !has_valid_hand)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(!actually_give)
return AI_BEHAVIOR_DELAY
if(!has_valid_hand || prob(50))
target.equip_to_slot_if_possible(held_item, (!has_left_pocket ? ITEM_SLOT_RPOCKET : (prob(50) ? ITEM_SLOT_LPOCKET : ITEM_SLOT_RPOCKET)))
else
target.put_in_hands(held_item)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/give/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
/datum/ai_behavior/consume
action_cooldown = 2 SECONDS
/datum/ai_behavior/consume/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hunger_timer_key)
var/mob/living/living_pawn = controller.pawn
var/obj/item/target = controller.blackboard[target_key]
if(QDELETED(target) || !living_pawn.is_holding(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.ai_interact(target = living_pawn, combat_mode = FALSE)
return AI_BEHAVIOR_DELAY | (is_content(living_pawn, target) ? AI_BEHAVIOR_SUCCEEDED : AI_BEHAVIOR_FAILED)
/datum/ai_behavior/consume/finish_action(datum/ai_controller/controller, succeeded, target_key, hunger_timer_key)
. = ..()
if(!succeeded)
return
controller.set_blackboard_key(hunger_timer_key, world.time + rand(12 SECONDS, 60 SECONDS))
var/mob/living/living_pawn = controller.pawn
var/obj/item/target = controller.blackboard[target_key]
if(!QDELETED(target) && !DOING_INTERACTION_WITH_TARGET(living_pawn, target))
controller.clear_blackboard_key(target_key)
living_pawn.dropItemToGround(target) // drops empty drink glasses
for(var/obj/item/trash/trash in living_pawn.held_items)
living_pawn.dropItemToGround(trash) // drops spawned trash items
/// Check if the target is fully consumed, or being actively consumed, or if we're just bored of eating it
/datum/ai_behavior/consume/proc/is_content(mob/living/living_pawm, obj/item/target)
if(QDELETED(target))
return TRUE
if(DOING_INTERACTION_WITH_TARGET(living_pawm, target))
return TRUE
if(target.reagents?.total_volume <= 0)
return TRUE
// Even if we don't finish it all we can randomly decide to be done
return prob(10)
// navigate to target item and pick it up if we can
/datum/ai_behavior/navigate_to_and_pick_up
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
action_cooldown = 2 SECONDS
/datum/ai_behavior/navigate_to_and_pick_up/setup(datum/ai_controller/controller, target_key, drop_held = TRUE)
. = ..()
set_movement_target(controller, controller.blackboard[target_key])
/datum/ai_behavior/navigate_to_and_pick_up/setup(datum/ai_controller/controller, target_key, drop_held = TRUE)
var/mob/living/living_pawn = controller.pawn
var/obj/item/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(living_pawn.is_holding(target)) // already in hands
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
if(!target.IsReachableBy(living_pawn)) // can't reach it, despite being adjacent
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(living_pawn.get_active_held_item()) // something is in our hands already
if(!drop_held || !living_pawn.dropItemToGround(living_pawn.get_active_held_item()))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.ai_interact(target, combat_mode = FALSE)
return AI_BEHAVIOR_DELAY | (target.loc == living_pawn ? AI_BEHAVIOR_SUCCEEDED : AI_BEHAVIOR_FAILED)
/**
* Drops items in hands, very important for future behaviors that require the pawn to grab stuff
*/
/datum/ai_behavior/drop_item
/datum/ai_behavior/drop_item/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
var/list/my_held_items = living_pawn.held_items - GetBestWeapon(controller, null, living_pawn.held_items)
if(!length(my_held_items))
return AI_BEHAVIOR_FAILED | AI_BEHAVIOR_DELAY
living_pawn.dropItemToGround(pick(my_held_items))
return AI_BEHAVIOR_SUCCEEDED | AI_BEHAVIOR_DELAY
/// This behavior involves attacking a target.
/datum/ai_behavior/attack
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/attack/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn) || !isturf(living_pawn.loc))
return AI_BEHAVIOR_DELAY
var/atom/movable/attack_target = controller.blackboard[BB_ATTACK_TARGET]
if(!attack_target || !can_see(living_pawn, attack_target, length = controller.blackboard[BB_VISION_RANGE]))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/living_target = attack_target
if(istype(living_target) && (living_target.stat == DEAD))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
set_movement_target(controller, living_target)
attack(controller, living_target)
return AI_BEHAVIOR_DELAY
/datum/ai_behavior/attack/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
controller.clear_blackboard_key(BB_ATTACK_TARGET)
/// A proc representing when the mob is pushed to actually attack the target. Again, subtypes can be used to represent different attacks from different animals, or it can be some other generic behavior
/datum/ai_behavior/attack/proc/attack(datum/ai_controller/controller, mob/living/living_target)
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn))
return
living_pawn.ClickOn(living_target, list())
/// This behavior involves attacking a target.
/datum/ai_behavior/follow
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
/datum/ai_behavior/follow/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn) || !isturf(living_pawn.loc))
return AI_BEHAVIOR_DELAY
var/atom/movable/follow_target = controller.blackboard[BB_FOLLOW_TARGET]
if(!follow_target || get_dist(living_pawn, follow_target) > controller.blackboard[BB_VISION_RANGE])
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/living_target = follow_target
if(istype(living_target) && (living_target.stat == DEAD))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
set_movement_target(controller, living_target)
return AI_BEHAVIOR_DELAY
/datum/ai_behavior/follow/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
controller.clear_blackboard_key(BB_FOLLOW_TARGET)
/datum/ai_behavior/perform_emote
/datum/ai_behavior/perform_emote/perform(seconds_per_tick, datum/ai_controller/controller, emote, speech_sound)
. = ..()
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn))
return AI_BEHAVIOR_INSTANT
living_pawn.manual_emote(emote)
if(speech_sound) // Only audible emotes will pass in a sound
playsound(living_pawn, speech_sound, 80, vary = TRUE, pressure_affected =TRUE, ignore_walls = FALSE)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/perform_speech
/datum/ai_behavior/perform_speech/perform(seconds_per_tick, datum/ai_controller/controller, speech, speech_sound)
. = ..()
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn))
return AI_BEHAVIOR_INSTANT
living_pawn.say(speech, forced = "AI Controller")
if(speech_sound)
playsound(living_pawn, speech_sound, 80, vary = TRUE)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/perform_speech_radio
/datum/ai_behavior/perform_speech_radio/perform(seconds_per_tick, datum/ai_controller/controller, speech, obj/item/radio/speech_radio, list/try_channels = list(RADIO_CHANNEL_COMMON))
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn) || !istype(speech_radio) || QDELETED(speech_radio) || !length(try_channels))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
speech_radio.talk_into(living_pawn, speech, pick(try_channels))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
//song behaviors
/datum/ai_behavior/setup_instrument
/datum/ai_behavior/setup_instrument/perform(seconds_per_tick, datum/ai_controller/controller, song_instrument_key, song_lines_key)
var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key]
var/datum/song/song = song_instrument.song
var/song_lines = controller.blackboard[song_lines_key]
//just in case- it won't do anything if the instrument isn't playing
song.stop_playing()
song.ParseSong(new_song = song_lines)
song.repeat = 10
song.volume = song.max_volume - 10
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/play_instrument
/datum/ai_behavior/play_instrument/perform(seconds_per_tick, datum/ai_controller/controller, song_instrument_key)
var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key]
var/datum/song/song = song_instrument.song
song.start_playing(controller.pawn)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/find_nearby
/datum/ai_behavior/find_nearby/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/list/possible_targets = list()
for(var/atom/thing in view(2, controller.pawn))
if(!thing.mouse_opacity)
continue
if(thing.IsObscured())
continue
if(isitem(thing))
var/obj/item/item = thing
if(item.item_flags & ABSTRACT)
continue
possible_targets += thing
if(!possible_targets.len)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(target_key, pick(possible_targets))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED