Files
9885763e3d hand refactor (#5677)
Co-authored-by: silicons <no@you.cat>
2024-11-20 09:25:57 +01:00

805 lines
23 KiB
Plaintext

//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* MOB PERRSPECTIVE SYSTEM
*
* allows managed control of client viewport/eye changes
*
* ! - - - ATTENTION - - -
* ! These datums will modify mob variables.
* ! This can result in AI mobs having inconsistent sight/whatevers
* ! from what they'd usually have after a player enters, and
* ! leaves them.
* ! Mob reset_perspective() should handle most of these cases, but be wary.
*
* ! - - - ATTENTION - - -
* ! View sizes should only be for clients.
* ! Do not read off this for stuff like AI view/target tracking.
*
* ? - - - ATTENTION - - -
* ? We track MAXIMUM view sizes.
* ? Client can resize up/down as needed.
* ? If you want to force a client to a specific size,
* ? regardless of their widescreen setting,
* ? use client.set_temporary_view.
* ? Augmented view will also ignore client prefs on widescreen, *kinda.*
*
* as of right now, perspectives will **trample** the following on every set:
* client.eye
* client.lazy_eye (unimplemented)
* client.virtual_eye (unimplemented)
* client.perspective
* client.view
* mob.see_in_dark
* mob.see_invisible
* mob.sight
*
* these will be added/removed using synchronized access,
* and therefore existing values will be left alone,
* as long as existing values are not also in the perspective:
* client.screen
* client.images
*
* this is intentional - most of mobcode uses their own screen/image synchronization code.
* perspectives will never be able to replace that without ruining a lot of lazy-load
* behavior. instead, perspective is focused on allowing using it to manage generic
* synchronization of screen/images, rather than forcing the rest of the codebase to use it.
*
* however, perspectives are designed to force synchronization of the vars it does trample,
* because there's no better way to do it (because those vars are, semantically, only relevant to our perspective),
* while screen/images can be used for embedded maps/hud/etc.
*
* ## Use Case
*
* Perspectives should for the management of an atom's semantic world-view.
* This is what they can / can not see around them.
* This should not be used for things like inventory HUDs and action buttons.
* Those are "internal" viewing / a mob's internal state.
*
* Basically, these are for what you can see, if you looked through someone's eyes.
*/
/datum/perspective
/// eye - where visual calcs go from
var/atom/movable/eye
/// client perspective var
var/perspective = EYE_PERSPECTIVE
/// images
var/list/image/images
/// screen objects
var/list/atom/movable/screens
/// sight var
var/sight = SIGHT_FLAGS_DEFAULT
/// active clients - this is not the same as mobs because a client can be looking somewhere that isn't their mob
var/list/client/clients
/// mobs that are using this - required for clean gcs
var/list/mob/mobs
/// when a client logs out of a mob, and it's using us, the mob should reset to its self_perspective
var/reset_on_logout = TRUE
/// see in dark
var/see_in_dark = 2
/// see_invisible
var/see_invisible = SEE_INVISIBLE_LIVING
//* HUDs *//
/// active atom HUD providers associated to a list of ids or paths of atom huds that's providing it.
var/list/datum/atom_hud_provider/atom_hud_providers
//? planes
/// planes
var/datum/plane_holder/mob_perspective/planes
/// trait-like list for forcing planes on
var/list/planes_visible
//? view size
/// default view; if null, world.view
var/default_view_size
/// view size increase x
var/augment_view_width = 0
/// view size increase y
var/augment_view_height = 0
/// suppression sources
var/list/view_suppression
/// cached view x
var/cached_view_width
/// cached view y
var/cached_view_height
/// view cache needs recompute
var/view_dirty = TRUE
//? vision - lighting / nightvision
// todo: most of these don't need to be on baes /datum/perspective, i think.
// alternatively, just add a way to control whether we bother using this system.
// "darksight_system"?
var/janky_should_we_use_darksight_system = FALSE
/// darksight overlay that we maintain
var/atom/movable/screen/darksight_fov/darksight_fov_overlay
/// darksight overlay that we maintain
var/atom/movable/screen/darksight_occlusion/darksight_occlusion_overlay
/// lighting plane alpha
var/hard_darkvision
/// soft darksight range
var/darkvision_range
/// soft darksight alpha
var/darkvision_alpha
/// soft darksight matrix
var/list/darkvision_matrix
/// do we use smart darkvision?
var/darkvision_smart
/// is soft darkvision in global (infinite range) mode?
var/darkvision_unlimited
/// soft darkvision fov cone
var/darkvision_fov
// todo: legacy: stuff like mesons that require hard darkvision use this to cap see_in_dark
var/darkvision_legacy_throttle
// todo: legacy: used for xray fulbright
var/legacy_forced_hard_darkvision
// todo: sigh, shadekin things
var/legacy_throttle_overridden
/datum/perspective/Destroy()
clear_clients()
clear_mobs()
clear_atom_hud_providers()
QDEL_NULL(darksight_fov_overlay)
QDEL_NULL(darksight_occlusion_overlay)
images = null
screens = null
clients = null
set_eye(null)
return ..()
/// ONLY CALL FROM CLIENT.SET_PERSPECTIVE
/// I DO NOT TRUST PEOPLE TO NOT SCREW THIS UP
/datum/perspective/proc/add_client(client/C)
SHOULD_NOT_OVERRIDE(TRUE)
SHOULD_CALL_PARENT(TRUE)
if(C in clients)
return
if(C.using_perspective)
CRASH("client already had perspective")
LAZYADD(clients, C)
C.using_perspective = src
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_CLIENT_REGISTER, C)
apply(C)
/// ONLY CALL FROM CLIENT.SET_PERSPECTIVE
/// I DO NOT TRUST PEOPLE TO NOT SCREW THIS UP
/datum/perspective/proc/remove_client(client/C, switching = FALSE)
SHOULD_NOT_OVERRIDE(TRUE)
SHOULD_CALL_PARENT(TRUE)
if(!(C in clients))
return
LAZYREMOVE(clients, C)
remove(C)
// if we're not doing this as part of a switch have them immediately switch to the mob
// oh and make sure they unregister
if(C.using_perspective != src)
stack_trace("client had wrong perspective")
C.using_perspective = null
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_CLIENT_UNREGISTER, C, switching)
if(!switching)
C.reset_perspective()
/**
* gets all clients viewing us
*/
/datum/perspective/proc/get_clients()
return isnull(clients)? list() : clients.Copy()
/**
* kicks all clients off us
*/
/datum/perspective/proc/clear_clients()
for(var/client/C as anything in clients)
remove_client(C)
/**
* kicks all obs off of us
*/
/datum/perspective/proc/clear_mobs()
for(var/mob/M as anything in mobs)
remove_mob(M)
/**
* registers as a mob's current perspective
* sets mob vars as necessary
*/
/datum/perspective/proc/add_mob(mob/M)
SHOULD_CALL_PARENT(TRUE)
if(M.using_perspective)
CRASH("mob already had perspective")
if(reset_on_logout && !M.client) // nah
return
LAZYADD(mobs, M)
M.using_perspective = src
M.sight = sight
M.see_in_dark = clamp(see_in_dark, 0, 255)
M.see_invisible = see_invisible
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_MOB_ADD, M)
/**
* unregisters as a mob's current perspective
* resets mob vars to initial() values
*/
/datum/perspective/proc/remove_mob(mob/M, switching = FALSE)
SHOULD_CALL_PARENT(TRUE)
M.sight = initial(M.sight)
M.see_in_dark = initial(M.see_in_dark)
M.see_invisible = initial(M.see_invisible)
LAZYREMOVE(mobs, M)
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_MOB_REMOVE, M, switching)
if(M.using_perspective == src)
M.using_perspective = null
if(!switching)
M.reset_perspective()
else
CRASH("mob had wrong perspective")
/**
* applys screen objs, etc, stuff that shouldn't be updated regularly
*/
/datum/perspective/proc/apply(client/C)
SHOULD_CALL_PARENT(TRUE)
assert_planes()
assert_vision_overlays()
if(!isnull(screens))
C.screen |= screens
if(!isnull(planes))
C.screen |= planes.screens()
if(!isnull(images))
C.images |= images
update(C)
/**
* fully reloads
*
* set owner to TRUE to apply prefs like AO.
*/
/datum/perspective/proc/reload(client/C, owner)
assert_planes()
if(owner)
planes.sync_owner(C)
apply(C)
/datum/perspective/proc/remove(client/C)
if(!isnull(screens))
C.screen -= screens
if(!isnull(planes))
C.screen -= planes.screens()
if(!isnull(images))
C.images -= images
/**
* updates eye, perspective var, virtual eye, lazy eye, sight, see in dark, see invis
*/
/datum/perspective/proc/update(client/C)
SEND_SIGNAL(src, COMSIG_PERSPECTIVE_CLIENT_UPDATE, C)
var/changed = C.eye
var/new_eye = get_eye(C)
if(!new_eye)
stack_trace("got null on get_eye, this shouldn't be possible")
C.eye = C.mob
else
C.eye = new_eye
if(changed != C.eye)
C.parallax_holder?.reset(force = TRUE)
C.perspective = get_eye_mode(C)
update_view_size(C)
/**
* update all viewers
*/
/datum/perspective/proc/update_all()
for(var/client/C as anything in clients)
update(C)
/**
* works with lists too
*/
/datum/perspective/proc/add_image(image/I)
var/change = length(images)
if(!change)
images = list()
images |= I
if(images.len != change)
for(var/client/C as anything in clients)
// |=, not +=, because we don't check dupes.
C.images |= I
/**
* works with lists too
*/
/datum/perspective/proc/remove_image(image/I)
var/change = length(images)
if(!change)
return
images -= I
if(images.len != change)
for(var/client/C as anything in clients)
C.images -= I
/**
* works with lists too
*/
/datum/perspective/proc/add_screen(atom/movable/AM)
var/change = length(screens)
if(!change)
screens = list()
screens |= AM
if(screens.len != change)
for(var/client/C as anything in clients)
// |=, not +=, because we don't check dupes.
C.screen |= AM
/**
* works with lists too
*/
/datum/perspective/proc/remove_screen(atom/movable/AM)
var/change = length(screens)
if(!change)
return
screens -= AM
if(change != screens.len)
for(var/client/C as anything in clients)
C.screen -= AM
/datum/perspective/proc/SetSight(flags)
var/change = sight ^ flags
sight = flags
if(change)
for(var/client/C as anything in clients)
C.mob.sight = sight
/datum/perspective/proc/AddSight(flags)
var/change = sight ^ flags
sight |= flags
if(change)
for(var/client/C as anything in clients)
C.mob.sight = sight
/datum/perspective/proc/RemoveSight(flags)
var/change = sight ^ flags
sight &= ~(flags)
if(change)
for(var/client/C as anything in clients)
C.mob.sight = sight
/datum/perspective/proc/SetSeeInvis(see_invisible)
var/change = src.see_invisible != see_invisible
src.see_invisible = see_invisible
if(change)
for(var/client/C as anything in clients)
C.mob.see_invisible = see_invisible
//? Abstraction - see_in_dark
/datum/perspective/proc/update_see_in_dark()
var/wanted = INFINITY // show everything
// if they have hard darkvision kick it back up
if(legacy_forced_hard_darkvision || hard_darkvision < 255)
wanted = INFINITY
// if they have legacy throttle..
if(!isnull(darkvision_legacy_throttle) && !legacy_throttle_overridden)
wanted = min(darkvision_legacy_throttle, wanted)
if(wanted != see_in_dark)
see_in_dark = wanted
for(var/mob/M as anything in mobs)
M.see_in_dark = clamp(see_in_dark, 0, 255)
//* Atom HUDs *//
/**
* accepts:
* * path
* * string id
* * instance
*
* currently will runtime if it's not a valid hud!
*/
/datum/perspective/proc/add_atom_hud(datum/atom_hud/hud, source)
ASSERT(istext(source))
hud = fetch_atom_hud(hud)
if(isnull(atom_hud_providers))
atom_hud_providers = list()
var/list/datum/atom_hud_provider/providers = hud.resolve_providers()
for(var/datum/atom_hud_provider/provider as anything in providers)
var/already_there = atom_hud_providers[provider]
if(already_there)
atom_hud_providers[provider] |= source
else
atom_hud_providers[provider] = list(source)
provider.add_perspective(src)
/**
* accepts:
* * path
* * string id
* * instance
*
* currently will runtime if it's not a valid hud!
*/
/datum/perspective/proc/remove_atom_hud(datum/atom_hud/hud, source)
ASSERT(istext(source))
if(!length(atom_hud_providers))
return
if(!hud)
// remove all of source
for(var/datum/atom_hud_provider/provider as anything in atom_hud_providers)
if(!(source in atom_hud_providers[provider]))
continue
atom_hud_providers[provider] -= source
if(!length(atom_hud_providers[provider]))
atom_hud_providers -= provider
provider.remove_perspective(src)
return
hud = fetch_atom_hud(hud)
var/list/datum/atom_hud_provider/providers = hud.resolve_providers()
for(var/datum/atom_hud_provider/provider as anything in providers)
if(!length(atom_hud_providers[provider]))
continue
atom_hud_providers[provider] -= source
if(!length(atom_hud_providers[provider]))
atom_hud_providers -= provider
provider.remove_perspective(src)
// todo: add_atom_hud_provider, remove_atom_hud_provider
/**
* clears all atom hud providers
*
* * seriously don't use this unless you know what you're doing
* * "why are huds broken" --> 9 / 10 times it's because someone used this when they shouldn't
* * remember that /datum/perspective does NOT state track HUD registration for you; you have to ensure things are added/removed as necessary
* * hint: if it's not on /silicon/robot, /simple_mob, or /dead/observer (or you don't know what those words are), you shouldn't be using this!
*/
/datum/perspective/proc/clear_atom_hud_providers()
for(var/datum/atom_hud_provider/provider as anything in atom_hud_providers)
provider.remove_perspective(src)
atom_hud_providers = null
//? Eye
/datum/perspective/proc/set_eye_mode(perspective)
var/change = src.perspective != perspective
if(!change)
return
src.perspective = perspective
for(var/client/C as anything in clients)
C.perspective = get_eye_mode(C)
/datum/perspective/proc/set_eye(atom/movable/AM)
var/change = src.eye != AM
if(!change)
return
if(!isnull(src.eye))
detach_from_eye(src.eye)
src.eye = AM
if(!isnull(src.eye))
attach_to_eye(src.eye)
for(var/client/C as anything in clients)
var/changed = C.eye
C.eye = get_eye(C)
if(changed != C.eye)
C.parallax_holder?.reset(force = TRUE)
C.perspective = get_eye_mode(C)
/datum/perspective/proc/attach_to_eye(atom/movable/AM)
RegisterSignal(AM, COMSIG_ATOM_DIR_CHANGE, PROC_REF(eye_dir_changed))
/datum/perspective/proc/detach_from_eye(atom/movable/AM)
UnregisterSignal(AM, COMSIG_ATOM_DIR_CHANGE)
/datum/perspective/proc/eye_dir_changed(atom/movable/source, old_dir, new_dir)
darksight_fov_overlay?.dir = new_dir
/datum/perspective/proc/get_eye(client/C)
return eye
/**
* get perspective var for a client
*/
/datum/perspective/proc/get_eye_mode(client/C)
// necessary for smooth transitions when calling update_perspective
// we default to eye perspectivie
// also this is honestly pointless but theoretically mob perspective should always be used while on mob
// this is stupid and i'm stupid
return eye == C.mob? MOB_PERSPECTIVE : perspective
//? lighting
/datum/perspective/proc/push_vision_stack(list/datum/vision/holders)
// reset to default
hard_darkvision = 255
darkvision_range = SOFT_DARKSIGHT_RANGE_DEFAULT
darkvision_alpha = SOFT_DARKSIGHT_ALPHA_DEFAULT
darkvision_matrix = construct_rgba_color_matrix()
darkvision_smart = TRUE
darkvision_unlimited = FALSE
darkvision_legacy_throttle = INFINITY
darkvision_fov = SOFT_DARKSIGHT_FOV_DEFAULT
legacy_throttle_overridden = FALSE
// push holders
for(var/datum/vision/holder as anything in holders)
holder.push(src)
// ensure darkvision matrix is full color format - we have a bug where not doing so can break shit after the first push (?!)
darkvision_matrix = color_matrix_expand(darkvision_matrix)
darkvision_unlimited = darkvision_range >= SOFT_DARKSIGHT_UNLIMITED_THRESHOLD
// update
update_vision()
/datum/perspective/proc/update_vision()
update_see_in_dark()
update_planes()
update_vision_overlays()
/datum/perspective/proc/assert_vision_overlays()
. = TRUE
if(isnull(darksight_fov_overlay))
darksight_fov_overlay = new
add_screen(darksight_fov_overlay)
if(isnull(darksight_occlusion_overlay))
darksight_occlusion_overlay = new
add_screen(darksight_occlusion_overlay)
if(!.)
return
update_vision_overlays()
/datum/perspective/proc/update_vision_overlays()
if(!isnull(darksight_fov_overlay))
// handle fov
var/state_to_use = "fade-omni-super"
var/color_to_use = null
switch(darkvision_fov)
if(SOFT_DARKSIGHT_FOV_270)
state_to_use = "fade-270-hard"
if(SOFT_DARKSIGHT_FOV_180)
state_to_use = "fade-180-hard"
if(SOFT_DARKSIGHT_FOV_90)
state_to_use = "fade-90-hard"
else
state_to_use = "fade-omni-super"
color_to_use = list(
0, 0, 0, -0.5,
0, 0, 0, -0.5,
0, 0, 0, -0.5,
0, 0, 0, 0,
0, 0, 0, 1,
)
darksight_fov_overlay.icon_state = state_to_use
darksight_fov_overlay.color = color_to_use
if(view_dirty)
recompute_view_size()
// todo: this should take shifting into account, for things like binoculars.
var/needed = max(cached_view_height, cached_view_width) / 15
var/matrix/transforming = matrix()
if(needed > 1)
transforming.Scale(needed, needed)
darksight_fov_overlay.transform = transforming
if(!isnull(darksight_occlusion_overlay))
// now, handle occlusion
var/matrix/cropping = matrix()
var/factor = ((darkvision_range / (WORLD_ICON_SIZE)) / ((15 / 2) + 1))
cropping.Scale(factor)
darksight_occlusion_overlay.transform = cropping
/datum/perspective/proc/legacy_force_set_hard_darkvision(amt)
. = legacy_forced_hard_darkvision == amt
legacy_forced_hard_darkvision = amt
if(.)
update_see_in_dark()
update_planes()
//? plane holder
/datum/perspective/proc/assert_planes()
if(!isnull(planes))
return
planes = new /datum/plane_holder/mob_perspective
update_planes()
/datum/perspective/proc/update_planes()
if(isnull(planes))
return
var/atom/movable/screen/plane_master/darkvision_plate = planes.by_plane_type(/atom/movable/screen/plane_master/darkvision)
if(!isnull(darkvision_plate))
darkvision_plate.color = darkvision_matrix?.Copy()
var/atom/movable/screen/plane_master/lighting/lighting_plane = planes?.by_plane_type(/atom/movable/screen/plane_master/lighting)
var/wanted_alpha = isnull(legacy_forced_hard_darkvision)? (isnull(hard_darkvision)? 255 : hard_darkvision) : legacy_forced_hard_darkvision
lighting_plane.alpha = wanted_alpha
var/lit_factor = darkvision_alpha / 255
darkvision_plate.alpha = round((lit_factor * (wanted_alpha / 255)) * 255, 1)
/**
* sets a plane visible if it wasn't already
* source should be a trait source
*
* @params
* * key - path to plane
* * source - trait source enum
*/
/datum/perspective/proc/set_plane_visible(key, source)
LAZYINITLIST(planes_visible)
var/was = planes_visible[key]
if(islist(was))
planes_visible[key] |= source
else
planes_visible[key] = list(source)
show_plane(key)
/**
* removes a plane's visibility if it was forced to be visible
* source should be a trait source
*
* @params
* * key - path to plane (optional); if not specified, all planes from that source is removed.
* * source - trait source enum.
*/
/datum/perspective/proc/unset_plane_visible(key, source)
if(isnull(key))
for(key in planes_visible)
if(source in planes_visible[key])
planes_visible[key] -= source
if(!length(planes_visible[key]))
planes_visible -= key
hide_plane(key)
return
var/was = planes_visible[key]
if(islist(was))
was -= source
if(!length(was))
planes_visible -= key
hide_plane(key)
/datum/perspective/proc/is_plane_visible(var/atom/movable/screen/plane_master/key, source)
if(!initial(key.default_invisible))
return TRUE
if(isnull(source))
return planes_visible[key]
return source in planes_visible[key]
/datum/perspective/proc/show_plane(key, force)
assert_planes()
var/atom/movable/screen/plane_master/plane = planes.by_plane_type(key)
if(isnull(plane))
return
plane.alpha = plane.default_alpha
/datum/perspective/proc/hide_plane(key, force)
assert_planes()
var/atom/movable/screen/plane_master/plane = planes.by_plane_type(key)
if(isnull(plane))
return
if(!plane.default_invisible && !force)
return
plane.alpha = 0
//? view size
/**
* Sets our default size.
*
* DO NOT use this in place of augmented.
* User widescreen preferences WILL override this!
*
* Ideally this shouldn't be used at all.
*/
/datum/perspective/proc/set_default_view(size)
if(size == default_view_size)
return
// assert it y'know, works
if(isnum(size))
default_view_size = size
else if(isnull(size))
default_view_size = size
else if(istext(size))
var/list/split = splittext(size, "x")
ASSERT(split.len == 2)
// if the above runtimes, let it so we know who fucked up
default_view_size = size
else
CRASH("What?")
view_dirty = TRUE
update_view_size()
/**
* Sets augmented size ontop of default size.
*
* Currently only supports positive numbers. Numbers apply equally
* to both directions of a dimension.
*/
/datum/perspective/proc/set_augmented_view(width, height)
if(!isnum(height) || !isnum(width))
CRASH("invalid")
// no negative augment values for now
augment_view_height = max(0, height)
augment_view_width = max(0, width)
view_dirty = TRUE
update_view_size()
/datum/perspective/proc/update_view_size(client/C)
if(view_dirty)
recompute_view_size()
update_vision_overlays()
if(C)
C.request_viewport_update()
else
for(var/client/_C as anything in clients)
update_view_size(_C)
/datum/perspective/proc/recompute_view_size()
view_dirty = FALSE
if(isnum(default_view_size))
cached_view_height = default_view_size + augment_view_width
cached_view_width = default_view_size + augment_view_height
return
else if(isnull(default_view_size))
cached_view_width = GLOB.max_client_view_x + augment_view_width
cached_view_height = GLOB.max_client_view_y + augment_view_height
return
var/list/parsed = splittext(default_view_size, "x")
if(length(parsed) != 2)
cached_view_width = GLOB.max_client_view_x + augment_view_width
cached_view_height = GLOB.max_client_view_y + augment_view_height
return
cached_view_width = parsed[1] + augment_view_width
cached_view_height = parsed[2] + augment_view_height
/datum/perspective/proc/suppress_view(source)
var/was = LAZYLEN(view_suppression)
LAZYDISTINCTADD(view_suppression, source)
if(!was && LAZYLEN(view_suppression))
view_dirty = TRUE
update_view_size()
/datum/perspective/proc/unsuppress_view(source)
var/was = LAZYLEN(view_suppression)
LAZYREMOVE(view_suppression, source)
if(was && !LAZYLEN(view_suppression))
view_dirty = TRUE
update_view_size()
/datum/perspective/proc/ensure_view_cached()
if(view_dirty)
recompute_view_size()
//! remotes
/datum/perspective/proc/considered_remote(mob/M)
return eye == M
/**
* used for self-perspectives - eye should always be the owner
*/
/datum/perspective/self
/datum/perspective/self/get_eye(client/C)
return get_top_atom(eye)
/datum/perspective/self/proc/get_top_atom(atom/movable/where)
if(isturf(where))
return where // already on turf
var/turf/T = get_turf(where)
if(!T)
return where // either the perspective owner's mob or someone fucked up royally
// we know we're on a turf now (assuming get_turf() didn't break but that'd be a byond thing) so
while(!isturf(where.loc))
where = where.loc
return where
/**
* temporary perspectives generated - automatically deletes when last mob is gone
*/
/datum/perspective/self/temporary
/datum/perspective/self/temporary/remove_mob(mob/M, switching)
. = ..()
if(!length(mobs))
qdel(src)
/**
* always remote - you usually want to override this
*/
/datum/perspective/remote
/datum/perspective/remote/considered_remote(mob/M)
return TRUE