From cab87c97e5c15e444e7195dd3d7586da63bceead Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sun, 30 Jul 2023 22:24:44 +0200 Subject: [PATCH] [MIRROR] Add leash component to pAIs that keeps them within range instead of directly teleporting them back, increases default range to max range [MDB IGNORE] (#22745) * Add leash component to pAIs that keeps them within range instead of directly teleporting them back, increases default range to max range (#77030) ## About The Pull Request Tries to keep pAIs in range of their owner by moving them closer when the owner moves, rather than jarringly teleporting every time the owner gets out of range. Does this by calculating the closest path a nearby tile and forcefully moving you there. Still a bit janky at times but is better than teleporting directly onto the owner 100% of the time I feel. Also prevents you from moving out of range, rather than forcefully teleporting you back. Increases the default pAI range to the maximum (9 tiles) ## Why It's Good For The Game New leashing makes being a leashed pAI significantly less jarring and obvious. Ideally we would also have a visible max range too. Default pAI range was pretty small in my testing and I think it's not unreasonable to think a lot of people won't bother changing it. That they are leashed at all is the important part. ## Changelog :cl: qol: pAIs now try to stay within range of their owner, and teleport back only when necessary qol: Default max pAI range has been changed to the maximum range you can choose (9 tiles) /:cl: --------- Co-authored-by: Jacquerel * Add leash component to pAIs that keeps them within range instead of directly teleporting them back, increases default range to max range * Fixes the leash enable/disable * A workaround because the procs are private. * Update card.dm * Update card.dm what I get for rushing * Update card.dm --------- Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Co-authored-by: Jacquerel Co-authored-by: Giz <13398309+vinylspiders@users.noreply.github.com> --- code/__DEFINES/dcs/signals/signals_leash.dm | 11 ++ code/__DEFINES/pai.dm | 2 + code/datums/components/leash.dm | 180 ++++++++++++++++++ code/modules/pai/card.dm | 13 +- code/modules/pai/pai.dm | 24 +-- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/leash.dm | 102 ++++++++++ .../code/datums/components/leash.dm | 30 +++ .../master_files/code/modules/pai/card.dm | 12 +- .../master_files/code/modules/pai/pai.dm | 16 -- tgstation.dme | 4 +- 11 files changed, 345 insertions(+), 50 deletions(-) create mode 100644 code/__DEFINES/dcs/signals/signals_leash.dm create mode 100644 code/datums/components/leash.dm create mode 100644 code/modules/unit_tests/leash.dm create mode 100644 modular_skyrat/master_files/code/datums/components/leash.dm delete mode 100644 modular_skyrat/master_files/code/modules/pai/pai.dm diff --git a/code/__DEFINES/dcs/signals/signals_leash.dm b/code/__DEFINES/dcs/signals/signals_leash.dm new file mode 100644 index 00000000000..4f83d790903 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_leash.dm @@ -0,0 +1,11 @@ +/// Called when a /datum/component/leash must forcibly teleport the parent to the owner. +/// Fired on the object with the leash component. +#define COMSIG_LEASH_FORCE_TELEPORT "leash_force_teleport" + +/// Called when a /datum/component/leash plans on pathfinding to the target, if out of range. +/// Fired on the object with the leash component. +#define COMSIG_LEASH_PATH_STARTED "leash_path_started" + +/// Called when a /datum/component/leash finishes its pathfinding to the target. +/// Fired on the object with the leash component. +#define COMSIG_LEASH_PATH_COMPLETE "leash_path_complete" diff --git a/code/__DEFINES/pai.dm b/code/__DEFINES/pai.dm index ed616a357a6..356cbc28843 100644 --- a/code/__DEFINES/pai.dm +++ b/code/__DEFINES/pai.dm @@ -15,6 +15,8 @@ #define HOLOFORM_MAX_RANGE 25 // SKYRAT EDIT CHANGE - ORIGINAL: #define HOLOFORM_MAX_RANGE 9 /// Minimum distance you can set the holoform leash #define HOLOFORM_MIN_RANGE 3 +/// Default holoform leash distance +#define HOLOFORM_DEFAULT_RANGE HOLOFORM_MAX_RANGE /// UI action to toggle huds #define PAI_TOGGLE_MEDICAL_HUD 0 diff --git a/code/datums/components/leash.dm b/code/datums/components/leash.dm new file mode 100644 index 00000000000..ac8bc271cfe --- /dev/null +++ b/code/datums/components/leash.dm @@ -0,0 +1,180 @@ +/// Keeps the parent within the distance of its owner as naturally as possible, +/// but teleporting if necessary. +/datum/component/leash + /// The owner of the leash. If this is qdeleted, the leash is as well. + var/atom/movable/owner + + /// The maximum distance you can move from your owner + var/distance + + /// The object type to create on the old turf when forcibly teleporting out + var/force_teleport_out_effect + + /// The object type to create on the new turf when forcibly teleporting out + var/force_teleport_in_effect + + VAR_PRIVATE + // Pathfinding can yield, so only move us closer if this is the best one + current_path_tick = 0 + last_completed_path_tick = 0 + + performing_path_move = FALSE + +/datum/component/leash/Initialize( + atom/movable/owner, + distance = 3, + force_teleport_out_effect, + force_teleport_in_effect, +) + . = ..() + + if (!ismovable(parent)) + stack_trace("Parent must be a movable") + return COMPONENT_INCOMPATIBLE + + if (!ismovable(owner)) + stack_trace("[owner] (owner) is not a movable") + return COMPONENT_INCOMPATIBLE + + if (!isnum(distance)) + stack_trace("[distance] (distance) must be a number") + return COMPONENT_INCOMPATIBLE + + if (!isnull(force_teleport_out_effect) && !ispath(force_teleport_out_effect)) + stack_trace("force_teleport_out_effect must be null or a path, not [force_teleport_out_effect]") + return COMPONENT_INCOMPATIBLE + + if (!isnull(force_teleport_in_effect) && !ispath(force_teleport_in_effect)) + stack_trace("force_teleport_in_effect must be null or a path, not [force_teleport_in_effect]") + return COMPONENT_INCOMPATIBLE + + src.owner = owner + src.distance = distance + src.force_teleport_out_effect = force_teleport_out_effect + src.force_teleport_in_effect = force_teleport_in_effect + + RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(on_owner_qdel)) + + var/static/list/container_connections = list( + COMSIG_MOVABLE_MOVED = PROC_REF(on_owner_moved), + ) + + AddComponent(/datum/component/connect_containers, owner, container_connections) + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_owner_moved)) + RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_parent_pre_move)) + + check_distance() + +/datum/component/leash/Destroy() + owner = null + return ..() + +/datum/component/leash/proc/set_distance(distance) + ASSERT(isnum(distance)) + src.distance = distance + check_distance() + +/datum/component/leash/proc/on_owner_qdel() + SIGNAL_HANDLER + PRIVATE_PROC(TRUE) + + qdel(src) + +/datum/component/leash/proc/on_owner_moved(atom/movable/source) + SIGNAL_HANDLER + PRIVATE_PROC(TRUE) + + check_distance() + +/datum/component/leash/proc/on_parent_pre_move(atom/movable/source, atom/new_location) + SIGNAL_HANDLER + PRIVATE_PROC(TRUE) + + if (performing_path_move) + return NONE + + var/turf/new_location_turf = get_turf(new_location) + if (get_dist(new_location_turf, owner) <= distance) + return NONE + + if (ismob(source)) + source.balloon_alert(source, "too far!") + + return COMPONENT_MOVABLE_BLOCK_PRE_MOVE + +/datum/component/leash/proc/check_distance() + set waitfor = FALSE + PRIVATE_PROC(TRUE) + + if (get_dist(parent, owner) <= distance) + return + + SEND_SIGNAL(parent, COMSIG_LEASH_PATH_STARTED) + + current_path_tick += 1 + var/our_path_tick = current_path_tick + + var/list/path = get_path_to(parent, owner, mintargetdist = distance) + + if (last_completed_path_tick > our_path_tick) + return + + last_completed_path_tick = our_path_tick + + commit_path(path) + +/datum/component/leash/proc/commit_path(list/turf/path) + SHOULD_NOT_SLEEP(TRUE) + PRIVATE_PROC(TRUE) + + performing_path_move = TRUE + + var/atom/movable/movable_parent = parent + + for (var/turf/to_move as anything in path) + // Could be an older path, don't make us teleport back + if (!to_move.Adjacent(parent)) + continue + + if (!movable_parent.Move(to_move)) + force_teleport_back("bad path step") + return + + if (get_dist(parent, owner) > distance) + force_teleport_back("incomplete path") + + performing_path_move = FALSE + SEND_SIGNAL(parent, COMSIG_LEASH_PATH_COMPLETE) + +/datum/component/leash/proc/force_teleport_back(reason) + PRIVATE_PROC(TRUE) + + var/atom/movable/movable_parent = parent + + SSblackbox.record_feedback("tally", "leash_force_teleport_back", 1, reason) + + if (force_teleport_out_effect) + new force_teleport_out_effect(movable_parent.loc) + + movable_parent.forceMove(get_turf(owner)) + + if (force_teleport_in_effect) + new force_teleport_in_effect(movable_parent.loc) + + if (ismob(movable_parent)) + movable_parent.balloon_alert(movable_parent, "moved out of range!") + + SEND_SIGNAL(parent, COMSIG_LEASH_FORCE_TELEPORT) + +/// A debug spawner that will create a corgi leashed to a bike horn, plus a beam +/obj/effect/spawner/debug_leash + +/obj/effect/spawner/debug_leash/Initialize(mapload) + . = ..() + + var/obj/item/bikehorn/bike_horn = new(loc) + var/mob/living/basic/pet/dog/corgi/corgi = new(loc) + + corgi.AddComponent(/datum/component/leash, bike_horn) + + corgi.Beam(bike_horn) diff --git a/code/modules/pai/card.dm b/code/modules/pai/card.dm index ec810f9d924..cc6f22e1ec5 100644 --- a/code/modules/pai/card.dm +++ b/code/modules/pai/card.dm @@ -67,20 +67,9 @@ /obj/item/pai_card/Initialize(mapload) . = ..() - var/static/list/containers_connections = list(COMSIG_MOVABLE_MOVED = PROC_REF(card_moved)) - AddComponent(/datum/component/connect_containers, tracked = src, connections = containers_connections) update_appearance() SSpai.pai_card_list += src -/obj/item/pai_card/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) - . = ..() - card_moved() - -/// Called when we, our loc, or our loc's loc, or our loc's loc's loc, or etc has moved -/obj/item/pai_card/proc/card_moved() - SIGNAL_HANDLER - pai?.check_distance() - /obj/item/pai_card/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] is staring sadly at [src]! [user.p_They()] can't keep living without real human intimacy!")) return OXYLOSS @@ -128,7 +117,7 @@ name = pai.name, transmit = pai.can_transmit, receive = pai.can_receive, - range = pai.leashed_distance, + range = pai.leash.distance, ) return data diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm index 04f673ee9f9..5ae643db31f 100644 --- a/code/modules/pai/pai.dm +++ b/code/modules/pai/pai.dm @@ -37,8 +37,6 @@ var/can_transmit = TRUE /// The card we inhabit var/obj/item/pai_card/card - /// The maximum distance we can travel away from our pai card - var/leashed_distance = 5 /// The current chasis that will appear when in holoform var/chassis = "repairbot" /// Toggles whether the pAI can hold encryption keys or not @@ -67,6 +65,8 @@ var/ram = 100 /// Toggles whether the Security HUD is active or not var/secHUD = FALSE + /// The current leash to the owner + var/datum/component/leash/leash // Onboard Items /// Atmospheric analyzer @@ -221,6 +221,7 @@ pai_card.set_personality(src) card = pai_card forceMove(pai_card) + leash = AddComponent(/datum/component/leash, pai_card, HOLOFORM_DEFAULT_RANGE, force_teleport_out_effect = /obj/effect/temp_visual/guardian/phase/out) addtimer(VARSET_WEAK_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_INIT_TIME) if(!holoform) add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), PAI_FOLDED) @@ -228,19 +229,6 @@ RegisterSignal(src, COMSIG_LIVING_CULT_SACRIFICED, PROC_REF(on_cult_sacrificed)) -/mob/living/silicon/pai/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) - . = ..() - check_distance() - -/// Checks if we're in range of our pai card -/mob/living/silicon/pai/proc/check_distance() - SIGNAL_HANDLER - if (get_dist(get_turf(card), get_turf(src)) <= leashed_distance) - return - to_chat(src, span_warning("You moved out of range of your holotransmitter!")) - new /obj/effect/temp_visual/guardian/phase/out(loc) - forceMove(get_turf(card)) - /mob/living/silicon/pai/make_laws() laws = new /datum/ai_laws/pai() return TRUE @@ -453,9 +441,7 @@ /// Updates the distance we can be from our pai card /mob/living/silicon/pai/proc/increment_range(increment_amount) - var/new_distance = leashed_distance + increment_amount + var/new_distance = leash.distance + increment_amount if (new_distance < HOLOFORM_MIN_RANGE || new_distance > HOLOFORM_MAX_RANGE) return - leashed_distance = new_distance - if (increment_amount < 0) - check_distance() + leash.set_distance(new_distance) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index a41e31f8043..508a37aa86b 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -156,6 +156,7 @@ #include "keybinding_init.dm" #include "knockoff_component.dm" #include "language_transfer.dm" +#include "leash.dm" #include "lesserform.dm" #include "limbsanity.dm" #include "liver.dm" diff --git a/code/modules/unit_tests/leash.dm b/code/modules/unit_tests/leash.dm new file mode 100644 index 00000000000..2372ebca7df --- /dev/null +++ b/code/modules/unit_tests/leash.dm @@ -0,0 +1,102 @@ +/datum/unit_test/leash + abstract_type = /datum/unit_test/leash + + var/atom/movable/owner + var/atom/movable/pet + + var/max_distance = 3 + + var/forcibly_teleported = FALSE + var/datum/leash_wait/leash_wait + +/datum/unit_test/leash/New() + . = ..() + + owner = allocate(/obj/item/pen) + pet = allocate(/obj/item/pen) + + pet.AddComponent(/datum/component/leash, owner, max_distance) + + RegisterSignal(pet, COMSIG_LEASH_FORCE_TELEPORT, PROC_REF(on_leash_force_teleport)) + RegisterSignal(pet, COMSIG_LEASH_PATH_STARTED, PROC_REF(on_leash_path_started)) + RegisterSignal(pet, COMSIG_LEASH_PATH_COMPLETE, PROC_REF(on_leash_path_complete)) + +/datum/unit_test/leash/Destroy() + QDEL_NULL(owner) + QDEL_NULL(pet) + + return ..() + +/datum/unit_test/leash/proc/on_leash_force_teleport() + SIGNAL_HANDLER + forcibly_teleported = TRUE + +/datum/unit_test/leash/proc/on_leash_path_complete() + SIGNAL_HANDLER + leash_wait?.completed() + +/datum/unit_test/leash/proc/on_leash_path_started() + SIGNAL_HANDLER + leash_wait?.started() + +/datum/unit_test/leash/proc/move_away(atom/movable/mover, distance) + RETURN_TYPE(/datum/leash_wait) + leash_wait = new + + for (var/_ in 1 to distance) + mover.Move(get_step(mover, EAST)) + + return leash_wait + +/datum/leash_wait + var/completed = FALSE + var/started = FALSE + + var/timed_out = FALSE + +/datum/leash_wait/New() + addtimer(VARSET_CALLBACK(src, timed_out, TRUE), 1 SECONDS) + +/datum/leash_wait/proc/completed() + completed = TRUE + +/datum/leash_wait/proc/started() + started = TRUE + +/datum/leash_wait/proc/assert_unmoved() + ASSERT(!started, "Leash started to move when it should not have") + +/datum/leash_wait/proc/wait() + ASSERT(started, "Leash doesn't plan on moving") + + UNTIL(completed || timed_out) + ASSERT(!timed_out, "Waiting for leash movement timed out, it didn't want to move") + +/// Validates the leash component will keep its parent within range without teleporting +/// when possible. +/datum/unit_test/leash/no_teleport + +/datum/unit_test/leash/no_teleport/Run() + move_away(owner, 1).assert_unmoved() + TEST_ASSERT_EQUAL(get_dist(owner, pet), 1, "Pet should not have moved") + + move_away(owner, max_distance).wait() // max_distance + 1 = we move closer, but don't teleport + TEST_ASSERT_EQUAL(get_dist(owner, pet), max_distance, "Pet should have stayed directly outside range of owner") + + TEST_ASSERT(!forcibly_teleported, "Pet should not have been forcibly teleported") + +/// Validates that the leash component will forcibly teleport when necessary +/datum/unit_test/leash/will_teleport + +/datum/unit_test/leash/will_teleport/Run() + leash_wait = new + owner.forceMove(locate(1, 1, 1)) + leash_wait.wait() + TEST_ASSERT(forcibly_teleported, "Pet should have been forcibly teleported, since they are too far away with no valid path") + +/// Validates that the leashed object cannot move outside of the max distance from owner +/datum/unit_test/leash/limit_range + +/datum/unit_test/leash/limit_range/Run() + move_away(pet, max_distance + 1) + TEST_ASSERT_EQUAL(get_dist(owner, pet), max_distance, "Pet should not have moved farther than max_distance") diff --git a/modular_skyrat/master_files/code/datums/components/leash.dm b/modular_skyrat/master_files/code/datums/components/leash.dm new file mode 100644 index 00000000000..8cbd4985e73 --- /dev/null +++ b/modular_skyrat/master_files/code/datums/components/leash.dm @@ -0,0 +1,30 @@ +/datum/component/leash + /// whether the leash is enabled or not + var/enabled = TRUE + +/// Stop listening for any signals +/datum/component/leash/proc/disable_leash() + if(!enabled) + return + + enabled = FALSE + UnregisterSignal(owner, COMSIG_MOVABLE_MOVED) + UnregisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE) + +/// Start listening for signals again +/datum/component/leash/proc/enable_leash() + if(enabled) + return + + enabled = TRUE + check_distance() // yoink them back + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_owner_moved)) + RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_parent_pre_move)) + +/// Enables or disables the leash, allowing or forbidding the PAI from leaving a specified range +/datum/component/leash/proc/toggle_leash() + to_chat(owner, span_warning("Your virtual leash has been [enabled ? "activated" : "deactivated"]!")) + if(enabled) + disable_leash() + else + enable_leash() diff --git a/modular_skyrat/master_files/code/modules/pai/card.dm b/modular_skyrat/master_files/code/modules/pai/card.dm index b804649d343..fff159f31df 100644 --- a/modular_skyrat/master_files/code/modules/pai/card.dm +++ b/modular_skyrat/master_files/code/modules/pai/card.dm @@ -1,9 +1,17 @@ +/obj/item/pai_card/download_candidate(mob/user, ckey) + . = ..() + + if(!.) + return + + pai.leash.disable_leash() // leash starts off disabled by default + /obj/item/pai_card/ui_data(mob/user) . = ..() if(!pai) return - .["pai"]["leash_enabled"] = pai.leashed + .["pai"]["leash_enabled"] = pai.leash.enabled /obj/item/pai_card/ui_act(action, list/params, datum/tgui/ui) . = ..() @@ -11,7 +19,7 @@ return TRUE if(pai && action == "toggle_leash") - pai.toggle_leash() + pai.leash.toggle_leash() return TRUE return FALSE diff --git a/modular_skyrat/master_files/code/modules/pai/pai.dm b/modular_skyrat/master_files/code/modules/pai/pai.dm deleted file mode 100644 index 33fc1cfeacc..00000000000 --- a/modular_skyrat/master_files/code/modules/pai/pai.dm +++ /dev/null @@ -1,16 +0,0 @@ -/mob/living/silicon/pai/ - /// Whether or not the PAI is subject to range limitations and able to roam freely, off by default - var/leashed = FALSE - -/// Enables or disables the leash, allowing or forbidding the PAI from leaving a specified range -/mob/living/silicon/pai/proc/toggle_leash() - leashed = !leashed - to_chat(src, span_warning("Your virtual leash has been [leashed ? "activated" : "deactivated"]!")) - if(leashed) - check_distance() // yoink them back - -/mob/living/silicon/pai/check_distance() - if(!leashed) - return - - return ..() diff --git a/tgstation.dme b/tgstation.dme index 5c2a2c64224..ba6bcb22dc0 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -289,6 +289,7 @@ #include "code\__DEFINES\dcs\signals\signals_janitor.dm" #include "code\__DEFINES\dcs\signals\signals_key.dm" #include "code\__DEFINES\dcs\signals\signals_ladder.dm" +#include "code\__DEFINES\dcs\signals\signals_leash.dm" #include "code\__DEFINES\dcs\signals\signals_lift.dm" #include "code\__DEFINES\dcs\signals\signals_light_eater.dm" #include "code\__DEFINES\dcs\signals\signals_material_container.dm" @@ -1058,6 +1059,7 @@ #include "code\datums\components\keep_me_secure.dm" #include "code\datums\components\knockoff.dm" #include "code\datums\components\label.dm" +#include "code\datums\components\leash.dm" #include "code\datums\components\light_eater.dm" #include "code\datums\components\lock_on_cursor.dm" #include "code\datums\components\manual_blinking.dm" @@ -5568,6 +5570,7 @@ #include "modular_skyrat\master_files\code\datums\bodypart_overlays\mutant_bodypart_overlay.dm" #include "modular_skyrat\master_files\code\datums\components\crafting.dm" #include "modular_skyrat\master_files\code\datums\components\fullauto.dm" +#include "modular_skyrat\master_files\code\datums\components\leash.dm" #include "modular_skyrat\master_files\code\datums\components\shielded_suit.dm" #include "modular_skyrat\master_files\code\datums\components\tippable.dm" #include "modular_skyrat\master_files\code\datums\greyscale\config_types\greyscale_configs.dm" @@ -5777,7 +5780,6 @@ #include "modular_skyrat\master_files\code\modules\mod\modules\modules_antag.dm" #include "modular_skyrat\master_files\code\modules\modular_computers\computers\item\laptop_presets.dm" #include "modular_skyrat\master_files\code\modules\pai\card.dm" -#include "modular_skyrat\master_files\code\modules\pai\pai.dm" #include "modular_skyrat\master_files\code\modules\paperwork\stamps.dm" #include "modular_skyrat\master_files\code\modules\power\cable.dm" #include "modular_skyrat\master_files\code\modules\power\powernet.dm"