mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-08-22 03:27:05 +01:00
Refactors AI / camera eyes and slows holopad holograms to walk speed (#25078)
* Refactor/deduplicate camera eye code Camera Eyes previously had duplicated logic across several files. This change uncooks the spaghetti. Additionally, half-baked support for TG's multicam feature has been removed, as it was not functional or in use. * lets ff now * Camera Eye refactor fixes and finishing touches This change completes a refactor of AI eyes, which were previously used by xenobio consoles, syndicate and abductor camera consoles, shuttle docking computers, holograms, and, of course, the AI. Duplicated logic has been extracted to an abstract base mob, /mob/camera/eye, from which new types for each of the above now derive. Functionality is largely the same, with only a few minor cosmetic differences (i.e. camera eyes are now appropriately named given their type and user), as well as a quality-of-life enhancement for holograms, slowing their movement speed to base run speed to prevent users from accidentally zooming out of calls. * Camera eye refactor: Fix AI acceleration toggle The acceleration toggle was broken in the camera eye refactor, as previously the boolean was stored on the AI rather than its eye. This change fixes that. * Camera eye refactor: Fix syndicate cam visibility With the camera eye refactor, the syndicate advanced camera consoles lost the ability to view maintenance tunnels and other areas without active cameras, seeing static in their place instead (as all other cameras do). This change reinstates the original behavior. * Camera eye refactor: Convert spaces to tabs * Camera eye refactor: Fix CRLF * Apply suggestions from code review General minor code quality improvements suggested by GDNgit Co-authored-by: GDN <96800819+GDNgit@users.noreply.github.com> * Apply suggestions from code review Rename parameter names to avoid src accesses, remove an ambiguous and unused mob_define and holopad range variable from a previous WIP, change the for loop in /mob/camera/eye/relaymove to a for-to loop, and change the chat message warning, sent when an AI Eye is created on an AI that already has one, to a stack trace * Adds toggle to AI commands for fast holograms * Refactor ripped Hologram Eye relaymove Previously, the relaymove proc for hologram eyes was redundant and nearly impossible to read. It has been separated out into a few different named procs, and has had its use of `spawn` removed. * Remove unnecessary src access * Fix bug involving shuttle placement outlines The camera eye refactor that this commit is a part of introduced a bug that prevented shuttle placement outlines from showing up on first use of the shuttle console. This change fixes that bug. * Unrevert some changes from #26306 lost in merge * Remove erroneous free xray vision on advanced cams * Autodoc camera acceleration vars * Remove redundant null var initialization per code review Co-authored-by: Drsmail <60036448+Drsmail@users.noreply.github.com> Signed-off-by: asciodev <81930475+asciodev@users.noreply.github.com> * Changed variables to camel_case, autodocs, cleanup Changed a number of camera eye-related variables to camel_case style, added appropriate autodoc comments, as per code review. Also removed an unused cameranet function, modified the call signature of a cameranet function to be more semantic, and changed a qdel-on-initialize in camera eyes to return INITIALIZE_HINT_QDEL instead. Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com> * Remove stray qdel(src) per code review Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com> Signed-off-by: asciodev <81930475+asciodev@users.noreply.github.com> --------- Signed-off-by: asciodev <81930475+asciodev@users.noreply.github.com> Co-authored-by: GDN <96800819+GDNgit@users.noreply.github.com> Co-authored-by: Drsmail <60036448+Drsmail@users.noreply.github.com> Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
This commit is contained in:
co-authored by
Luc
GDN
Drsmail
parent
dac57002ff
commit
93ed0f096d
@@ -0,0 +1,60 @@
|
||||
# AI Freelook
|
||||
|
||||
## Credits
|
||||
|
||||
Initial code credit for this goes to Uristqwerty.
|
||||
Debugging, functionality, all comments and porting by Giacom.
|
||||
|
||||
## What is this?
|
||||
|
||||
This is a replacement for the current camera movement system, of the AI. Before
|
||||
this, the AI had to move between cameras and could only see what the cameras
|
||||
could see. Not only this but the cameras could see through walls, which created
|
||||
problems. With this, the AI controls an "AI Eye" mob, which moves just like a
|
||||
ghost; such as moving through walls and being invisible to players. The AI's eye
|
||||
is set to this mob and then we use a system (explained below) to determine what
|
||||
the cameras around the AI Eye can and cannot see. If the camera cannot see a
|
||||
turf, it will black it out, otherwise it won't and the AI will be able to see
|
||||
it. This creates several features, such as.. no more see-through-wall cameras,
|
||||
easier to control camera movement, easier tracking, the AI only being able to
|
||||
track mobs which are visible to a camera, only trackable mobs appearing on the
|
||||
mob list and many more.
|
||||
|
||||
## How it Works
|
||||
|
||||
It works by first creating a camera network datum. Inside of this camera network
|
||||
are "chunks" (which will be explained later) and "cameras". The cameras list is
|
||||
kept up to date by obj/machinery/camera/New() and Destroy().
|
||||
|
||||
Next the camera network has chunks. These chunks are a 16x16 tile block of turfs
|
||||
and cameras contained inside the chunk. These turfs are then sorted out based on
|
||||
what the cameras can and cannot see. If none of the cameras can see the turf,
|
||||
inside the 16x16 block, it is listed as an "obscured" turf. Meaning the AI won't
|
||||
be able to see it.
|
||||
|
||||
## How it Updates
|
||||
|
||||
The camera network uses a streaming method in order to effeciently update
|
||||
chunks. Since the server will have doors opening, doors closing, turf being
|
||||
destroyed and other lag inducing stuff, we want to update it under certain
|
||||
conditions and not every tick.
|
||||
|
||||
The chunks are not created straight away, only when an AI eye moves into it's
|
||||
area is when it gets created. One a chunk is created, when a non glass door
|
||||
opens/closes or an opacity turf is destroyed, we check to see if an AI Eye is
|
||||
looking in the area. We do this with the "seenby" list, which updates everytime
|
||||
an AI is near a chunk. If there is an AI eye inside the area, we update the
|
||||
chunk that the changed atom is inside and all surrounding chunks, since a
|
||||
camera's vision could leak onto another chunk. If there is no AI Eye, we instead
|
||||
flag the chunk to update whenever it is loaded by an AI Eye. This is basically
|
||||
how the chunks update and keep it in sync. We then add some lag reducing
|
||||
measures, such as an UPDATE_BUFFER which stops a chunk from updating too many
|
||||
times in a certain time-frame, only updating if the changed atom was blocking
|
||||
sight; for example, we don't update glass airlocks or floors.
|
||||
|
||||
## Where is Everything?
|
||||
|
||||
- `cameranet.dm`: Everything about the cameranet datum.
|
||||
- `chunk.dm`: Everything about the chunk datum.
|
||||
- `eye.dm`: Everything about the AI and the AIEye.
|
||||
- `updating.dm`: Everything about triggers that will update chunks.
|
||||
@@ -0,0 +1,160 @@
|
||||
// CAMERA NET
|
||||
//
|
||||
// The datum containing all the chunks.
|
||||
GLOBAL_DATUM_INIT(cameranet, /datum/cameranet, new())
|
||||
|
||||
/datum/cameranet
|
||||
var/name = "Camera Net"
|
||||
|
||||
/// The cameras on the map, no matter if they work or not. Updated in obj/machinery/camera.dm by New() and Destroy().
|
||||
var/list/cameras = list()
|
||||
/// The chunks of the map, mapping the areas that the cameras can see.
|
||||
var/list/chunks = list()
|
||||
var/ready = FALSE
|
||||
|
||||
/// Returns the chunk at (x, y, z) if it exists, otherwise returns null.
|
||||
/datum/cameranet/proc/chunk_generated(x, y, z)
|
||||
x &= ~(CAMERA_CHUNK_SIZE - 1)
|
||||
y &= ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/key = "[x],[y],[z]"
|
||||
return (chunks[key])
|
||||
|
||||
/// Returns the chunk at (x, y, z) if it exists, otherwise returns a new chunk at that location.
|
||||
/datum/cameranet/proc/get_camera_chunk(x, y, z)
|
||||
x &= ~(CAMERA_CHUNK_SIZE - 1)
|
||||
y &= ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/key = "[x],[y],[z]"
|
||||
if(!chunks[key])
|
||||
chunks[key] = new /datum/camerachunk(null, x, y, z)
|
||||
|
||||
return chunks[key]
|
||||
|
||||
/// Updates what the camera network can see.
|
||||
/datum/cameranet/proc/visibility(list/moved_eyes, client/C)
|
||||
if(!islist(moved_eyes))
|
||||
moved_eyes = moved_eyes ? list(moved_eyes) : list()
|
||||
|
||||
var/list/chunks_pre_seen = list()
|
||||
var/list/chunks_post_seen = list()
|
||||
|
||||
for(var/V in moved_eyes)
|
||||
var/mob/camera/eye/eye = V
|
||||
if(C)
|
||||
chunks_pre_seen |= eye.visible_camera_chunks
|
||||
// 0xf = 15
|
||||
var/static_range = eye.static_visibility_range
|
||||
var/x1 = max(0, eye.x - static_range) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/y1 = max(0, eye.y - static_range) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/x2 = min(world.maxx, eye.x + static_range) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/y2 = min(world.maxy, eye.y + static_range) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
|
||||
var/list/visible_chunks = list()
|
||||
|
||||
for(var/x = x1; x <= x2; x += CAMERA_CHUNK_SIZE)
|
||||
for(var/y = y1; y <= y2; y += CAMERA_CHUNK_SIZE)
|
||||
visible_chunks |= get_camera_chunk(x, y, eye.z)
|
||||
|
||||
var/list/remove = eye.visible_camera_chunks - visible_chunks
|
||||
var/list/add = visible_chunks - eye.visible_camera_chunks
|
||||
|
||||
for(var/chunk in remove)
|
||||
var/datum/camerachunk/c = chunk
|
||||
c.remove(eye, FALSE)
|
||||
|
||||
for(var/chunk in add)
|
||||
var/datum/camerachunk/c = chunk
|
||||
c.add(eye, FALSE)
|
||||
|
||||
if(C)
|
||||
chunks_post_seen |= eye.visible_camera_chunks
|
||||
|
||||
if(C)
|
||||
var/list/remove = chunks_pre_seen - chunks_post_seen
|
||||
var/list/add = chunks_post_seen - chunks_pre_seen
|
||||
|
||||
for(var/chunk in remove)
|
||||
var/datum/camerachunk/c = chunk
|
||||
C.images -= c.obscured
|
||||
|
||||
for(var/chunk in add)
|
||||
var/datum/camerachunk/c = chunk
|
||||
C.images += c.obscured
|
||||
|
||||
|
||||
/// Updates the chunks that the turf is located in. Use this when obstacles are destroyed or when doors open.
|
||||
/datum/cameranet/proc/update_visibility(atom/A, opacity_check = 1)
|
||||
if(!SSticker || (opacity_check && !A.opacity))
|
||||
return
|
||||
major_chunk_change(update = A)
|
||||
|
||||
/// Removes a camera from a chunk.
|
||||
/datum/cameranet/proc/remove_camera(obj/machinery/camera/c)
|
||||
major_chunk_change(remove = c)
|
||||
|
||||
/// Add a camera to a chunk.
|
||||
/datum/cameranet/proc/add_camera(obj/machinery/camera/c)
|
||||
major_chunk_change(add = c)
|
||||
|
||||
/// Refreshes the chunk location of a portable camera. Used by Cyborgs.
|
||||
/datum/cameranet/proc/update_portable_camera(obj/machinery/camera/c, turf/old_loc)
|
||||
major_chunk_change(add = c, old_loc = old_loc)
|
||||
|
||||
/// Private method for updating the chunk an atom is in, and all surrounding chunks.
|
||||
/// `add` will be added as a camera to the chunk of its current location and all surrounding chunks.
|
||||
/// `remove` will be removed as a camera from the chunk of its current location and all surrounding chunks.
|
||||
/// `update` will not be added or removed as a camera, but its surrounding chunks will be updated.
|
||||
/// These parameters are mutually exclusive.
|
||||
/datum/cameranet/proc/major_chunk_change(atom/add = null, atom/remove = null, atom/update = null, turf/old_loc = null)
|
||||
// 0xf = 15
|
||||
if(!add && !remove && !update)
|
||||
return
|
||||
if(add && remove)
|
||||
CRASH("Adding and removing a camera to the cameranet simultaneously is not implemented")
|
||||
|
||||
var/atom/c = remove
|
||||
if(!c)
|
||||
c = add
|
||||
if(!c)
|
||||
c = update
|
||||
|
||||
var/turf/T = get_turf(c)
|
||||
if(!T)
|
||||
return
|
||||
|
||||
// Check if the turf to add falls in the same chunk as the old_loc. If so, do nothing
|
||||
if(old_loc)
|
||||
if(T.x & ~(CAMERA_CHUNK_SIZE - 1) == old_loc.x & ~(CAMERA_CHUNK_SIZE - 1) && T.y & ~(CAMERA_CHUNK_SIZE - 1) == old_loc.y & ~(CAMERA_CHUNK_SIZE - 1))
|
||||
return
|
||||
|
||||
// Use camera view distance here to actually know how far a camera can max watch
|
||||
var/x1 = max(0, T.x - CAMERA_VIEW_DISTANCE) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/y1 = max(0, T.y - CAMERA_VIEW_DISTANCE) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/x2 = min(world.maxx, T.x + CAMERA_VIEW_DISTANCE) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
var/y2 = min(world.maxy, T.y + CAMERA_VIEW_DISTANCE) & ~(CAMERA_CHUNK_SIZE - 1)
|
||||
|
||||
for(var/x = x1; x <= x2; x += CAMERA_CHUNK_SIZE)
|
||||
for(var/y = y1; y <= y2; y += CAMERA_CHUNK_SIZE)
|
||||
if(chunk_generated(x, y, T.z))
|
||||
var/datum/camerachunk/chunk = get_camera_chunk(x, y, T.z)
|
||||
if(remove)
|
||||
// Remove the camera.
|
||||
chunk.remove_camera(c)
|
||||
else if(add)
|
||||
// You can't have the same camera in the list twice.
|
||||
chunk.add_camera(c)
|
||||
chunk.has_changed()
|
||||
|
||||
/// Returns 1 if a mob is on a viewable turf, otherwise returns 0.
|
||||
/datum/cameranet/proc/check_camera_vis(mob/living/target as mob)
|
||||
// 0xf = 15
|
||||
var/turf/position = get_turf(target)
|
||||
return check_turf_vis(position)
|
||||
|
||||
/datum/cameranet/proc/check_turf_vis(turf/position)
|
||||
var/datum/camerachunk/chunk = get_camera_chunk(position.x, position.y, position.z)
|
||||
if(chunk)
|
||||
if(chunk.changed)
|
||||
chunk.has_changed(1) // Update now, no matter if it's visible or not.
|
||||
if(chunk.visible_turfs[position])
|
||||
return 1
|
||||
return 0
|
||||
@@ -0,0 +1,171 @@
|
||||
/// A 16x16 grid of the map with a list of turfs that can be seen, are visible and are dimmed.
|
||||
/// Allows camera eyes to stream these chunks and know what they can and cannot see.
|
||||
/datum/camerachunk
|
||||
var/list/obscured_turfs = list()
|
||||
var/list/visible_turfs = list()
|
||||
var/list/obscured = list()
|
||||
var/list/active_cameras = list()
|
||||
var/list/inactive_cameras = list()
|
||||
var/list/turfs = list()
|
||||
var/list/seenby = list()
|
||||
var/changed = FALSE
|
||||
var/updating = FALSE
|
||||
var/x = 0
|
||||
var/y = 0
|
||||
var/z = 0
|
||||
|
||||
/// Adds a camera to the chunk if it has not already been added.
|
||||
/datum/camerachunk/proc/add_camera(obj/machinery/camera/cam)
|
||||
if(active_cameras[cam] || inactive_cameras[cam])
|
||||
return
|
||||
if(cam.non_chunking_camera)
|
||||
return
|
||||
// Register all even though it is active/inactive. Won't get called incorrectly
|
||||
RegisterSignal(cam, COMSIG_CAMERA_OFF, PROC_REF(deactivate_camera), TRUE)
|
||||
RegisterSignal(cam, COMSIG_CAMERA_ON, PROC_REF(activate_camera), TRUE)
|
||||
RegisterSignal(cam, COMSIG_PARENT_QDELETING, PROC_REF(remove_camera), TRUE)
|
||||
RegisterSignal(cam, COMSIG_CAMERA_MOVED, PROC_REF(camera_moved), TRUE)
|
||||
if(cam.can_use())
|
||||
active_cameras[cam] = cam
|
||||
else
|
||||
inactive_cameras[cam] = cam
|
||||
|
||||
/// Handles each movement by a camera that has been added to the chunk.
|
||||
/datum/camerachunk/proc/camera_moved(obj/machinery/camera/cam, atom/old_loc)
|
||||
var/turf/T = get_turf(cam)
|
||||
|
||||
// Falls outside of the chunk view distance
|
||||
if(T.x + CAMERA_VIEW_DISTANCE < x || T.x - CAMERA_VIEW_DISTANCE >= x + CAMERA_CHUNK_SIZE || T.y + CAMERA_VIEW_DISTANCE < y || T.y - CAMERA_VIEW_DISTANCE >= y + CAMERA_CHUNK_SIZE || T.z != z)
|
||||
remove_camera(cam)
|
||||
|
||||
/// Removes a camera from the chunk.
|
||||
/datum/camerachunk/proc/remove_camera(obj/machinery/camera/cam)
|
||||
UnregisterSignal(cam, list(COMSIG_CAMERA_OFF, COMSIG_CAMERA_ON, COMSIG_PARENT_QDELETING, COMSIG_CAMERA_MOVED))
|
||||
active_cameras -= cam
|
||||
inactive_cameras -= cam
|
||||
SScamera.queue(src)
|
||||
|
||||
/datum/camerachunk/proc/activate_camera(obj/machinery/camera/cam)
|
||||
inactive_cameras -= cam
|
||||
active_cameras += cam
|
||||
SScamera.queue(src)
|
||||
|
||||
/datum/camerachunk/proc/deactivate_camera(obj/machinery/camera/cam)
|
||||
inactive_cameras += cam
|
||||
active_cameras -= cam
|
||||
SScamera.queue(src)
|
||||
|
||||
/// Add a camera eye to the chunk, then update if changed.
|
||||
/datum/camerachunk/proc/add(mob/camera/eye/eye, add_images = TRUE)
|
||||
if(add_images)
|
||||
var/client/client = eye.get_viewer_client()
|
||||
if(client)
|
||||
client.images += obscured
|
||||
eye.visible_camera_chunks += src
|
||||
seenby += eye
|
||||
RegisterSignal(eye, COMSIG_PARENT_QDELETING, PROC_REF(eye_destroyed))
|
||||
if(changed)
|
||||
SScamera.queue(src)
|
||||
|
||||
/datum/camerachunk/proc/eye_destroyed(mob/camera/eye/eye)
|
||||
remove(eye, FALSE)
|
||||
|
||||
/// Remove a camera eye from the chunk, then update if changed.
|
||||
/datum/camerachunk/proc/remove(mob/camera/eye/eye, remove_images = TRUE)
|
||||
if(remove_images)
|
||||
var/client/client = eye.get_viewer_client()
|
||||
if(client)
|
||||
client.images -= obscured
|
||||
eye.visible_camera_chunks -= src
|
||||
seenby -= eye
|
||||
UnregisterSignal(eye, COMSIG_PARENT_QDELETING)
|
||||
|
||||
/// Called when a chunk has changed. I.E: A wall was deleted.
|
||||
/datum/camerachunk/proc/visibility_changed(turf/loc)
|
||||
if(!visible_turfs[loc])
|
||||
return
|
||||
has_changed()
|
||||
|
||||
/// Updates the chunk, makes sure that it doesn't update too much. If the chunk isn't being watched it will
|
||||
/// instead be flagged to update the next time a camera eye moves near it.
|
||||
/datum/camerachunk/proc/has_changed(update_now = 0)
|
||||
if(update_now)
|
||||
update()
|
||||
SScamera.remove_from_queue(src)
|
||||
|
||||
if(length(seenby))
|
||||
SScamera.queue(src)
|
||||
else
|
||||
changed = TRUE
|
||||
|
||||
/// Gathers the visible turfs from cameras and puts them into the appropiate lists.
|
||||
/datum/camerachunk/proc/update()
|
||||
var/list/new_visible_turfs = list()
|
||||
|
||||
for(var/obj/machinery/camera/c as anything in active_cameras)
|
||||
var/turf/point = locate(src.x + (CAMERA_CHUNK_SIZE / 2), src.y + (CAMERA_CHUNK_SIZE / 2), src.z)
|
||||
var/turf/T = get_turf(c)
|
||||
if(get_dist(point, T) > CAMERA_VIEW_DISTANCE + (CAMERA_CHUNK_SIZE / 2))
|
||||
// Still needed for Ais who get created on Z level 1 on the spot of the new player
|
||||
continue
|
||||
|
||||
for(var/turf/t in c.can_see())
|
||||
if(turfs[t])
|
||||
new_visible_turfs[t] = t
|
||||
|
||||
var/list/vis_added = new_visible_turfs - visible_turfs
|
||||
var/list/vis_removed = visible_turfs - new_visible_turfs
|
||||
|
||||
visible_turfs = new_visible_turfs
|
||||
obscured_turfs = turfs - new_visible_turfs
|
||||
var/list/images_to_remove = list()
|
||||
var/list/images_to_add = list()
|
||||
for(var/turf/t as anything in vis_added)
|
||||
if(t.obscured)
|
||||
obscured -= t.obscured
|
||||
images_to_remove += t.obscured
|
||||
|
||||
for(var/turf/t as anything in vis_removed)
|
||||
if(!t.obscured)
|
||||
t.obscured = image('icons/effects/cameravis.dmi', t, null, BYOND_LIGHTING_LAYER + 0.1)
|
||||
t.obscured.plane = BYOND_LIGHTING_PLANE + 1
|
||||
obscured += t.obscured
|
||||
images_to_add += t.obscured
|
||||
|
||||
for(var/mob/camera/eye/eye as anything in seenby)
|
||||
var/client/client = eye.get_viewer_client()
|
||||
if(client)
|
||||
client.images -= images_to_remove
|
||||
client.images += images_to_add
|
||||
changed = FALSE
|
||||
|
||||
/// Create a new camera chunk, since the chunks are made as they are needed.
|
||||
/datum/camerachunk/New(loc, x, y, z)
|
||||
|
||||
// 0xf = 15
|
||||
x &= ~(CAMERA_CHUNK_SIZE - 1)
|
||||
y &= ~(CAMERA_CHUNK_SIZE - 1)
|
||||
|
||||
src.x = x
|
||||
src.y = y
|
||||
src.z = z
|
||||
|
||||
var/half_chunk = CAMERA_CHUNK_SIZE / 2
|
||||
for(var/obj/machinery/camera/c in urange(half_chunk + CAMERA_VIEW_DISTANCE, locate(x + half_chunk, y + half_chunk, z)))
|
||||
add_camera(c)
|
||||
|
||||
for(var/turf/t in block(max(x, 1), max(y, 1), max(z, 1), min(x + CAMERA_CHUNK_SIZE - 1, world.maxx), min(y + CAMERA_CHUNK_SIZE - 1, world.maxy), z))
|
||||
turfs[t] = t
|
||||
|
||||
for(var/obj/machinery/camera/c as anything in active_cameras)
|
||||
for(var/turf/t in c.can_see())
|
||||
if(turfs[t])
|
||||
visible_turfs[t] = t
|
||||
|
||||
obscured_turfs = turfs - visible_turfs
|
||||
|
||||
for(var/turf/t as anything in obscured_turfs)
|
||||
if(!t.obscured)
|
||||
t.obscured = image('icons/effects/cameravis.dmi', t, "black", BYOND_LIGHTING_LAYER + 0.1)
|
||||
t.obscured.plane = BYOND_LIGHTING_PLANE + 1
|
||||
obscured += t.obscured
|
||||
@@ -0,0 +1,191 @@
|
||||
/// Camera eyes are remote-control mobs that can move and see throughout the global cameranet.
|
||||
/// They're used in AI eyes, holograms, advanced camera consoles, abductor consoles, shuttle consoles,
|
||||
/// and xenobiology consoles. When created, the user with which they are initialized will be granted control,
|
||||
/// and their movements will be relayed to the camera eye instead. When destroyed, the user's control of the
|
||||
/// camera eye will be released; if they were previously remote controlling another object (such as another
|
||||
/// camera eye) then they will be put back in control of that object; otherwise they will return to their body.
|
||||
/mob/camera/eye
|
||||
name = "Inactive Camera Eye"
|
||||
icon = 'icons/obj/abductor.dmi'
|
||||
icon_state = "camera_target"
|
||||
alpha = 127
|
||||
invisibility = SEE_INVISIBLE_OBSERVER
|
||||
|
||||
/// The list of camera chunks currently visible to the camera eye.
|
||||
var/list/visible_camera_chunks = list()
|
||||
/// The user controlling the eye.
|
||||
var/mob/living/user
|
||||
/// The thing that the user was previously remote controlling before this eye.
|
||||
var/user_previous_remote_control
|
||||
/// The object that created the eye.
|
||||
var/origin
|
||||
/// If true, speech near the camera eye will be relayed to its controller.
|
||||
var/relay_speech = FALSE
|
||||
/// Sets the camera eye visibility range; does not expand viewport, only affects cameranet obscuring
|
||||
var/static_visibility_range = 16
|
||||
/// Toggles whether this eye is detectable by AI Detectors.
|
||||
var/ai_detector_visible = TRUE
|
||||
/// Toggles whether the eye's icon should be visible to its user.
|
||||
var/visible_icon = FALSE
|
||||
/// The list of cameranets that this camera eye can see and access.
|
||||
var/list/networks = list("SS13")
|
||||
/// The in-memory image of the camera eye's icon.
|
||||
var/image/user_image
|
||||
|
||||
// Camera acceleration settings
|
||||
// Initially, the camera moves one turf per move. If there is no movement for
|
||||
// cooldown_rate in deciseconds, the camera will reset to this movement rate.
|
||||
// Every move otherwise increases sprint by acceleration_rate, until sprint
|
||||
// exceeds sprint_threshold, and the movement rate increases by one per move.
|
||||
// The movement rate is 1 + round(sprint / sprint_threshold).
|
||||
|
||||
/// The maximum sprint value - this caps acceleration
|
||||
var/max_sprint = 50
|
||||
/// The minimum sprint needed to increase base velocity
|
||||
var/sprint_threshold = 20
|
||||
/// The amount that sprint is increased per move
|
||||
var/acceleration_rate = 0.5
|
||||
/// Keeps track of acceleration - movement rate is 1 + round(sprint / sprint_threshold)
|
||||
var/sprint = 10
|
||||
/// The absolute time that sprint will reset to its initial value
|
||||
var/cooldown = 0
|
||||
/// The time after which sprint should be reset to its initial state, if no movements are made
|
||||
var/cooldown_rate = 5
|
||||
/// Toggles camera acceleration on or off.
|
||||
var/acceleration = 1
|
||||
|
||||
/mob/camera/eye/Initialize(mapload, owner_name, camera_origin, mob/living/user)
|
||||
. = ..()
|
||||
name = "Camera Eye ([owner_name])"
|
||||
origin = camera_origin
|
||||
give_control(user)
|
||||
update_visibility()
|
||||
refresh_visible_icon()
|
||||
if(!validate_active_cameranet())
|
||||
return INITIALIZE_HINT_QDEL
|
||||
|
||||
/// Validates that there is an active cameranet. If strict is 0, does nothing.
|
||||
/// Returns 1 if there is an active cameranet. Warns the user and returns 0 if there is not.
|
||||
/mob/camera/eye/proc/validate_active_cameranet(strict = 0)
|
||||
var/camera = first_active_camera()
|
||||
if(strict && !camera)
|
||||
to_chat(user, "<span class='warning'>ERROR: No linked and active camera network found.</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/// Returns the turf of the first active camera in the global cameranet.
|
||||
/mob/camera/eye/proc/first_active_camera()
|
||||
for(var/obj/machinery/camera/C in GLOB.cameranet.cameras)
|
||||
if(!C.can_use())
|
||||
continue
|
||||
if(length(C.network & networks))
|
||||
return get_turf(C)
|
||||
|
||||
/// Updates what the global cameranet can see with respect to this eye and its user's client.
|
||||
/mob/camera/eye/proc/update_visibility()
|
||||
GLOB.cameranet.visibility(src, user.client)
|
||||
|
||||
/// Refreshes user_image in the user's client.images.
|
||||
/mob/camera/eye/proc/refresh_visible_icon()
|
||||
if(visible_icon && user.client)
|
||||
user.client.images -= user_image
|
||||
user_image = image(icon,loc,icon_state,FLY_LAYER)
|
||||
user.client.images += user_image
|
||||
|
||||
/// Sets the camera eye's location to T, updates global cameranet visibility, and refreshes user_images.
|
||||
/mob/camera/eye/set_loc(T)
|
||||
if(user)
|
||||
T = get_turf(T)
|
||||
..(T)
|
||||
update_visibility()
|
||||
refresh_visible_icon()
|
||||
|
||||
/// Disables independent movement by camera eyes; camera eyes must be controlled by relaymove.
|
||||
/mob/camera/eye/Move()
|
||||
return FALSE
|
||||
|
||||
/// If `usr` is an AI, set the camera eye's location to the location of the atom clicked.
|
||||
/atom/proc/move_camera_by_click()
|
||||
if(is_ai(usr))
|
||||
var/mob/living/silicon/ai/AI = usr
|
||||
if(AI.eyeobj && (AI.client.eye == AI.eyeobj) && (AI.eyeobj.z == z))
|
||||
AI.camera_follow = null
|
||||
if(isturf(loc) || isturf(src))
|
||||
AI.eyeobj.set_loc(src)
|
||||
|
||||
/// Returns the user's client, if it exists; otherwise returns null.
|
||||
/mob/camera/eye/proc/get_viewer_client()
|
||||
return user?.client
|
||||
|
||||
/// Removes obscured chunk images and user_images from the user's client.images.
|
||||
/mob/camera/eye/proc/remove_images()
|
||||
var/client/C = get_viewer_client()
|
||||
if(!C)
|
||||
return
|
||||
for(var/datum/camerachunk/chunk as anything in visible_camera_chunks)
|
||||
C.images -= chunk.obscured
|
||||
if(visible_icon)
|
||||
C.images -= user_image
|
||||
|
||||
/// Calls `remove_images`, changes the user's remote control from this camera eye to `user_previous_remote_control`.
|
||||
/mob/camera/eye/proc/release_control()
|
||||
if(!istype(user))
|
||||
return
|
||||
if(user.client)
|
||||
user.reset_perspective(user.client.mob)
|
||||
remove_images()
|
||||
user.remote_control = null
|
||||
if(user_previous_remote_control)
|
||||
user.reset_perspective(user_previous_remote_control)
|
||||
user.remote_control = user_previous_remote_control
|
||||
user_previous_remote_control = null
|
||||
user = null
|
||||
|
||||
/// Forces this eye's current user to release control, renames this eye, and grants `new_user` control of this eye.
|
||||
/mob/camera/eye/proc/give_control(mob/new_user)
|
||||
if(!istype(new_user))
|
||||
return
|
||||
release_control()
|
||||
user = new_user
|
||||
rename_camera(user.name)
|
||||
if(istype(user.remote_control))
|
||||
user_previous_remote_control = user.remote_control
|
||||
user.remote_control = src
|
||||
user.reset_perspective(src)
|
||||
|
||||
/// Renames the camera eye (only visible in observer Orbit menu)
|
||||
/mob/camera/eye/proc/rename_camera(new_name)
|
||||
name = "Camera Eye ([new_name])"
|
||||
|
||||
/// Remove this eye from all chunks containing it.
|
||||
/mob/camera/eye/proc/release_chunks()
|
||||
for(var/datum/camerachunk/chunk as anything in visible_camera_chunks)
|
||||
chunk.remove(src)
|
||||
|
||||
/mob/camera/eye/Destroy()
|
||||
release_control()
|
||||
release_chunks()
|
||||
return ..()
|
||||
|
||||
/// Called when the user controlling this eye attempts to move; uses camera acceleration settings.
|
||||
/mob/camera/eye/relaymove(mob/user,direct)
|
||||
var/initial = initial(sprint)
|
||||
|
||||
if(cooldown && cooldown < world.timeofday)
|
||||
sprint = initial
|
||||
|
||||
for(var/i in 0 to sprint step sprint_threshold)
|
||||
var/turf/next_step= get_turf(get_step(src, direct))
|
||||
if(next_step)
|
||||
set_loc(next_step)
|
||||
|
||||
cooldown = world.timeofday + cooldown_rate
|
||||
if(acceleration)
|
||||
sprint = min(sprint + acceleration_rate, max_sprint)
|
||||
else
|
||||
sprint = initial
|
||||
|
||||
/// If `relay_speech` is truthy, allows the camera eye's user to hear speech spoken at the eye's location.
|
||||
/mob/camera/eye/hear_say(list/message_pieces, verb = "says", italics = 0, mob/speaker = null, sound/speech_sound, sound_vol, sound_frequency, use_voice = TRUE)
|
||||
if(relay_speech)
|
||||
user.hear_say(message_pieces, verb, italics, speaker, speech_sound, sound_vol, sound_frequency)
|
||||
@@ -0,0 +1,14 @@
|
||||
/mob/camera/eye/abductor
|
||||
name = "Abductor Camera Eye"
|
||||
ai_detector_visible = FALSE
|
||||
|
||||
/mob/camera/eye/abductor/Initialize(mapload, owner_name, camera_origin, user)
|
||||
..()
|
||||
set_loc(first_active_camera())
|
||||
|
||||
/mob/camera/eye/abductor/rename_camera(new_name)
|
||||
name = "Abductor Camera Eye ([new_name])"
|
||||
|
||||
/// Requires the cameranet to be validated.
|
||||
/mob/camera/eye/abductor/validate_active_cameranet()
|
||||
..(TRUE)
|
||||
@@ -0,0 +1,34 @@
|
||||
/mob/camera/eye/ai
|
||||
name = "Inactive AI Eye"
|
||||
icon = 'icons/mob/ai.dmi'
|
||||
icon_state = "eye"
|
||||
var/mob/living/silicon/ai/ai = null
|
||||
|
||||
/mob/camera/eye/ai/Initialize(mapload, owner_name, camera_origin, mob/living/user)
|
||||
. = ..()
|
||||
ai = user
|
||||
if(is_ai_eye(ai.eyeobj))
|
||||
stack_trace("Tried to create an AI Eye for [user], but there is one already assigned")
|
||||
return INITIALIZE_HINT_QDEL
|
||||
name = "[owner_name] (AI Eye)"
|
||||
|
||||
/// Ensures that the user's perspective is on this eye (instead of, for example, a mech or hologram eye) and
|
||||
/// updates parallax
|
||||
/mob/camera/eye/ai/set_loc(T)
|
||||
..()
|
||||
user.reset_perspective(src)
|
||||
update_parallax_contents()
|
||||
|
||||
/// Cancels the camera's follow target if tracking has stopped, and lights up nearby lights if the AI has
|
||||
/// "Toggle Camera Lights" enabled
|
||||
/mob/camera/eye/ai/relaymove(mob/user, direct)
|
||||
..()
|
||||
if(!ai.tracking)
|
||||
ai.camera_follow = null
|
||||
if(ai.camera_light_on)
|
||||
ai.light_cameras()
|
||||
|
||||
/// Relays speech near the AI eye to the AI if the AI has surveillance, either from a malf module or a station trait
|
||||
/mob/camera/eye/ai/hear_say(list/message_pieces, verb = "says", italics = 0, mob/speaker = null, sound/speech_sound, sound_vol, sound_frequency, use_voice = TRUE)
|
||||
if(relay_speech)
|
||||
ai.relay_speech(speaker, message_pieces, verb)
|
||||
@@ -0,0 +1,53 @@
|
||||
/mob/camera/eye/hologram
|
||||
name = "Inactive Hologram Eye"
|
||||
ai_detector_visible = FALSE
|
||||
acceleration = FALSE
|
||||
relay_speech = TRUE
|
||||
var/obj/machinery/hologram/holopad/holopad
|
||||
|
||||
/mob/camera/eye/hologram/Initialize(mapload, owner_name, camera_origin, mob/living/user)
|
||||
..()
|
||||
holopad = camera_origin
|
||||
set_loc(holopad)
|
||||
|
||||
/mob/camera/eye/hologram/rename_camera(new_name)
|
||||
name = "Hologram ([new_name])"
|
||||
|
||||
/// Hologram movement copies the delays and diagonal delays of regular mob movement
|
||||
/// for crew, and for AI unless fast holograms are enabled
|
||||
/mob/camera/eye/hologram/relaymove(mob/user, direct)
|
||||
var/mob/living/silicon/ai/ai = user
|
||||
if(istype(ai) && ai.fast_holograms)
|
||||
return ..()
|
||||
var/turf/new_location = get_turf(get_step(src, direct))
|
||||
if(new_location.z != z || get_dist(new_location, src) > 1)
|
||||
return
|
||||
var/base_delay = GLOB.configuration.movement.base_run_speed
|
||||
var/diag_delay = base_delay * SQRT_2
|
||||
var/delay = (direct & (direct - 1)) ? diag_delay : base_delay
|
||||
user.client.move_delay = debounced_move(normalized_delay_speed(delay))
|
||||
user.last_movement = world.time
|
||||
set_loc(new_location)
|
||||
|
||||
/mob/camera/eye/hologram/proc/debounced_move(delay_speed)
|
||||
var/old_move_delay = user.client.move_delay
|
||||
if(old_move_delay + world.tick_lag > world.time)
|
||||
return old_move_delay + delay_speed
|
||||
else
|
||||
return world.time + delay_speed
|
||||
|
||||
/mob/camera/eye/hologram/proc/normalized_delay_speed(delay_speed)
|
||||
return TICKS2DS(-round(-(DS2TICKS(delay_speed))))
|
||||
|
||||
/// Requires the cameranet to be validated.
|
||||
/mob/camera/eye/hologram/validate_active_cameranet()
|
||||
..(TRUE)
|
||||
|
||||
/// Moves the associated hologram, and the previously controlled object (probably an AI eye), to the new location.
|
||||
/mob/camera/eye/hologram/set_loc(T)
|
||||
. = ..()
|
||||
var/turf/new_location = get_turf(T)
|
||||
holopad.move_hologram(user, new_location)
|
||||
var/mob/camera/eye/previous_eye = user_previous_remote_control
|
||||
if(istype(previous_eye))
|
||||
previous_eye.set_loc(new_location)
|
||||
@@ -0,0 +1,30 @@
|
||||
/mob/camera/eye/shuttle_docker
|
||||
name = "Shuttle Docker Camera Eye"
|
||||
ai_detector_visible = FALSE
|
||||
simulated = FALSE
|
||||
var/list/placement_images = list()
|
||||
var/list/placed_images = list()
|
||||
|
||||
/mob/camera/eye/shuttle_docker/Initialize(mapload, owner_name, camera_origin, mob/living/user)
|
||||
..()
|
||||
set_loc(first_active_camera())
|
||||
|
||||
/mob/camera/eye/shuttle_docker/rename_camera(new_name)
|
||||
name = "Shuttle Docker Camera Eye ([new_name])"
|
||||
|
||||
/// Prevents the shuttle docker eye from updating global cameranet chunks. Shuttle dockers don't use station cameras.
|
||||
/mob/camera/eye/shuttle_docker/update_visibility()
|
||||
return FALSE
|
||||
|
||||
/// Prevents moving the camera eye into the station, and updates the current location's landing spot validity.
|
||||
/mob/camera/eye/shuttle_docker/set_loc(T)
|
||||
if(isspaceturf(get_turf(T)) || istype(get_area(T), /area/space) || istype(get_area(T), /area/shuttle))
|
||||
..()
|
||||
var/obj/machinery/computer/camera_advanced/shuttle_docker/console = origin
|
||||
console.check_landing_spot()
|
||||
|
||||
/mob/camera/eye/shuttle_docker/update_remote_sight(mob/living/user)
|
||||
user.sight = SEE_TURFS
|
||||
|
||||
..()
|
||||
return TRUE
|
||||
@@ -0,0 +1,10 @@
|
||||
/mob/camera/eye/syndicate
|
||||
name = "Syndicate Researcher Camera Eye"
|
||||
ai_detector_visible = FALSE
|
||||
|
||||
/mob/camera/eye/syndicate/Initialize(mapload, owner_name, camera_origin, user)
|
||||
..()
|
||||
set_loc(first_active_camera())
|
||||
|
||||
/mob/camera/eye/syndicate/rename_camera(new_name)
|
||||
name = "Syndicate Researcher Camera Eye ([new_name])"
|
||||
@@ -0,0 +1,22 @@
|
||||
/mob/camera/eye/xenobio
|
||||
name = "Xenobiology Console Camera Eye"
|
||||
visible_icon = TRUE
|
||||
ai_detector_visible = FALSE
|
||||
var/allowed_area
|
||||
|
||||
/mob/camera/eye/xenobio/Initialize(mapload, owner_name, camera_origin, mob/living/user)
|
||||
. = ..()
|
||||
var/area/A = get_area(camera_origin)
|
||||
allowed_area = A.name
|
||||
|
||||
/mob/camera/eye/xenobio/rename_camera(new_name)
|
||||
name = "Xenobiology Console ([new_name])"
|
||||
|
||||
/// Prevents the camera eye from going outside of the xenobiology area
|
||||
/mob/camera/eye/xenobio/set_loc(T)
|
||||
var/area/new_area = get_area(T)
|
||||
if(!new_area)
|
||||
return
|
||||
if(new_area.name != allowed_area && !new_area.xenobiology_compatible)
|
||||
return
|
||||
return ..()
|
||||
Reference in New Issue
Block a user