From ed31561817d51783985026c40f63a35d0cb7ebc1 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Wed, 20 May 2026 11:41:23 -0500 Subject: [PATCH] More cuffed item fixes (#96086) ## About The Pull Request 1. Picking up both items someone has cuffed to them no longer breaks the link after dropping the first item. 2. Picking up two items linked to two separate people no longer works. You are forced to drop the item (or if nodrop is somehow applied, the cuffs are broken.) 3. If the beam is broken (such as by changing z-level), it will re-instantiate. ## Changelog :cl: Melbert fix: Fixed issues that arise from picking up two items cuffed to the same person fix: Fixed issues that arise from picking up two items cuffed to two different people fix: Fixed issues that arise from traveling z-levels while having a cuffed item /:cl: --- code/datums/components/_component.dm | 5 +- code/datums/status_effects/cuffed_item.dm | 108 +++++++++++++++++----- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm index 7dbbf24bb0e..ec60916b8ac 100644 --- a/code/datums/components/_component.dm +++ b/code/datums/components/_component.dm @@ -370,9 +370,12 @@ if((source in old_component.sources) && !old_component.allow_source_update(source)) return old_component // source already registered, no work to do - if(old_component.on_source_add(arglist(list(source) + raw_args.Copy(2))) == COMPONENT_INCOMPATIBLE) + var/source_result = old_component.on_source_add(arglist(list(source) + raw_args.Copy(2))) + if(source_result == COMPONENT_INCOMPATIBLE) stack_trace("incompatible source added to a [old_component.type]. Args: [json_encode(raw_args)]") return null + if(source_result == COMPONENT_REDUNDANT) + return null else if(!new_component) new_component = new component_type(raw_args) // There's a valid dupe mode but there's no old component, act like normal diff --git a/code/datums/status_effects/cuffed_item.dm b/code/datums/status_effects/cuffed_item.dm index 4e7b53061e4..297666efc75 100644 --- a/code/datums/status_effects/cuffed_item.dm +++ b/code/datums/status_effects/cuffed_item.dm @@ -16,15 +16,15 @@ var/obj/item/restraints/handcuffs/cuffs ///Reference to the bodypart we're cuffed to var/obj/item/bodypart/arm/cuffed_to - //Tracks the various things we apply to whatever we are cuffed to for cleanup - VAR_PRIVATE/datum/component/leash/link_effect - VAR_PRIVATE/datum/component/tug_towards/tug_effect - VAR_PRIVATE/datum/beam/beam_effect + ///Reference to the component managing the link between the mob and the item + VAR_PRIVATE/datum/component/chained_together/link_effect /datum/status_effect/cuffed_item/on_creation(mob/living/new_owner, obj/item/cuffed, obj/item/restraints/handcuffs/cuffs) src.cuffed = cuffed src.cuffs = cuffs . = ..() //throws the alert and all + if(QDELETED(src)) + return linked_alert.update_appearance(UPDATE_OVERLAYS) /datum/status_effect/cuffed_item/on_apply() @@ -108,11 +108,6 @@ examine_list += span_warning("There's [cuffed.examine_title(user)] bound to [owner.p_their()] \ [cuffed_to.plaintext_zone] by [cuffs.examine_title(user)].") -///What happens if one of the items is moved away from the mob -/datum/status_effect/cuffed_item/proc/on_displaced(datum/source) - SIGNAL_HANDLER - qdel(src) - /// What happens if the limb we're cuffed to is removed? /datum/status_effect/cuffed_item/proc/cuffed_to_removed(datum/source, mob/living/carbon/owner, special) SIGNAL_HANDLER @@ -150,35 +145,52 @@ /datum/status_effect/cuffed_item/proc/update_link() // when held, we need no tether if(cuffed.loc == owner) - break_leash() - return TRUE + return break_leash() // when on the ground, init a tether between item <-> owner if(isturf(cuffed.loc)) - init_leash(cuffed) - return TRUE + return init_leash(cuffed) // when being picked up by something else, init a tether between grabber <-> owner if(ismovable(cuffed.loc) && isturf(cuffed.loc.loc)) - init_leash(cuffed.loc) - return TRUE + return init_leash(cuffed.loc) // we have no idea where it is... return FALSE /// Inits the leash and beam effect to the given target, cleaning up old ones if necessary /datum/status_effect/cuffed_item/proc/init_leash(atom/movable/leash_to) - if(link_effect && link_effect.parent != leash_to) + if(link_effect) + if(link_effect.parent == leash_to) + return TRUE break_leash() - link_effect ||= leash_to.AddComponent(/datum/component/leash, owner = src.owner, distance = 1) - tug_effect ||= leash_to.AddComponent(/datum/component/tug_towards, tugging_to = src.owner, strength = 0.66) - beam_effect ||= leash_to.Beam(owner, "chain") + link_effect = leash_to.AddComponentFrom(REF(src), /datum/component/chained_together, chained_to = owner) + if(!QDELETED(link_effect)) + return TRUE + + // chain component failed to apply + if(ismob(leash_to)) + var/mob/leash_to_mob = leash_to + addtimer(CALLBACK(src, PROC_REF(eject_item), leash_to_mob), 1) + return TRUE + + return FALSE /datum/status_effect/cuffed_item/proc/break_leash() - QDEL_NULL(link_effect) - QDEL_NULL(tug_effect) - QDEL_NULL(beam_effect) + link_effect?.parent.RemoveComponentSource(REF(src), /datum/component/chained_together) + if(QDELETED(link_effect)) + link_effect = null + return TRUE + +// Delayed unequip after an invalid pickup. This sucks but I can't think of a better way around due to move order shenanigans +/datum/status_effect/cuffed_item/proc/eject_item(mob/leash_to_mob) + if(QDELETED(src) || cuffed.loc != leash_to_mob) + return + if(!leash_to_mob.dropItemToGround(cuffed)) + qdel(src) + return + to_chat(leash_to_mob, span_warning("[cuffs] binding [cuffed] to [owner] tugs it out of your grasp!")) /// Stops it from being stored anywhere /datum/status_effect/cuffed_item/proc/block_storage_insert(obj/item/source, atom/target_storage, mob/user, force, messages) @@ -287,3 +299,55 @@ if(.) var/datum/status_effect/cuffed_item/effect = attached_effect effect?.try_remove_cuffs(owner) + +// Chains two movable to be adjacent to each other. YMMV using this +/datum/component/chained_together + dupe_mode = COMPONENT_DUPE_SOURCES + /// Weakref to the thing we're chained to + var/datum/weakref/chained_to_weakref + // Tracks the various things we apply to whatever we are cuffed to for cleanup + VAR_PRIVATE/datum/component/leash/link_effect + VAR_PRIVATE/datum/component/tug_towards/tug_effect + VAR_PRIVATE/datum/beam/beam_effect + +/datum/component/chained_together/on_source_add(source, atom/movable/chained_to) + // more sources are only allowed if they're chaining to the same thing + // having something linked to two different things is not supported now, and will break horribly + return chained_to_weakref?.resolve() == chained_to ? ..() : COMPONENT_REDUNDANT + +/datum/component/chained_together/Initialize(atom/movable/chained_to) + if(!ismovable(parent) || !ismovable(chained_to)) + return COMPONENT_INCOMPATIBLE + + chained_to_weakref = WEAKREF(chained_to) + var/atom/movable/movable_parent = parent + link_effect = movable_parent.AddComponent(/datum/component/leash, owner = chained_to, distance = 1) + tug_effect = movable_parent.AddComponent(/datum/component/tug_towards, tugging_to = chained_to, strength = 0.66) + beam_effect = movable_parent.Beam(chained_to, "chain") + RegisterSignal(link_effect, COMSIG_QDELETING, PROC_REF(delete_self)) + RegisterSignal(tug_effect, COMSIG_QDELETING, PROC_REF(delete_self)) + RegisterSignal(beam_effect, COMSIG_QDELETING, PROC_REF(recreate_beam)) + +/datum/component/chained_together/Destroy() + UnregisterSignal(beam_effect, COMSIG_QDELETING) + UnregisterSignal(link_effect, COMSIG_QDELETING) + UnregisterSignal(tug_effect, COMSIG_QDELETING) + if(!QDELETED(link_effect)) + QDEL_NULL(link_effect) + if(!QDELETED(tug_effect)) + QDEL_NULL(tug_effect) + if(!QDELETED(beam_effect)) + QDEL_NULL(beam_effect) + return ..() + +/datum/component/chained_together/proc/recreate_beam(datum/beam/source) + SIGNAL_HANDLER + + UnregisterSignal(beam_effect, COMSIG_QDELETING) + var/atom/movable/movable_parent = parent + beam_effect = movable_parent.Beam(chained_to_weakref.resolve(), "chain") + RegisterSignal(beam_effect, COMSIG_QDELETING, PROC_REF(recreate_beam)) + +/datum/component/chained_together/proc/delete_self(datum/source) + SIGNAL_HANDLER + qdel(src)