Canreach refactor (#93165)

## About The Pull Request
ports https://github.com/DaedalusDock/daedalusdock/pull/1144
ports https://github.com/DaedalusDock/daedalusdock/pull/1147

full credit to @Kapu1178 for the juice

instead of `reacher.CanReach(target)` we now do
`target.CanBeReachedBy(reacher)`, this allows us to give special
behavior to atoms which we want to reach, which is exactly what I need
for a feature I'm working on.
## Why It's Good For The Game
allows us to be more flexible with reachability
## Changelog
🆑
refactor: refactored how reaching items works, report any oddities with
being unable to reach something you should be able to!
/🆑
This commit is contained in:
die
2025-10-07 20:28:59 +02:00
committed by GitHub
parent c82cb1734d
commit 0204ab8fdd
69 changed files with 190 additions and 162 deletions
@@ -126,7 +126,7 @@
/datum/action/cooldown/spell/touch/flesh_surgery/proc/register_held_organ(obj/item/organ/new_held_organ, obj/item/melee/touch_attack/hand)
hand.vis_contents += new_held_organ
held_organ = new_held_organ
new_held_organ.flags_1 |= IS_ONTOP_1
ADD_TRAIT(new_held_organ, TRAIT_SKIP_BASIC_REACH_CHECK, ref(src))
new_held_organ.vis_flags |= VIS_INHERIT_PLANE
RegisterSignal(new_held_organ, COMSIG_MOVABLE_MOVED, PROC_REF(unregister_held_organ))
RegisterSignal(new_held_organ, COMSIG_QDELETING, PROC_REF(unregister_held_organ))
@@ -139,7 +139,7 @@
/datum/action/cooldown/spell/touch/flesh_surgery/proc/unregister_held_organ(obj/item/organ/removed_organ)
LAZYREMOVE(attached_hand.vis_contents, removed_organ)
held_organ = null
removed_organ.flags_1 &= ~IS_ONTOP_1
REMOVE_TRAIT(removed_organ, TRAIT_SKIP_BASIC_REACH_CHECK, ref(src))
removed_organ.vis_flags &= ~VIS_INHERIT_PLANE
UnregisterSignal(removed_organ, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
// Reset item offsets
@@ -88,7 +88,7 @@
var/obj/item/sword = target
if(length(line) <= 1 || !teleport(user, line[length(line) - 1])) // teleports to the second last turf, should be adjacent to the target
return
if(!user.CanReach(stabbing, target))
if(!stabbing.IsReachableBy(user, sword?.reach))
return
sword.melee_attack_chain(user, stabbing)
if(prob(5) && check_holidays(APRIL_FOOLS))
@@ -417,7 +417,7 @@
/obj/item/soulstone/proc/check_menu(mob/user, obj/structure/constructshell/shell)
if(!istype(user))
return FALSE
if(user.incapacitated || !user.is_holding(src) || !user.CanReach(shell, src))
if(user.incapacitated || !user.is_holding(src) || !shell.IsReachableBy(user, reach))
return FALSE
return TRUE
@@ -489,7 +489,7 @@
/// Called when a ghost is chosen to become a shade.
/obj/item/soulstone/proc/on_poll_concluded(mob/living/master, mob/living/victim, mob/dead/observer/ghost)
if(isnull(victim) || master.incapacitated || !master.is_holding(src) || !master.CanReach(victim, src))
if(isnull(victim) || master.incapacitated || !master.is_holding(src) || !victim.IsReachableBy(master, reach))
return FALSE
if(isnull(ghost?.client))
to_chat(master, span_danger("There were no spirits willing to become a shade."))
+2 -2
View File
@@ -360,7 +360,7 @@
/obj/item/canvas/proc/can_select_frame(mob/user)
if(!istype(loc, /obj/structure/sign/painting))
return FALSE
if(!user?.CanReach(loc) || IS_DEAD_OR_INCAP(user))
if(!loc.IsReachableBy(user) || IS_DEAD_OR_INCAP(user))
return FALSE
if(!last_patron || !IS_WEAKREF_OF(user?.mind, last_patron))
return FALSE
@@ -930,7 +930,7 @@
var/our_dir = get_dir(user, on_wall)
var/check_dir = our_dir & (EAST|WEST) ? NORTH : EAST
var/turf/closed/wall/second_wall = get_step(on_wall, check_dir)
if(!istype(second_wall) || !user.CanReach(second_wall))
if(!istype(second_wall) || !second_wall.IsReachableBy(user))
to_chat(user, span_warning("You need a reachable wall to the [check_dir == EAST ? "right" : "left"] of this one to mount this frame!"))
return FALSE
if(check_wall_item(second_wall, our_dir, wall_external))
+3 -2
View File
@@ -103,6 +103,9 @@
playsound(loc, 'sound/machines/click.ogg', 30, TRUE)
return ITEM_INTERACT_SUCCESS
/obj/structure/fish_mount/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return TRUE
/obj/structure/fish_mount/proc/add_fish(obj/item/fish/fish, from_persistence = FALSE, catcher)
if(QDELETED(src)) // don't ever try to add a fish to one of these that's already been deleted - and get rid of the one that was created
qdel(fish)
@@ -113,7 +116,6 @@
mounted_fish.forceMove(loc)
fish.forceMove(src)
vis_contents += fish
fish.flags_1 |= IS_ONTOP_1
fish.vis_flags |= (VIS_INHERIT_PLANE|VIS_INHERIT_LAYER)
fish.interaction_flags_item &= ~INTERACT_ITEM_ATTACK_HAND_PICKUP
fish.obj_flags &= ~UNIQUE_RENAME
@@ -195,7 +197,6 @@
if(!QDELETED(mounted_fish) && (!persistence_loaded_fish || roll_for_safe_removal()))
rotate_fish(0, dir)
UnregisterSignal(mounted_fish, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW))
mounted_fish.flags_1 &= ~IS_ONTOP_1
mounted_fish.vis_flags &= ~(VIS_INHERIT_PLANE|VIS_INHERIT_LAYER)
mounted_fish.interaction_flags_item |= INTERACT_ITEM_ATTACK_HAND_PICKUP
mounted_fish.obj_flags |= UNIQUE_RENAME
+1 -1
View File
@@ -284,7 +284,7 @@ GLOBAL_LIST_EMPTY(fishing_challenges_by_user)
/datum/fishing_challenge/proc/on_float_or_user_move(datum/source)
SIGNAL_HANDLER
if(!user.CanReach(location))
if(!location.IsReachableBy(user))
user.balloon_alert(user, "too far!")
interrupt()
@@ -41,6 +41,9 @@
return
variant = rand(1,3)
/obj/machinery/griddle/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return ..() || (contained in griddled_objects)
/obj/machinery/griddle/proc/on_expose_reagent(atom/parent_atom, datum/reagent/exposing_reagent, reac_volume, methods)
SIGNAL_HANDLER
@@ -141,7 +144,6 @@
/obj/machinery/griddle/proc/AddToGrill(obj/item/item_to_grill, mob/user)
vis_contents += item_to_grill
griddled_objects += item_to_grill
item_to_grill.flags_1 |= IS_ONTOP_1
item_to_grill.vis_flags |= VIS_INHERIT_PLANE
SEND_SIGNAL(item_to_grill, COMSIG_ITEM_GRILL_PLACED, user)
@@ -155,7 +157,6 @@
/obj/machinery/griddle/proc/ItemRemovedFromGrill(obj/item/ungrill)
SIGNAL_HANDLER
ungrill.flags_1 &= ~IS_ONTOP_1
ungrill.vis_flags &= ~VIS_INHERIT_PLANE
griddled_objects -= ungrill
vis_contents -= ungrill
@@ -46,6 +46,9 @@
remove_shared_particles(particle_type)
return ..()
/obj/machinery/oven/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return ..() || istype(contained, /obj/item/plate/oven_tray)
/// Used to determine if the oven appears active and cooking, or offline.
/obj/machinery/oven/proc/appears_active()
return !open && length(used_tray?.contents) && !(machine_stat & (BROKEN|NOPOWER))
@@ -127,7 +130,6 @@
if(!open)
oven_tray.vis_flags |= VIS_HIDE
vis_contents += oven_tray
oven_tray.flags_1 |= IS_ONTOP_1
oven_tray.vis_flags |= VIS_INHERIT_PLANE
oven_tray.pixel_y = OVEN_TRAY_Y_OFFSET
oven_tray.pixel_x = OVEN_TRAY_X_OFFSET
@@ -144,7 +146,6 @@
/obj/machinery/oven/proc/tray_removed_from_oven(obj/item/oven_tray)
SIGNAL_HANDLER
oven_tray.flags_1 &= ~IS_ONTOP_1
oven_tray.vis_flags &= ~VIS_INHERIT_PLANE
vis_contents -= oven_tray
used_tray = null
@@ -232,7 +232,7 @@
/// We pick up a number of slimes equal to the rating of the matter bin
var/slimes_picked = 0
for(var/mob/living/basic/slime/slime in range(1,src))
if(!CanReach(slime)) //don't take slimes behind glass panes or somesuch; also makes it ignore slimes inside the processor
if(!slime.IsReachableBy(src)) //don't take slimes behind glass panes or somesuch; also makes it ignore slimes inside the processor
continue
if(slime.stat)
var/datum/food_processor_process/recipe = PROCESSOR_SELECT_RECIPE(slime)
@@ -217,7 +217,7 @@
/datum/component/stove/proc/add_container(obj/item/new_container, mob/user)
var/obj/real_parent = parent
real_parent.vis_contents += new_container
new_container.flags_1 |= IS_ONTOP_1
ADD_TRAIT(new_container, TRAIT_SKIP_BASIC_REACH_CHECK, ref(src))
new_container.vis_flags |= VIS_INHERIT_PLANE
container = new_container
@@ -230,7 +230,7 @@
/datum/component/stove/proc/remove_container()
var/obj/real_parent = parent
container.flags_1 &= ~IS_ONTOP_1
REMOVE_TRAIT(container, TRAIT_SKIP_BASIC_REACH_CHECK, ref(src))
container.vis_flags &= ~VIS_INHERIT_PLANE
real_parent.vis_contents -= container
+3 -2
View File
@@ -53,10 +53,12 @@
target.attackby(object_to_eat, user)
return TRUE //No normal attack
/obj/item/plate/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return TRUE
///This proc adds the food to viscontents and makes sure it can deregister if this changes.
/obj/item/plate/proc/AddToPlate(obj/item/item_to_plate)
vis_contents += item_to_plate
item_to_plate.flags_1 |= IS_ONTOP_1
item_to_plate.vis_flags |= VIS_INHERIT_PLANE
RegisterSignal(item_to_plate, COMSIG_MOVABLE_MOVED, PROC_REF(ItemMoved))
RegisterSignal(item_to_plate, COMSIG_QDELETING, PROC_REF(ItemMoved))
@@ -72,7 +74,6 @@
///This proc cleans up any signals on the item when it is removed from a plate, and ensures it has the correct state again.
/obj/item/plate/proc/ItemRemovedFromPlate(obj/item/removed_item)
removed_item.flags_1 &= ~IS_ONTOP_1
removed_item.vis_flags &= ~VIS_INHERIT_PLANE
vis_contents -= removed_item
UnregisterSignal(removed_item, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
@@ -527,7 +527,7 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999))
/obj/item/analyzer/hilbertsanalyzer/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(!istype(interacting_with, /obj/item/hilbertshotel))
return ..()
if(!user.CanReach(interacting_with))
if(!interacting_with.IsReachableBy(user))
to_chat(user, span_warning("It's to far away to scan!"))
return ITEM_INTERACT_BLOCKING
var/obj/item/hilbertshotel/sphere = interacting_with
+1 -1
View File
@@ -61,7 +61,7 @@
attacked_atom = singular_turf
break
if(user.CanReach(attacked_atom))
if(attacked_atom.IsReachableBy(user))
return ITEM_INTERACT_BLOCKING
var/atom/bullet = fire_projectile(/obj/projectile/grapple_hook, attacked_atom, 'sound/items/weapons/zipline_fire.ogg')
+1 -1
View File
@@ -109,7 +109,7 @@
/datum/action/cooldown/mob_cooldown/exenterate/proc/slash_em(atom/caster)
for(var/mob/living/victim in range(ability_range, caster))
if(faction_check(victim.faction, immune_factions) && owner.CanReach(victim))
if(faction_check(victim.faction, immune_factions) && victim.IsReachableBy(owner))
continue
to_chat(caster, span_warning("You slice [victim]!"))
to_chat(victim, span_warning("You are cut by [caster]'s blades!"))
@@ -366,7 +366,7 @@
update_bot_mode(new_mode = BOT_IDLE)
return
if(CanReach(patient))
if(patient.IsReachableBy(src))
melee_attack(patient)
/datum/id_trim/medibot
+3 -3
View File
@@ -597,7 +597,7 @@
. = ..()
var/list/reachable_turfs = list()
for(var/turf/adjacent_turf in RANGE_TURFS(1, owner.loc))
if(adjacent_turf == owner.loc || !owner.CanReach(adjacent_turf) || !isopenturf(adjacent_turf))
if(adjacent_turf == owner.loc || !adjacent_turf.IsReachableBy(owner) || !isopenturf(adjacent_turf))
continue
reachable_turfs += adjacent_turf
@@ -625,10 +625,10 @@
/datum/action/cooldown/exquisite_bunch/Trigger(mob/clicker, trigger_flags, atom/target)
if(activating)
return
var/bunch_turf = get_step(owner.loc, owner.dir)
var/atom/bunch_turf = get_step(owner.loc, owner.dir)
if(!bunch_turf)
return
if(!owner.CanReach(bunch_turf) || !isopenturf(bunch_turf))
if(!bunch_turf.IsReachableBy(owner) || !isopenturf(bunch_turf))
owner.balloon_alert(owner, "can't do that here!")
return
activating = TRUE
@@ -112,7 +112,7 @@
/datum/ai_behavior/goliath_dig/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/turf/target_turf = controller.blackboard[target_key]
var/mob/living/basic/basic_mob = controller.pawn
if(!basic_mob.CanReach(target_turf))
if(!target_turf.IsReachableBy(basic_mob))
return AI_BEHAVIOR_DELAY
basic_mob.melee_attack(target_turf)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
@@ -37,7 +37,7 @@
var/times_to_attack = 4
/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/Activate(atom/target)
if(owner.CanReach(target))
if(target.IsReachableBy(owner))
attack_combo(target)
StartCooldown()
return TRUE
@@ -74,14 +74,14 @@
owner.throw_at(target = final_turf, range = 7, speed = 1, spin = FALSE, callback = CALLBACK(src, PROC_REF(attack_combo), target))
/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/proc/attack_combo(atom/target)
if(!owner.CanReach(target))
if(!target.IsReachableBy(owner))
return FALSE
for(var/i in 0 to (times_to_attack - 1))
addtimer(CALLBACK(src, PROC_REF(attack_target), target), i * attack_interval)
/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/proc/attack_target(atom/target)
if(!owner.CanReach(target) || owner.stat == DEAD)
if(!target.IsReachableBy(owner) || owner.stat == DEAD)
return
var/mob/living/basic/basic_owner = owner
basic_owner.melee_attack(target, ignore_cooldown = TRUE)
@@ -292,7 +292,7 @@
possible_headwear += item
if(!length(possible_headwear))
for(var/obj/item/item in orange(1))
if(ispath(item.dog_fashion, /datum/dog_fashion/head) && CanReach(item))
if(ispath(item.dog_fashion, /datum/dog_fashion/head) && item.IsReachableBy(src))
possible_headwear += item
if(!length(possible_headwear))
return
@@ -85,7 +85,7 @@
for(var/atom/movable/movable in orange(victim, 1))
if(movable == victim)
continue
if(!victim.CanReach(movable) || movable.invisibility > victim.see_invisible)
if(!movable.IsReachableBy(victim) || movable.invisibility > victim.see_invisible)
continue
candidates += movable
if(!length(candidates))
+2 -2
View File
@@ -1055,7 +1055,7 @@
if(active_storage)
var/storage_is_important_recurisve = (active_storage.parent in important_recursive_contents?[RECURSIVE_CONTENTS_ACTIVE_STORAGE])
var/can_reach_active_storage = CanReach(active_storage.parent, view_only = TRUE)
var/can_reach_active_storage = active_storage.parent.IsReachableBy(src)
if(!storage_is_important_recurisve && !can_reach_active_storage)
active_storage.hide_contents(src)
@@ -1375,7 +1375,7 @@
to_chat(src, span_warning("Your holochasis does not allow you to do this!"))
return FALSE
if(!(action_bitflags & BYPASS_ADJACENCY) && ((action_bitflags & NOT_INSIDE_TARGET) || !recursive_loc_check(src, target)) && !CanReach(target))
if(!(action_bitflags & BYPASS_ADJACENCY) && ((action_bitflags & NOT_INSIDE_TARGET) || !recursive_loc_check(src, target)) && !target.IsReachableBy(src))
if(HAS_SILICON_ACCESS(src) && !ispAI(src))
if(!(action_bitflags & ALLOW_SILICON_REACH)) // silicons can ignore range checks (except pAIs)
if(!(action_bitflags & SILENT_ADJACENCY))
@@ -248,3 +248,6 @@
/// First element is the current martial art - any other elements are "saved" for if they unlearn the first one
/// Reference handling is done by the martial arts themselves
var/list/datum/martial_art/martial_arts
/// how many tiles can this mob reach with their hands? 1 tile is adjacent.
var/reach_length = 1
@@ -158,7 +158,7 @@
to_chat(src, span_warning("[offered.p_Theyre()] unable to take anything in [offered.p_their()] current state!"))
return
if(!CanReach(offered))
if(!offered.IsReachableBy(src))
to_chat(src, span_warning("You have to be beside [offered.p_them()]!"))
return
@@ -266,7 +266,7 @@
return ITEM_INTERACT_BLOCKING
/obj/item/tether_anchor/attack_hand_secondary(mob/user, list/modifiers)
if (!can_interact(user) || !user.CanReach(src) || !isturf(loc))
if (!can_interact(user) || !IsReachableBy(user) || !isturf(loc))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
if(HAS_TRAIT_FROM(user, TRAIT_TETHER_ATTACHED, REF(src)))
@@ -289,10 +289,10 @@
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))
if (!can_interact(user) || !IsReachableBy(user) || !isturf(loc))
return
if (!isliving(target) || !target.CanReach(src))
if (!isliving(target) || !IsReachableBy(target))
return
if(HAS_TRAIT_FROM(target, TRAIT_TETHER_ATTACHED, REF(src)))
+1 -1
View File
@@ -114,7 +114,7 @@
if(client) //Clientless mobs can just go ahead and point
if(ismovable(pointing_at))
var/atom/movable/pointed_movable = pointing_at
if(pointed_movable.flags_1 & IS_ONTOP_1)
if(HAS_TRAIT(pointed_movable, TRAIT_SKIP_BASIC_REACH_CHECK) || pointing_at.loc.IsContainedAtomAccessible(pointing_at, src))
pointing_at = pointed_movable.loc
if(!(pointing_at in view(client.view, src)))
@@ -203,7 +203,7 @@
return
new_name = apply_text_macros(new_name)
var/obj/item/hitting_implement = (locate(/obj/item/reagent_containers/cup/glass/bottle) in user.held_items) || user.get_item_for_held_index(hand)
if(!user.CanReach(attacked, hitting_implement))
if(!attacked.IsReachableBy(user, hitting_implement.reach))
user.balloon_alert(user, "out of range!")
return
var/obj/item/reagent_containers/cup/glass/bottle/bottle = hitting_implement
@@ -212,7 +212,7 @@
return CLICK_ACTION_SUCCESS
/obj/machinery/computer/emergency_shuttle/proc/attempt_hijack_stage(mob/living/user)
if(!user.CanReach(src))
if(!IsReachableBy(user))
return
if(HAS_TRAIT(user, TRAIT_HANDS_BLOCKED))
to_chat(user, span_warning("You need your hands free before you can manipulate [src]."))
+1 -1
View File
@@ -204,7 +204,7 @@
if(greyscale_colors)
vended_item.set_greyscale(colors=greyscale_colors)
if(user.CanReach(src) && user.put_in_hands(vended_item))
if(IsReachableBy(user) && user.put_in_hands(vended_item))
to_chat(user, span_notice("You take [item_record.name] out of the slot."))
vended_item.do_pickup_animation(user, src)
else