From 76bc3afae7271a9f480e9f2ee816ee5bae412e7c Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:49:01 +0200 Subject: [PATCH] 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 :cl: 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. /:cl: --- code/datums/components/tether.dm | 180 ++++++++++++++---- .../fluid_spread/effects_foam.dm | 2 +- .../mod/modules/modules_engineering.dm | 18 +- 3 files changed, 164 insertions(+), 36 deletions(-) diff --git a/code/datums/components/tether.dm b/code/datums/components/tether.dm index cbbd2b1c18f..b2aedaeb9ff 100644 --- a/code/datums/components/tether.dm +++ b/code/datums/components/tether.dm @@ -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 diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm index 788740e9f32..f3394e21e19 100644 --- a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm +++ b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm @@ -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! diff --git a/code/modules/mod/modules/modules_engineering.dm b/code/modules/mod/modules/modules_engineering.dm index 54c6f09b8b1..5838e2391cd 100644 --- a/code/modules/mod/modules/modules_engineering.dm +++ b/code/modules/mod/modules/modules_engineering.dm @@ -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