Files
MrMelbert ed31561817 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

🆑 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
/🆑
2026-05-20 18:41:23 +02:00

354 lines
14 KiB
Plaintext

/**
* The status effect given by the cuffable_item.
* It basically binds an item to your arm, basically making it undroppable until the cuffs or item are removed, usually done by one of:
* - clicking the status alert
* - using the topic hyperlink
* - strip menu for others
* - alternatively, dismemberment or destroying the item
*/
/datum/status_effect/cuffed_item
id = "cuffed_item"
status_type = STATUS_EFFECT_MULTIPLE
alert_type = /atom/movable/screen/alert/status_effect/cuffed_item
///Reference to the item stuck into the player's hand
var/obj/item/cuffed
///Reference to the pair of handcuffs used to bind the item
var/obj/item/restraints/handcuffs/cuffs
///Reference to the bodypart we're cuffed to
var/obj/item/bodypart/arm/cuffed_to
///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()
owner.temporarilyRemoveItemFromInventory(cuffs, force = TRUE)
cuffed_to = owner.get_inactive_hand()
if(isnull(cuffed_to) || !update_link())
owner.put_in_hands(cuffs)
qdel(src)
return FALSE
RegisterSignals(cuffed, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED, COMSIG_MOVABLE_MOVED), PROC_REF(check_for_link))
RegisterSignal(cuffed, COMSIG_QDELETING, PROC_REF(cleanup_effect))
RegisterSignal(cuffed, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(on_item_update_appearance))
RegisterSignal(cuffed, COMSIG_ATOM_EXAMINE, PROC_REF(cuffed_reminder))
RegisterSignal(cuffed, COMSIG_TOPIC, PROC_REF(topic_handler))
RegisterSignal(cuffed, COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS, PROC_REF(get_strippable_action))
RegisterSignal(cuffed, COMSIG_ITEM_STRIPPABLE_ALT_ACTION, PROC_REF(do_strippable_action))
RegisterSignal(cuffed, COMSIG_ITEM_PRE_STORAGE_INSERTION, PROC_REF(block_storage_insert))
RegisterSignal(cuffed, COMSIG_ITEM_PRE_CUFFED_TO_MOB, PROC_REF(block_item_cuff))
RegisterSignals(cuffs, list(COMSIG_ITEM_EQUIPPED, COMSIG_QDELETING, COMSIG_MOVABLE_MOVED), PROC_REF(cleanup_effect))
RegisterSignal(cuffs, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(on_item_update_appearance))
cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier + 0.25, cuffed_to.click_cd_modifier + 0.25)
RegisterSignal(cuffed_to, COMSIG_QDELETING, PROC_REF(cleanup_effect))
RegisterSignal(cuffed_to, COMSIG_BODYPART_REMOVED, PROC_REF(cuffed_to_removed))
RegisterSignal(owner, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_examine_more))
owner.log_message("bound [cuffed] to [owner.p_themselves()] with restraints", LOG_GAME)
SSblackbox.record_feedback("tally", "cuffed_item", 1, cuffed.type)
return TRUE
/datum/status_effect/cuffed_item/on_remove()
//Prevent possible recursions from these signals
UnregisterSignal(cuffed, list(
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_UPDATE_APPEARANCE,
COMSIG_ITEM_PRE_CUFFED_TO_MOB,
COMSIG_ITEM_DROPPED,
COMSIG_ITEM_EQUIPPED,
COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS,
COMSIG_ITEM_PRE_STORAGE_INSERTION,
COMSIG_ITEM_STRIPPABLE_ALT_ACTION,
COMSIG_MOVABLE_MOVED,
COMSIG_QDELETING,
COMSIG_TOPIC,
))
UnregisterSignal(cuffs, list(
COMSIG_ATOM_UPDATE_APPEARANCE,
COMSIG_ITEM_EQUIPPED,
COMSIG_MOVABLE_MOVED,
COMSIG_QDELETING,
))
UnregisterSignal(cuffed_to, list(
COMSIG_BODYPART_REMOVED,
COMSIG_QDELETING,
))
UnregisterSignal(owner, list(
COMSIG_ATOM_EXAMINE_MORE,
COMSIG_CARBON_POST_ATTACH_LIMB,
))
cuffed = null
if(!QDELETED(cuffs))
cuffs.on_uncuffed(wearer = owner)
if(!QDELETED(owner) && cuffs.loc == owner && !(cuffs in owner.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD)))
cuffs.forceMove(owner.drop_location())
cuffs = null
cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier - 0.25, cuffed_to.click_cd_modifier - 0.25)
cuffed_to = null
break_leash()
///Called when someone examines the owner twice, so they can know if someone has a cuffed item
/datum/status_effect/cuffed_item/proc/on_examine_more(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
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 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
// if special we will just wait for the new limb
if(special)
UnregisterSignal(cuffed_to, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED))
cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier - 0.25, cuffed_to.click_cd_modifier - 0.25)
cuffed_to = null
RegisterSignal(owner, COMSIG_CARBON_POST_ATTACH_LIMB, PROC_REF(new_cuffed_to_attached))
return
// otherwise we wipe the effect
qdel(src)
/// Specifically if our cuffed limb is removed "specially", change it to the newly applied arm
/datum/status_effect/cuffed_item/proc/new_cuffed_to_attached(datum/source, obj/item/bodypart/limb, special)
SIGNAL_HANDLER
if(!istype(limb, /obj/item/bodypart/arm))
return
cuffed_to = limb
cuffed_to.set_speed_modifiers(cuffed_to.interaction_modifier + 0.25, cuffed_to.click_cd_modifier + 0.25)
RegisterSignal(cuffed_to, COMSIG_QDELETING, PROC_REF(cleanup_effect))
RegisterSignal(cuffed_to, COMSIG_BODYPART_REMOVED, PROC_REF(cuffed_to_removed))
UnregisterSignal(owner, COMSIG_CARBON_POST_ATTACH_LIMB)
/// Check if we need to spawn the tether effect or not
/datum/status_effect/cuffed_item/proc/check_for_link(...)
SIGNAL_HANDLER
if(!update_link())
qdel(src)
/// Updates our link and beam effect based on our state
/// Returns TRUE if we are in a valid link state, FALSE otherwise
/datum/status_effect/cuffed_item/proc/update_link()
// when held, we need no tether
if(cuffed.loc == owner)
return break_leash()
// when on the ground, init a tether between item <-> owner
if(isturf(cuffed.loc))
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))
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)
if(link_effect.parent == leash_to)
return TRUE
break_leash()
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()
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)
SIGNAL_HANDLER
if(messages)
target_storage.balloon_alert(user, "can't store [source.name] while cuffed!")
return BLOCK_STORAGE_INSERT
/// Stops double cuff
/datum/status_effect/cuffed_item/proc/block_item_cuff(obj/item/source, mob/cuffer, obj/item/cuffs)
SIGNAL_HANDLER
source.balloon_alert(cuffer, "cuffed to someone else!")
return BLOCK_ITEM_CUFF
///What happens if one of the items is moved away from the mob
/datum/status_effect/cuffed_item/proc/cleanup_effect(datum/source)
SIGNAL_HANDLER
qdel(src)
///Tell the player that the item is stuck to their hands someway. Also another way to trigger the try_remove_cuffs proc.
/datum/status_effect/cuffed_item/proc/cuffed_reminder(obj/item/item, mob/user, list/examine_texts)
SIGNAL_HANDLER
if(user == owner)
examine_texts += span_notice("[item.p_Theyre()] cuffed to you by \a [cuffs]. You can <a href='byond://?src=[REF(item)];remove_cuffs_item=1'>remove them</a>.")
/// This mainly exists as a fallback in the rare case the alert icon is not reachable (too many alerts?). You should be somewhat able to examine items while blind so all good.
/datum/status_effect/cuffed_item/proc/topic_handler(atom/source, user, href_list)
SIGNAL_HANDLER
if(user == owner && href_list["remove_cuffs_item"])
INVOKE_ASYNC(src, PROC_REF(try_remove_cuffs), user)
/datum/status_effect/cuffed_item/proc/get_strippable_action(obj/item/source, atom/owner, mob/user, list/alt_actions)
SIGNAL_HANDLER
alt_actions += "remove_item_cuffs"
/datum/status_effect/cuffed_item/proc/do_strippable_action(obj/item/source, atom/owner, mob/user, action_key)
SIGNAL_HANDLER
if(action_key != "remove_item_cuffs")
return NONE
if(source != cuffed || !isliving(user))
return NONE
INVOKE_ASYNC(src, PROC_REF(try_remove_cuffs), user)
return COMPONENT_ALT_ACTION_DONE
///The main proc responsible for attempting to remove the hancfuss.
/datum/status_effect/cuffed_item/proc/try_remove_cuffs(mob/living/user)
var/interaction_key = REF(src)
if(LAZYACCESS(user.do_afters, interaction_key))
return FALSE
if(!(user.mobility_flags & MOBILITY_USE) || (user != owner && !owner.IsReachableBy(user)))
owner.balloon_alert(user, "can't do it right now!")
return FALSE
if(user != owner)
owner.visible_message(span_notice("[user] tries to remove [cuffs] binding [cuffed] to [owner]"), span_warning("[user] is trying to remove [cuffs] binding [cuffed] to you."))
owner.balloon_alert(user, "removing cuffs...")
playsound(owner, cuffs.cuffsound, 30, TRUE, -2)
if(!do_after(user, cuffs.get_handcuff_time(user) * 1.5, owner, interaction_key = interaction_key) || QDELETED(src))
owner.balloon_alert(user, "interrupted!")
return FALSE
if(user != owner)
owner.visible_message(span_notice("[user] removes [cuffs] binding [cuffed] to [owner]"), span_warning("[user] removes [cuffs] binding [cuffed] to you."))
log_combat(user, owner, "removed restraints binding [cuffed] to")
var/obj/item/restraints/handcuffs/ref_cuffs = cuffs
var/mob/living/ref_owner = owner
ref_cuffs.forceMove(owner.drop_location()) //This will cause the status effect to delete itself, which unsets the 'cuffs' var
user.put_in_hands(ref_cuffs)
ref_owner.balloon_alert(user, "cuffs removed from item")
return TRUE
///Whenever the appearance of one of either cuffed or cuffs is updated, update the alert appearance
/datum/status_effect/cuffed_item/proc/on_item_update_appearance(datum/source)
SIGNAL_HANDLER
linked_alert.update_appearance(UPDATE_OVERLAYS)
///The status alert linked to the cuffed_item status effect
/atom/movable/screen/alert/status_effect/cuffed_item
name = "Cuffed Item"
desc = "You've an item firmly cuffed to your arm. You probably won't be accidentally dropping it somewhere anytime soon."
use_user_hud_icon = USER_HUD_STYLE_INHERIT
clickable_glow = TRUE
click_master = FALSE
/atom/movable/screen/alert/status_effect/cuffed_item/update_overlays()
. = ..()
if(!attached_effect)
return
var/datum/status_effect/cuffed_item/effect = attached_effect
. += add_atom_icon(effect.cuffed)
var/mutable_appearance/cuffs_appearance = add_atom_icon(effect.cuffs)
cuffs_appearance.transform *= 0.8
. += cuffs_appearance
/atom/movable/screen/alert/status_effect/cuffed_item/Click(location, control, params)
. = ..()
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)