From ef6fe48e08ae00cbe939cbe7498b5ecd83146e2d Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sun, 10 Dec 2023 03:58:57 +0100 Subject: [PATCH] [MIRROR] Refactors Object Possession into a Component (moar modular, less `/mob` vars) [MDB IGNORE] (#25534) * Refactors Object Possession into a Component (moar modular, less `/mob` vars) (#80160) ## About The Pull Request We have two verbs that allow any given mob to take control of an object and move it ephemerally, `/proc/possess()` and `/proc/release()`. These ones leveraged two vars present on every `/mob`: `name_archive` and `control_object`. I don't like having vars clog up my VV and this just injected snowflake behavior in a lot of spots - let's just make it a component that'll clean everything else up. This also opens up the ability to have more objects be under mob control without giving someone verbs that spit out to the blackbox as an admin verb + logs + message admins but that's a later thing. This just subs in the behavior in a nice way. Also, since it's a component, I added a small QoL that we can support now: A screen alert that allows you to get out of the possession early without navigating the stat panel for the specific verb. I think it's neat. You can also trigger the aghost keybind if that's something you want as well. Also also, nothing actually ever cleaned up `control_object` by setting it to null. This means that in the old framework, if a mob got qdelled during a possession, that would have triggered a hung ref harddel. That won't happen anymore. ## Why It's Good For The Game Two less variables taking up crud space in the VSC debugger + view variables panel. Better behavior injection that is far more reusable. Component handling this behavior allows for better extensibility of this function in the future. ![image](https://github.com/tgstation/tgstation/assets/34697715/a84238af-e014-4cff-9b4b-6cbaa36c44fd) ## Changelog :cl: admin: Object Possession has been reworked, please report any potential bugs. qol: Object Possession should now throw a screen alert for you to unpossess the object instead of you having to search the stat-panel for the "release obj" verb. You can still use the verb but it's a lot nicer now. Aghosting will also work now. /:cl: * Refactors Object Possession into a Component (moar modular, less `/mob` vars) * Refactor hydra quirk The name_archive var is gone, let's store it in the quirk instead --------- Co-authored-by: san7890 Co-authored-by: Giz <13398309+vinylspiders@users.noreply.github.com> --- code/__DEFINES/alerts.dm | 1 + .../signals/signals_mob/signals_mob_main.dm | 5 + code/_onclick/hud/alert.dm | 12 ++ code/datums/components/object_possession.dm | 127 ++++++++++++++++++ code/modules/admin/verbs/possess.dm | 52 ++----- code/modules/mob/mob.dm | 4 - code/modules/mob/mob_defines.dm | 11 -- code/modules/mob/mob_movement.dm | 23 +--- modular_skyrat/modules/hydra/code/neutral.dm | 18 ++- tgstation.dme | 1 + 10 files changed, 173 insertions(+), 81 deletions(-) create mode 100644 code/datums/components/object_possession.dm diff --git a/code/__DEFINES/alerts.dm b/code/__DEFINES/alerts.dm index cfb408898ec..1933b592d55 100644 --- a/code/__DEFINES/alerts.dm +++ b/code/__DEFINES/alerts.dm @@ -24,6 +24,7 @@ #define ALERT_EMBEDDED_OBJECT "embeddedobject" #define ALERT_SHOES_KNOT "shoealert" #define ALERT_RADIOACTIVE_AREA "radioactive_area" +#define ALERT_UNPOSSESS_OBJECT "unpossess_object" //antag related #define ALERT_HYPNOSIS "hypnosis" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm index 16c3861d8fa..2023493ac95 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm @@ -39,6 +39,11 @@ /// Should we stop the current living movement attempt #define COMSIG_MOB_CLIENT_BLOCK_PRE_LIVING_MOVE COMPONENT_MOVABLE_BLOCK_PRE_MOVE +/// From base of /client/Move(), invoked when a non-living mob is attempting to move: (list/move_args) +#define COMSIG_MOB_CLIENT_PRE_NON_LIVING_MOVE "mob_client_move_possessed_object" + /// Cancels the move attempt + #define COMSIG_MOB_CLIENT_BLOCK_PRE_NON_LIVING_MOVE COMPONENT_MOVABLE_BLOCK_PRE_MOVE + /// From base of /client/Move(): (new_loc, direction) #define COMSIG_MOB_CLIENT_PRE_MOVE "mob_client_pre_move" /// Should always match COMPONENT_MOVABLE_BLOCK_PRE_MOVE as these are interchangeable and used to block movement. diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm index 36c1bf8a799..2f3df52dfd3 100644 --- a/code/_onclick/hud/alert.dm +++ b/code/_onclick/hud/alert.dm @@ -869,6 +869,18 @@ or shoot a gun to move around via Newton's 3rd Law of Motion." carbon_owner.changeNext_move(CLICK_CD_RESIST) carbon_owner.shoes.handle_tying(carbon_owner) +/atom/movable/screen/alert/unpossess_object + name = "Unpossess" + desc = "You are possessing an object. Click this alert to unpossess it." + icon_state = "buckled" + +/atom/movable/screen/alert/unpossess_object/Click() + . = ..() + if(!.) + return + + qdel(owner.GetComponent(/datum/component/object_possession)) + // PRIVATE = only edit, use, or override these if you're editing the system as a whole // Re-render all alerts - also called in /datum/hud/show_hud() because it's needed there diff --git a/code/datums/components/object_possession.dm b/code/datums/components/object_possession.dm new file mode 100644 index 00000000000..c62f0905068 --- /dev/null +++ b/code/datums/components/object_possession.dm @@ -0,0 +1,127 @@ +/// Component that allows a user to control any object as if it were a mob. Does give the user incorporeal movement. +/datum/component/object_possession + dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS + /// Stores a reference to the obj that we are currently possessing. + var/obj/possessed = null + /// Ref to the screen object that is currently being displayed. + var/datum/weakref/screen_alert_ref = null + /** + * back up of the real name during user possession + * + * When a user possesses an object it's real name is set to the user name and this + * stores whatever the real name was previously. When possession ends, the real name + * is reset to this value + */ + var/stashed_name = null + +/datum/component/object_possession/Initialize(obj/target) + . = ..() + if(!isobj(target) || !ismob(parent)) + return COMPONENT_INCOMPATIBLE + + if(!bind_to_new_object(target)) + return COMPONENT_INCOMPATIBLE + + var/mob/user = parent + screen_alert_ref = WEAKREF(user.throw_alert(ALERT_UNPOSSESS_OBJECT, /atom/movable/screen/alert/unpossess_object)) + + // we can expect to be possessed by either a nonliving or a living mob + RegisterSignals(parent, list(COMSIG_MOB_CLIENT_PRE_LIVING_MOVE, COMSIG_MOB_CLIENT_PRE_NON_LIVING_MOVE), PROC_REF(on_move)) + RegisterSignals(parent, list(COMSIG_MOB_GHOSTIZED, COMSIG_KB_ADMIN_AGHOST_DOWN), PROC_REF(end_possession)) + +/datum/component/object_possession/Destroy() + cleanup_object_binding() + UnregisterSignal(parent, list( + COMSIG_KB_ADMIN_AGHOST_DOWN, + COMSIG_MOB_CLIENT_PRE_LIVING_MOVE, + COMSIG_MOB_CLIENT_PRE_NON_LIVING_MOVE, + COMSIG_MOB_GHOSTIZED, + )) + + var/mob/user = parent + var/atom/movable/screen/alert/alert_to_clear = screen_alert_ref?.resolve() + if(!QDELETED(alert_to_clear)) + user.clear_alert(ALERT_UNPOSSESS_OBJECT) + + return ..() + +/datum/component/object_possession/InheritComponent(datum/component/object_possession/old_component, i_am_original, obj/target) + cleanup_object_binding() + if(!bind_to_new_object(target)) + qdel(src) + + stashed_name = old_component.stashed_name + +/// Binds the mob to the object and sets up the naming and everything. +/// Returns FALSE if we don't bind, TRUE if we succeed. +/datum/component/object_possession/proc/bind_to_new_object(obj/target) + if((target.obj_flags & DANGEROUS_POSSESSION) && CONFIG_GET(flag/forbid_singulo_possession)) + to_chat(parent, "[target] is too powerful for you to possess.", confidential = TRUE) + return FALSE + + var/mob/user = parent + + stashed_name = user.real_name + possessed = target + + user.forceMove(target) + user.real_name = target.name + user.name = target.name + user.reset_perspective(target) + + target.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + + RegisterSignal(target, COMSIG_QDELETING, PROC_REF(end_possession)) + return TRUE + +/// Cleans up everything pertinent to the current possessed object. +/datum/component/object_possession/proc/cleanup_object_binding() + if(QDELETED(possessed)) + return + + var/mob/poltergeist = parent + + possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + UnregisterSignal(possessed, COMSIG_QDELETING) + + if(!isnull(stashed_name)) + poltergeist.real_name = stashed_name + poltergeist.name = stashed_name + if(ishuman(poltergeist)) + var/mob/living/carbon/human/human_user = poltergeist + human_user.name = human_user.get_visible_name() + + poltergeist.forceMove(get_turf(possessed)) + poltergeist.reset_perspective() + + possessed = null + +/** + * force move the parent object instead of the source mob. + * + * Has no sanity other than checking the possed obj's density. this means it effectively has incorporeal movement, making it only good for badminnery. + * + * We always want to return `COMPONENT_MOVABLE_BLOCK_PRE_MOVE` here regardless + */ +/datum/component/object_possession/proc/on_move(datum/source, new_loc, direct) + SIGNAL_HANDLER + . = COMPONENT_MOVABLE_BLOCK_PRE_MOVE // both signals that invoke this are explicitly tied to listen for this define as the return value + + if(QDELETED(possessed)) + return . + + if(!possessed.density) + possessed.forceMove(get_step(possessed, direct)) + else + step(possessed, direct) + + if(QDELETED(possessed)) + return . + + possessed.setDir(direct) + return . + +/// Just the overall "get me outta here" proc. +/datum/component/object_possession/proc/end_possession(datum/source) + SIGNAL_HANDLER + qdel(src) diff --git a/code/modules/admin/verbs/possess.dm b/code/modules/admin/verbs/possess.dm index 28d76abb101..63304104ab5 100644 --- a/code/modules/admin/verbs/possess.dm +++ b/code/modules/admin/verbs/possess.dm @@ -1,56 +1,32 @@ -/proc/possess(obj/O in world) +/proc/possess(obj/target in world) set name = "Possess Obj" set category = "Object" - if((O.obj_flags & DANGEROUS_POSSESSION) && CONFIG_GET(flag/forbid_singulo_possession)) - to_chat(usr, "[O] is too powerful for you to possess.", confidential = TRUE) + var/result = usr.AddComponent(/datum/component/object_possession, target) + + if(isnull(result)) // trigger a safety movement just in case we yonk + usr.forceMove(get_turf(usr)) return - var/turf/T = get_turf(O) + var/turf/target_turf = get_turf(target) + var/message = "[key_name(usr)] has possessed [target] ([target.type]) at [AREACOORD(target_turf)]" + message_admins(message) + log_admin(message) - if(T) - log_admin("[key_name(usr)] has possessed [O] ([O.type]) at [AREACOORD(T)]") - message_admins("[key_name(usr)] has possessed [O] ([O.type]) at [AREACOORD(T)]") - else - log_admin("[key_name(usr)] has possessed [O] ([O.type]) at an unknown location") - message_admins("[key_name(usr)] has possessed [O] ([O.type]) at an unknown location") - - if(!usr.control_object) //If you're not already possessing something... - usr.name_archive = usr.real_name - - usr.forceMove(O) - usr.real_name = O.name - usr.name = O.name - usr.reset_perspective(O) - usr.control_object = O - O.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) BLACKBOX_LOG_ADMIN_VERB("Possess Object") /proc/release() set name = "Release Obj" set category = "Object" - if(!usr.control_object) //lest we are banished to the nullspace realm. - return - - if(usr.name_archive) //if you have a name archived - usr.real_name = usr.name_archive - usr.name_archive = "" - usr.name = usr.real_name - if(ishuman(usr)) - var/mob/living/carbon/human/H = usr - H.name = H.get_visible_name() - - usr.control_object.RemoveElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) - usr.forceMove(get_turf(usr.control_object)) - usr.reset_perspective() - usr.control_object = null + qdel(usr.GetComponent(/datum/component/object_possession)) BLACKBOX_LOG_ADMIN_VERB("Release Object") -/proc/givetestverbs(mob/M in GLOB.mob_list) +/proc/give_possession_verbs(mob/dude in GLOB.mob_list) set desc = "Give this guy possess/release verbs" set category = "Debug" set name = "Give Possessing Verbs" - add_verb(M, GLOBAL_PROC_REF(possess)) - add_verb(M, GLOBAL_PROC_REF(release)) + + add_verb(dude, GLOBAL_PROC_REF(possess)) + add_verb(dude, GLOBAL_PROC_REF(release)) BLACKBOX_LOG_ADMIN_VERB("Give Possessing Verbs") diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index c9e41734f15..dc080a22535 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1604,10 +1604,6 @@ /mob/vv_edit_var(var_name, var_value) switch(var_name) - if(NAMEOF(src, control_object)) - var/obj/O = var_value - if(!istype(O) || (O.obj_flags & DANGEROUS_POSSESSION)) - return FALSE if(NAMEOF(src, machine)) set_machine(var_value) . = TRUE diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index ffc343f491d..be218941aea 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -89,14 +89,6 @@ /// What is the mobs real name (name is overridden for disguises etc) var/real_name = null - /** - * back up of the real name during admin possession - * - * If an admin possesses an object it's real name is set to the admin name and this - * stores whatever the real name was previously. When possession ends, the real name - * is reset to this value - */ - var/name_archive //For admin things like possession /// Default body temperature var/bodytemperature = BODYTEMP_NORMAL //310.15K / 98.6F @@ -158,9 +150,6 @@ /// Can they interact with station electronics var/has_unlimited_silicon_privilege = FALSE - ///Used by admins to possess objects. All mobs should have this var - var/obj/control_object - ///Calls relay_move() to whatever this is set to when the mob tries to move var/atom/movable/remote_control diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index a5aa8c2c145..4189d08f3bd 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -8,25 +8,6 @@ if(!iscyborg(mob) && mob.stat == CONSCIOUS) mob.dropItemToGround(mob.get_active_held_item()) return - -/** - * force move the control_object of your client mob - * - * Used in admin possession and called from the client Move proc - * ensures the possessed object moves and not the admin mob - * - * Has no sanity other than checking density - */ -/client/proc/Move_object(direct) - if(mob?.control_object) - if(mob.control_object.density) - step(mob.control_object,direct) - if(!mob.control_object) - return - mob.control_object.setDir(direct) - else - mob.control_object.forceMove(get_step(mob.control_object,direct)) - /** * Move a client in a direction * @@ -76,9 +57,9 @@ return FALSE if(HAS_TRAIT(mob, TRAIT_NO_TRANSFORM)) return FALSE //This is sorta the goto stop mobs from moving trait - if(mob.control_object) - return Move_object(direct) if(!isliving(mob)) + if(SEND_SIGNAL(mob, COMSIG_MOB_CLIENT_PRE_NON_LIVING_MOVE, new_loc, direct) & COMSIG_MOB_CLIENT_BLOCK_PRE_NON_LIVING_MOVE) + return FALSE return mob.Move(new_loc, direct) if(mob.stat == DEAD) mob.ghostize() diff --git a/modular_skyrat/modules/hydra/code/neutral.dm b/modular_skyrat/modules/hydra/code/neutral.dm index fe4165a33bf..4c92eccc10d 100644 --- a/modular_skyrat/modules/hydra/code/neutral.dm +++ b/modular_skyrat/modules/hydra/code/neutral.dm @@ -7,6 +7,8 @@ lose_text = span_danger("All of your minds become singular.") medical_record_text = "There are multiple heads and personalities affixed to one body." icon = FA_ICON_HORSE_HEAD + // remember what the name was before activation + var/original_name /datum/quirk/hydra/add(client/client_source) var/mob/living/carbon/human/hydra = quirk_holder @@ -31,17 +33,19 @@ /datum/action/innate/hydrareset/Activate() var/mob/living/carbon/human/hydra = owner - if(!hydra.name_archive) // sets the archived 'real' name if not set. - hydra.name_archive = hydra.real_name - hydra.real_name = hydra.name_archive + var/datum/quirk/hydra/hydra_quirk = hydra.get_quirk(/datum/quirk/hydra) + if(!hydra_quirk.original_name) // sets the archived 'real' name if not set. + hydra_quirk.original_name = hydra.real_name + hydra.real_name = hydra_quirk.original_name hydra.visible_message(span_notice("[hydra.name] pushes all three heads forwards; they seem to be talking as a collective."), \ - span_notice("You are now talking as [hydra.name_archive]!"), ignored_mobs=owner) + span_notice("You are now talking as [hydra_quirk.original_name]!"), ignored_mobs=owner) /datum/action/innate/hydra/Activate() //Oops, all hydra! var/mob/living/carbon/human/hydra = owner - if(!hydra.name_archive) // sets the archived 'real' name if not set. - hydra.name_archive = hydra.real_name - var/list/names = splittext(hydra.name_archive,"-") + var/datum/quirk/hydra/hydra_quirk = hydra.get_quirk(/datum/quirk/hydra) + if(!hydra_quirk.original_name) // sets the archived 'real' name if not set. + hydra_quirk.original_name = hydra.real_name + var/list/names = splittext(hydra_quirk.original_name,"-") var/selhead = input("Who would you like to speak as?","Heads:") in names hydra.real_name = selhead hydra.visible_message(span_notice("[hydra.name] pulls the rest of their heads back; and puts [selhead]'s forward."), \ diff --git a/tgstation.dme b/tgstation.dme index 7266182334d..03859db1d55 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1184,6 +1184,7 @@ #include "code\datums\components\multiple_lives.dm" #include "code\datums\components\mutant_hands.dm" #include "code\datums\components\nuclear_bomb_operator.dm" +#include "code\datums\components\object_possession.dm" #include "code\datums\components\omen.dm" #include "code\datums\components\on_hit_effect.dm" #include "code\datums\components\onwear_mood.dm"