Files
Bubberstation/code/datums/components/tether.dm
SmArtKar 76bc3afae7 Significantly improves MODtether behavior, fixes multiple bugs and a potential server crash (#92807)
## About The Pull Request

This PR rewrites how MODtethers behave when something appears in their
way, when they run out of slack or get stuck on a corner. Currently,
first case would freeze you in place, while the other two can result in
the tether being blocked by an object which shouldn't interrupt the
beam. Now when it cannot find a direct LOS or get too far from the
owner, the tether will attempt to move to the side a bit to (hopefully)
slide around the corner or whatever object is blocking it, as long as
its distance permits it to do so. They'll also automatically snap if
they cannot find LOS even after trying to move to the side.

Also fixed multiple bugs and a potential crash due to recursion stemming
from MODtethers.
Additionally, while looking around path_info code I found that foam was
calling its passibility check on an incorrect turf, checking if an
object from the target turf could arrive to the turf itself, rather than
sourcing it from its own location. I also fixed that, should prevent
foam from going through directional objects

## Why It's Good For The Game

This should make them less of a pain to use in-game, especially when the
other half is connected to an unanchored object.

## Changelog
🆑
qol: Significantly improved MODtether behavior, they should be easier to
work with now.
fix: Fixed multiple MODtethers bugs and a potential server crash related
to them.
fix: Fixed foam sometimes passing through directional windows.
/🆑
2025-09-09 11:49:01 +02:00

349 lines
14 KiB
Plaintext

/// Creates a tether between two objects that limits movement range. Tether requires LOS and can be adjusted by left/right clicking its
/datum/component/tether
dupe_mode = COMPONENT_DUPE_ALLOWED
/// Other side of the tether
var/atom/tether_target
/// Maximum (and initial) distance that this tether can be adjusted to
var/max_dist
/// What the tether is going to be called
var/tether_name
/// Current extension distance
var/cur_dist
/// Embedded item that the tether "should" originate from
var/atom/embed_target
/// Beam effect
var/datum/beam/tether_beam
/// Tether module if we were created by one
var/obj/item/mod/module/tether/parent_module
/// Ref of source, if any, for TRAIT_TETHER_ATTACHED we add
var/tether_trait_source
/// If TRUE, only add TRAIT_TETHER_ATTACHED to our parent
var/no_target_trait
/// Are we currently attempting to forcefully shorten the tether?
var/force_moving_target = FALSE
/datum/component/tether/Initialize(atom/tether_target, max_dist = 7, tether_name, atom/embed_target = null, start_distance = null, \
parent_module = null, tether_trait_source = null, no_target_trait = FALSE)
if(!ismovable(parent) || !istype(tether_target) || !tether_target.loc)
return COMPONENT_INCOMPATIBLE
if(isatom(tether_trait_source))
stack_trace("Tried to add a [src.type] with a tether_trait_source that is a hard ref! Use REF() first before passing!")
return COMPONENT_INCOMPATIBLE
src.tether_target = tether_target
src.embed_target = embed_target
src.max_dist = max_dist
src.parent_module = parent_module
src.tether_trait_source = tether_trait_source
src.no_target_trait = no_target_trait
cur_dist = max_dist
if (start_distance != null)
cur_dist = start_distance
var/datum/beam/beam = tether_target.Beam(parent, "line", 'icons/obj/clothing/modsuit/mod_modules.dmi', emissive = FALSE, beam_type = /obj/effect/ebeam/tether)
tether_beam = beam
if (ispath(tether_name, /atom))
var/atom/tmp = tether_name
src.tether_name = initial(tmp.name)
else
src.tether_name = tether_name
if (!isnull(tether_trait_source))
ADD_TRAIT(parent, TRAIT_TETHER_ATTACHED, tether_trait_source)
if (!no_target_trait)
ADD_TRAIT(tether_target, TRAIT_TETHER_ATTACHED, tether_trait_source)
/datum/component/tether/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(check_tether))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(check_snap))
RegisterSignal(tether_target, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(check_tether))
RegisterSignal(tether_target, COMSIG_MOVABLE_MOVED, PROC_REF(check_snap))
RegisterSignal(tether_target, COMSIG_QDELETING, PROC_REF(on_delete))
RegisterSignal(tether_beam.visuals, COMSIG_CLICK, PROC_REF(beam_click))
// Also snap if the beam gets deleted, more of a backup check than anything
RegisterSignal(tether_beam.visuals, COMSIG_QDELETING, PROC_REF(on_delete))
if (!isnull(embed_target))
RegisterSignal(embed_target, COMSIG_ITEM_UNEMBEDDED, PROC_REF(on_embedded_removed))
RegisterSignal(embed_target, COMSIG_QDELETING, PROC_REF(on_delete))
if (!isnull(parent_module))
RegisterSignals(parent_module, list(COMSIG_QDELETING, COMSIG_MOVABLE_MOVED, COMSIG_MOD_TETHER_SNAP), PROC_REF(snap))
RegisterSignal(parent_module, COMSIG_MODULE_TRIGGERED, PROC_REF(on_parent_use))
/datum/component/tether/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOVABLE_MOVED))
if (!isnull(tether_trait_source))
REMOVE_TRAIT(parent, TRAIT_TETHER_ATTACHED, tether_trait_source)
if (!QDELETED(tether_beam))
UnregisterSignal(tether_beam.visuals, list(COMSIG_CLICK, COMSIG_QDELETING))
qdel(tether_beam)
if (!QDELETED(embed_target))
UnregisterSignal(embed_target, list(COMSIG_ITEM_UNEMBEDDED, COMSIG_QDELETING))
if (!QDELETED(tether_target))
UnregisterSignal(tether_target, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
if (!isnull(tether_trait_source) && !no_target_trait)
REMOVE_TRAIT(tether_target, TRAIT_TETHER_ATTACHED, tether_trait_source)
SEND_SIGNAL(tether_target, COMSIG_ATOM_TETHER_SNAPPED, tether_trait_source)
SEND_SIGNAL(parent, COMSIG_ATOM_TETHER_SNAPPED, tether_trait_source)
/datum/component/tether/proc/check_tether(atom/source, new_loc)
SIGNAL_HANDLER
if (check_snap(is_moving = TRUE))
return
if (!isturf(new_loc))
to_chat(source, span_warning("[tether_name] prevents you from entering [new_loc]!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
// If this was called, we know its a movable
var/atom/movable/movable_source = source
var/atom/movable/anchor = (source == tether_target ? parent : tether_target)
// Ignore distance limitations if we're attempting to move the other part of the tether
if (get_dist(anchor, new_loc) > cur_dist && !force_moving_target)
if (!istype(anchor) || anchor.anchored || anchor.move_resist > movable_source.move_force)
to_chat(source, span_warning("[tether_name] runs out of slack and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
force_moving_target = TRUE
if (!try_adjust_position(anchor, new_loc, source))
force_moving_target = FALSE
to_chat(source, span_warning("[tether_name] runs out of slack and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
force_moving_target = FALSE
var/atom/blocker = check_line(anchor, new_loc, list(source))
if (blocker)
if (!istype(anchor) || anchor.anchored || anchor.move_resist > movable_source.move_force)
to_chat(source, span_warning("[tether_name] runs out of slack and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
// If the tether would snag on something when we move, see if we could move to the side to get LOS back
if (!try_adjust_position(anchor, new_loc, source))
to_chat(source, span_warning("[tether_name] catches on [blocker] and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
if (get_dist(anchor, new_loc) != cur_dist || !ismovable(source) || force_moving_target)
return
var/datum/drift_handler/handler = movable_source.drift_handler
if (handler)
handler.remove_angle_force(get_angle(anchor, source))
/// Try adjust the anchor's position to move closer to the target or regain LOS
/// true_source is an optional argument in case we're looking for a LOS/closer turf to a new location rather than the actual owner, and need to ignore them
/datum/component/tether/proc/try_adjust_position(atom/movable/anchor, atom/target, atom/true_source)
if (!istype(anchor) || anchor.anchored)
return FALSE
if (anchor.x == target.x && anchor.y == target.y)
return TRUE
var/datum/can_pass_info/pass_info = new(no_id = TRUE)
pass_info.pass_flags = anchor.pass_flags
pass_info.movement_type = anchor.movement_type
if (isliving(anchor))
var/mob/living/living_anchor = anchor
pass_info.is_living = TRUE
pass_info.mob_size = living_anchor.mob_size
pass_info.incorporeal_move = living_anchor.incorporeal_move
pass_info.is_bot = isbot(living_anchor)
var/list/pass_turfs = list()
var/turf/anchor_turf = get_turf(anchor)
var/primary_cardinal = null
if (abs(anchor.x - target.x) > abs(anchor.y - target.y))
primary_cardinal = anchor.x > target.x ? WEST : EAST
else
primary_cardinal = anchor.y > target.y ? SOUTH : NORTH
var/anchor_dir = get_dir(anchor, target)
if (primary_cardinal == anchor_dir)
pass_turfs += get_step(anchor, primary_cardinal)
else if (get_dist(anchor, get_step(target, REVERSE_DIR(primary_cardinal))) >= get_dist(anchor, get_step(target, REVERSE_DIR(anchor_dir))))
pass_turfs += get_step(anchor, anchor_dir)
pass_turfs += get_step(anchor, primary_cardinal)
else
pass_turfs += get_step(anchor, primary_cardinal)
pass_turfs += get_step(anchor, anchor_dir)
// Make a list of secondary dirs to try and sidestep into if we cannot go in our main direction
var/list/match_dirs = null
if (primary_cardinal == NORTH || primary_cardinal == SOUTH)
match_dirs = list(EAST, WEST)
else
match_dirs = list(NORTH, SOUTH)
for (var/match_dir in match_dirs)
if ((match_dir & primary_cardinal) != anchor_dir)
pass_turfs += get_step(anchor, match_dir | primary_cardinal)
for (var/match_dir in match_dirs)
pass_turfs += get_step(anchor, match_dir)
// The final list is something like (direct path, main cardinal, diagonals to main cardinal, 90* dirs to the main cardinal)
// Whichever one we manage to move onto first is our pick
var/list/turf_cache = list()
for (var/turf/pass_turf in pass_turfs) // keep the typecheck in case we accidentally go out of map bounds
if (pass_turf.density || get_dist(pass_turf, target) > cur_dist)
continue
if (anchor_turf.LinkBlockedWithAccess(pass_turf, pass_info))
continue
if (check_line(pass_turf, target, list(anchor, true_source), turf_cache))
continue
if (anchor.Move(pass_turf))
return TRUE
return FALSE
/// Check LOS availibility of a tile, returns a blocking atom, if any
/// turf_cache could be used to reduce the amount of calculations if multiple lines are cast and expected to have multiple shared turfs
/// by sharing located results
/datum/component/tether/proc/check_line(atom/start, atom/end, list/to_ignore, list/turf_cache = list())
var/turf/start_loc = get_turf(start)
var/turf/end_loc = get_turf(end)
var/start_dir = get_dir(start_loc, end_loc)
var/end_dir = REVERSE_DIR(start_dir)
var/list/turf/turf_line = get_line(start_loc, end_loc)
for (var/turf/line_turf in turf_line)
if (turf_cache[line_turf])
return turf_cache[line_turf]
if (line_turf.density && line_turf != start_loc && line_turf != end_loc)
turf_cache[line_turf] = line_turf
return line_turf
if (line_turf == start_loc)
for (var/atom/in_turf in line_turf)
if (in_turf.density && (in_turf.flags_1 & ON_BORDER_1) && (in_turf.dir & start_dir) && in_turf != start && !(in_turf in to_ignore))
turf_cache[line_turf] = in_turf
return in_turf
continue
if (line_turf == end_loc)
for (var/atom/in_turf in line_turf)
if (in_turf.density && (in_turf.flags_1 & ON_BORDER_1) && (in_turf.dir & end_dir) && in_turf != end && !(in_turf in to_ignore))
turf_cache[line_turf] = in_turf
return in_turf
continue
for (var/atom/in_turf in line_turf)
if (!in_turf.density || (in_turf in to_ignore))
continue
if ((in_turf.flags_1 & ON_BORDER_1))
// If the tether is in a straight line, we can ignore border objects parallel to us
if (!(in_turf.dir & start_dir) && !(in_turf.dir & end_dir))
continue
// Also ignore objects that we don't intersect with
if (!(get_step(in_turf, in_turf.dir) in turf_line))
continue
turf_cache[line_turf] = in_turf
return in_turf
turf_cache[line_turf] = null
/datum/component/tether/proc/check_snap(atom/movable/source, atom/old_loc, dir, forced, list/old_locs, is_moving = FALSE)
SIGNAL_HANDLER
var/atom/atom_target = parent
// Something broke us out, snap the tether
if (get_dist(atom_target, tether_target) > cur_dist + 1 || !isturf(atom_target.loc) || !isturf(tether_target.loc) || atom_target.z != tether_target.z)
snap()
else if (!is_moving && check_line(atom_target, tether_target) && !(try_adjust_position(atom_target, tether_target) || try_adjust_position(tether_target, atom_target)))
snap()
/datum/component/tether/proc/snap()
SIGNAL_HANDLER
var/atom/atom_target = parent
atom_target.visible_message(span_warning("[atom_target]'s [tether_name] snaps!"), span_userdanger("Your [tether_name] snaps!"), span_hear("You hear a cable snapping."))
playsound(atom_target, 'sound/effects/snap.ogg', 50, TRUE)
qdel(src)
/datum/component/tether/proc/on_parent_use(obj/item/mod/module/module, atom/target)
SIGNAL_HANDLER
if (get_turf(target) == get_turf(tether_target))
return MOD_ABORT_USE
/datum/component/tether/proc/on_delete()
SIGNAL_HANDLER
qdel(src)
/datum/component/tether/proc/on_embedded_removed(atom/source, mob/living/victim)
SIGNAL_HANDLER
parent.AddComponent(/datum/component/tether, source, max_dist, tether_name, cur_dist)
qdel(src)
/datum/component/tether/proc/beam_click(atom/source, atom/location, control, params, mob/user)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(process_beam_click), source, location, params, user)
/datum/component/tether/proc/process_beam_click(atom/source, atom/location, params, mob/user)
var/turf/nearest_turf
for (var/turf/line_turf in get_line(get_turf(parent), get_turf(tether_target)))
if (user.CanReach(line_turf))
nearest_turf = line_turf
break
if (isnull(nearest_turf))
return
if (!user.can_perform_action(nearest_turf))
nearest_turf.balloon_alert(user, "cannot reach!")
return
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, CTRL_CLICK))
location.balloon_alert(user, "cutting the tether...")
if (!do_after(user, 2 SECONDS, user, (user == parent || user == tether_target) ? IGNORE_USER_LOC_CHANGE|IGNORE_TARGET_LOC_CHANGE : NONE))
return
qdel(src)
location.balloon_alert(user, "tether cut!")
to_chat(parent, span_danger("Your [tether_name] has been cut!"))
return
if (LAZYACCESS(modifiers, RIGHT_CLICK))
if (cur_dist >= max_dist)
location.balloon_alert(user, "no coil remaining!")
return
cur_dist += 1
location.balloon_alert(user, "tether extended")
return
if (cur_dist <= 0)
location.balloon_alert(user, "too short!")
return
if (cur_dist > CEILING(get_dist(parent, tether_target), 1))
cur_dist -= 1
location.balloon_alert(user, "tether shortened")
return
if (!ismovable(parent) && !ismovable(tether_target))
location.balloon_alert(user, "too short!")
return
var/atom/movable/movable_parent = parent
var/atom/movable/movable_target = tether_target
if (istype(movable_parent) && !movable_parent.anchored && movable_parent.move_resist <= movable_target.move_force && movable_parent.Move(get_step(movable_parent.loc, get_dir(movable_parent, movable_target))))
cur_dist -= 1
location.balloon_alert(user, "tether shortened")
return
if (istype(movable_target) && !movable_target.anchored && movable_target.move_resist <= movable_parent.move_force && movable_target.Move(get_step(movable_target.loc, get_dir(movable_target, movable_parent))))
cur_dist -= 1
location.balloon_alert(user, "tether shortened")
return
location.balloon_alert(user, "too short!")
/obj/effect/ebeam/tether
mouse_opacity = MOUSE_OPACITY_ICON