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.
/🆑
This commit is contained in:
SmArtKar
2025-09-09 11:49:01 +02:00
committed by GitHub
parent d7f393e5bb
commit 76bc3afae7
3 changed files with 164 additions and 36 deletions
+147 -33
View File
@@ -19,6 +19,8 @@
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)
@@ -71,22 +73,22 @@
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_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)
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())
if (check_snap(is_moving = TRUE))
return
if (!isturf(new_loc))
@@ -96,50 +98,162 @@
// 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)
if (get_dist(anchor, new_loc) > cur_dist)
if (!istype(anchor) || anchor.anchored || !(!anchor.anchored && anchor.move_resist <= movable_source.move_force && anchor.Move(get_step_towards(anchor, new_loc))))
// 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
var/atom/blocker
var/anchor_dir = get_dir(source, anchor)
for (var/turf/line_turf in get_line(anchor, new_loc))
if (line_turf.density && line_turf != anchor.loc && line_turf != source.loc)
blocker = line_turf
break
if (line_turf == anchor.loc || line_turf == source.loc)
for (var/atom/in_turf in line_turf)
if ((in_turf.flags_1 & ON_BORDER_1) && (in_turf.dir & anchor_dir))
blocker = in_turf
break
else
for (var/atom/in_turf in line_turf)
if (in_turf.density && in_turf != source && in_turf != tether_target)
blocker = in_turf
break
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
if (!isnull(blocker))
break
force_moving_target = FALSE
var/atom/blocker = check_line(anchor, new_loc, list(source))
if (blocker)
to_chat(source, span_warning("[tether_name] catches on [blocker] and prevents you from moving!"))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
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 (get_dist(anchor, new_loc) != cur_dist || !ismovable(source))
// 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 (isnull(handler))
return
handler.remove_angle_force(get_angle(anchor, source))
if (handler)
handler.remove_angle_force(get_angle(anchor, source))
/datum/component/tether/proc/check_snap()
/// 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
@@ -151,7 +151,7 @@
info.pass_flags = PASSTABLE | PASSGRILLE | PASSMACHINE | PASSSTRUCTURE
for(var/iter_dir in GLOB.cardinals)
var/turf/spread_turf = get_step(src, iter_dir)
if(spread_turf?.density || spread_turf.LinkBlockedWithAccess(spread_turf, info))
if(spread_turf?.density || location.LinkBlockedWithAccess(spread_turf, info))
continue
var/obj/effect/particle_effect/fluid/foam/foundfoam = locate() in spread_turf //Don't spread foam where there's already foam!
@@ -222,6 +222,7 @@
anchor.pixel_x = hitx
anchor.pixel_y = hity
anchor.anchored = TRUE
anchor.reset_pixel_pos = TRUE
anchor.parent_module = parent_module
firer.AddComponent(/datum/component/tether, anchor, 7, "MODtether", parent_module = parent_module, tether_trait_source = REF(parent_module))
@@ -238,6 +239,8 @@
interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND | INTERACT_ATOM_UI_INTERACT
/// MODsuit tether module that created our projectile
var/obj/item/mod/module/tether/parent_module
/// Should we reset our pixel positions next time we move?
var/reset_pixel_pos = FALSE
/obj/item/tether_anchor/Initialize(mapload)
. = ..()
@@ -255,8 +258,12 @@
/obj/item/tether_anchor/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
return ITEM_INTERACT_SUCCESS
if (default_unfasten_wrench(user, tool))
pixel_x = 0
pixel_y = 0
reset_pixel_pos = FALSE
return ITEM_INTERACT_SUCCESS
return ITEM_INTERACT_BLOCKING
/obj/item/tether_anchor/attack_hand_secondary(mob/user, list/modifiers)
if (!can_interact(user) || !user.CanReach(src) || !isturf(loc))
@@ -274,6 +281,13 @@
user.AddComponent(/datum/component/tether, src, 7, "tether", tether_trait_source = REF(src))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/item/tether_anchor/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
if (reset_pixel_pos)
pixel_x = 0
pixel_y = 0
reset_pixel_pos = FALSE
/obj/item/tether_anchor/mouse_drop_receive(atom/target, mob/user, params)
if (!can_interact(user) || !user.CanReach(src) || !isturf(loc))
return