Files
Bubberstation/code/datums/components/pet_commands/obeys_commands.dm
MrMelbert ed2f04f486 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
/🆑
2023-04-23 17:07:17 -06:00

91 lines
3.7 KiB
Plaintext

/**
* # Obeys Commands Component
* Manages a list of pet command datums, allowing you to boss it around
* Creates a radial menu of pet commands when this creature is alt-clicked, if it has any
*/
/datum/component/obeys_commands
/// List of commands you can give to the owner of this component
var/list/available_commands = list()
/// The available_commands parameter should be passed as a list of typepaths
/datum/component/obeys_commands/Initialize(list/command_typepaths = list())
. = ..()
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
var/mob/living/living_parent = parent
if (!living_parent.ai_controller)
return COMPONENT_INCOMPATIBLE
if (!length(command_typepaths))
CRASH("Initialised obedience component with no commands.")
for (var/command_path in command_typepaths)
var/datum/pet_command/new_command = new command_path(parent)
available_commands[new_command.command_name] = new_command
/datum/component/obeys_commands/Destroy(force, silent)
. = ..()
QDEL_NULL(available_commands)
/datum/component/obeys_commands/RegisterWithParent()
RegisterSignal(parent, COMSIG_LIVING_BEFRIENDED, PROC_REF(add_friend))
RegisterSignal(parent, COMSIG_LIVING_UNFRIENDED, PROC_REF(remove_friend))
RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine))
RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(display_menu))
/datum/component/obeys_commands/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_LIVING_BEFRIENDED, COMSIG_LIVING_UNFRIENDED, COMSIG_PARENT_EXAMINE, COMSIG_CLICK_ALT))
/// Add someone to our friends list
/datum/component/obeys_commands/proc/add_friend(datum/source, mob/living/new_friend)
SIGNAL_HANDLER
for (var/command_name as anything in available_commands)
var/datum/pet_command/command = available_commands[command_name]
INVOKE_ASYNC(command, TYPE_PROC_REF(/datum/pet_command, add_new_friend), new_friend)
/// Remove someone from our friends list
/datum/component/obeys_commands/proc/remove_friend(datum/source, mob/living/old_friend)
SIGNAL_HANDLER
for (var/command_name as anything in available_commands)
var/datum/pet_command/command = available_commands[command_name]
INVOKE_ASYNC(command, TYPE_PROC_REF(/datum/pet_command, remove_friend), old_friend)
/// Add a note about whether they will follow the instructions of the inspecting mob
/datum/component/obeys_commands/proc/on_examine(mob/living/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if (IS_DEAD_OR_INCAP(source))
return
if (!(user in source.ai_controller?.blackboard[BB_FRIENDS_LIST]))
return
examine_list += span_notice("[source.p_they(capitalized = TRUE)] seem[source.p_s()] happy to see you!")
/// Displays a radial menu of commands
/datum/component/obeys_commands/proc/display_menu(datum/source, mob/living/clicker)
SIGNAL_HANDLER
var/mob/living/living_parent = parent
if (IS_DEAD_OR_INCAP(living_parent))
return
if (!(clicker in living_parent.ai_controller?.blackboard[BB_FRIENDS_LIST]))
return // Not our friend, can't boss us around
INVOKE_ASYNC(src, PROC_REF(display_radial_menu), clicker)
/// Actually display the radial menu and then do something with the result
/datum/component/obeys_commands/proc/display_radial_menu(mob/living/clicker)
var/list/radial_options = list()
for (var/command_name as anything in available_commands)
var/datum/pet_command/command = available_commands[command_name]
var/datum/radial_menu_choice/choice = command.provide_radial_data()
if (!choice)
continue
radial_options += choice
var/pick = show_radial_menu(clicker, clicker, radial_options, require_near = TRUE, tooltips = TRUE)
if (!pick)
return
var/datum/pet_command/picked_command = available_commands[pick]
picked_command.try_activate_command(clicker)