From 546dc7fdfa03de0f81e12881086191db5f0f3add Mon Sep 17 00:00:00 2001
From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Date: Fri, 20 Feb 2026 17:58:39 -0800
Subject: [PATCH] Quarters camera chunk area, Implements better yielding to
subsystem on update. (#94530)
## About The Pull Request
[cameras should actually show all of their
view](https://github.com/tgstation/tgstation/commit/a8ef0f3bd7f16e590b9fae193f645c6cb6255f8f)
Melb fucked up luminosity a bit (cries)
[Implements camera range as the deciding factor for who can see a
chunk](https://github.com/tgstation/tgstation/commit/bf4f9776dfce1c15e756ae1933a96ff0cff2dbf0)
This allows us to safely modify chunk size (which I do, down to 8, which
increases the actual cost involved in doing a full z update, but
decreases the cost per chunk significantly)
I use the range to macro optimize chunk New, we can safely use a
bounding box instead of multiple urange calls and get about 1/3 the
performance hit, even with a lower range (and thus 4x as many chunks)
Standard caching and such applies
[Fixes cameras not looking down
correctly](https://github.com/tgstation/tgstation/commit/99b23f25203f34f04c0f92bb7d191699b91a439a)
[Implements yielding support for individual
chunks](https://github.com/tgstation/tgstation/commit/bdd2d01106465b8498307c16f83e9dd90706652a)
This should functionally remove lagspikes from the cameras subsystem,
which just leaves the problem of ais flying around.
Much better tick obedience:
https://github.com/user-attachments/assets/607d61a4-ad96-450c-acdf-10da967ab49b
I tested this pr with #94522 to confirm that halving chunk sizes would
have a benefit, see below:
Things to note:
Mean cost of finding turfs goes from 2ms to 1ms
Max cost of finding turfs goes down by somewhere between 30% and 50%.
I'd call this worthwhile, even if total cost of updating has gone up
slightly.
[Properly manages the camera refs in
chunks](https://github.com/tgstation/tgstation/pull/94530/commits/00c97183ca83a91d95b9afcde70814c36a8f9a5e)
[00c9718](https://github.com/tgstation/tgstation/pull/94530/commits/00c97183ca83a91d95b9afcde70814c36a8f9a5e)
Turns out our solution for moving cameras in chunk code was to just...
NOT CARE!! This means any moving camera is going to cause hard deletes.
This was an issue during unit tests because of a stripping unit test. I
suspect it only showed up here because I reduced the chunk size so we
actually enter/leave a new set of chunks.
I fixed this by reworking things to use a "leaving/joining" pattern.
There's some other stuff here, mostly cleaning up old copypasta'd cruft
in the code adjacent to moving cameras. I also changed the cameras and
processing cameras alists back into lists, mostly for the sake of vv
ability this didn't actually break anything.
The harddel wasn't showing up because it was a number keyed alist, and
reference tracking isn't built to find those (yet, pr up)
## Why It's Good For The Game
I want AIs flying around to not cause massive lag spikes. That + the
camera subsystem are one of like 4 things that's causing reliable tidi
on live and it makes me upset.
## Changelog
:cl:
fix: Cameras should work a little better (won't fail in the dark, will
display multiz correctly
refactor: Changed how static turf generation thinks about cameras.
Shouldn't break anything but it MIGHT so keep an eye out for that. The
chunks we use to divvy up the world for them are also 1/4th the area
now, which should make flying around as ai less lagsikey
/:cl:
---------
Co-authored-by: Lucy
---
code/__DEFINES/cameranets.dm | 7 +-
code/__HELPERS/spatial_info.dm | 2 +-
code/controllers/subsystem/cameras.dm | 80 ++++++++---
code/datums/components/simple_bodycam.dm | 12 +-
code/game/machinery/camera/camera.dm | 13 +-
.../mob/living/silicon/ai/freelook/chunk.dm | 135 ++++++++++++++----
code/modules/mob/living/silicon/silicon.dm | 1 -
.../mob/living/silicon/silicon_movement.dm | 25 +---
.../file_system/programs/secureye.dm | 2 +-
code/modules/vehicles/mecha/_mecha.dm | 2 -
code/modules/vehicles/mecha/mecha_movement.dm | 27 +---
.../wiremod/components/atom/remotecam.dm | 4 +-
12 files changed, 202 insertions(+), 108 deletions(-)
diff --git a/code/__DEFINES/cameranets.dm b/code/__DEFINES/cameranets.dm
index 75cdb072ea9..9889dbd7535 100644
--- a/code/__DEFINES/cameranets.dm
+++ b/code/__DEFINES/cameranets.dm
@@ -1,7 +1,10 @@
+// Needs to be constant because we need to know how many turfs out to update chunks from our source
+#define MAX_CAMERA_RANGE 7
+
/// We only want chunk sizes that are to the power of 2. E.g: 2, 4, 8, 16, etc..
-#define CHUNK_SIZE 16
+#define CHUNK_SIZE 8
/// Takes a position, transforms it into a chunk bounded position. Indexes at 1 so it'll land on actual turfs always
-#define GET_CHUNK_COORD(v) (max((FLOOR(v, CHUNK_SIZE)), 1))
+#define GET_CHUNK_COORD(v) max((FLOOR((v), CHUNK_SIZE)), 1)
//List of different camera nets, cameras are given this in the map and camera consoles can only view them if
//they share this network with them.
diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm
index 1135c93b055..424cd26348b 100644
--- a/code/__HELPERS/spatial_info.dm
+++ b/code/__HELPERS/spatial_info.dm
@@ -406,7 +406,7 @@
/// get_hear that only gets turfs so we can use as_anything
/proc/get_hear_turfs(range, atom/source)
var/lum = source.luminosity
- source.luminosity = 6
+ source.luminosity = range + 2
. = list()
for(var/turf/turf in view(range, source))
. += turf
diff --git a/code/controllers/subsystem/cameras.dm b/code/controllers/subsystem/cameras.dm
index 5f5e9a2ec1b..62e8af1c973 100644
--- a/code/controllers/subsystem/cameras.dm
+++ b/code/controllers/subsystem/cameras.dm
@@ -38,8 +38,8 @@ SUBSYSTEM_DEF(cameras)
var/list/current_run = src.current_run
while(current_run.len)
var/datum/camerachunk/chunk = current_run[current_run.len]
- chunk.force_update(only_if_necessary = TRUE) // Forces an update if necessary
- current_run.len--
+ if(chunk.yield_update())
+ current_run.len--
if(MC_TICK_CHECK)
break
@@ -131,13 +131,55 @@ SUBSYSTEM_DEF(cameras)
major_chunk_change(new_cam, ADD_CAMERA)
/**
- * Used for Cyborg/mecha cameras. Since portable cameras can be in ANY chunk.
- * update_delay_buffer is passed all the way to queue_update() from their camera updates on movement
- * to change the time between static updates.
+ * Used to update cameras that are movin, since most everything in the game can.
+ * update_delay_buffer is passed to allow variable update delays. useful so one
+ * camera'd guy moving doesn't absolutely spam updates to watching ais (laggin the server)
*/
-/datum/controller/subsystem/cameras/proc/update_portable_camera(obj/machinery/camera/updating_camera, update_delay_buffer)
- if(updating_camera.can_use())
- major_chunk_change(updating_camera, ADD_CAMERA, update_delay_buffer)
+/datum/controller/subsystem/cameras/proc/camera_moved(obj/machinery/camera/updating_camera, turf/old_turf, turf/new_turf, update_delay_buffer)
+ if(old_turf == new_turf)
+ return
+
+ var/list/old_chunks = list()
+ var/range_difference = MAX_CAMERA_RANGE + 1
+ if(!isnull(old_turf))
+ var/x1 = max(1, old_turf.x - range_difference)
+ var/y1 = max(1, old_turf.y - range_difference)
+ var/x2 = min(world.maxx, old_turf.x + range_difference)
+ var/y2 = min(world.maxy, old_turf.y + range_difference)
+ for(var/x = x1; x <= x2; x += CHUNK_SIZE)
+ for(var/y = y1; y <= y2; y += CHUNK_SIZE)
+ var/datum/camerachunk/chunk = get_camera_chunk(x, y, old_turf.z)
+ if(isnull(chunk))
+ continue
+ old_chunks += chunk
+ // if we've moved, what we can see will have changed so queue er up
+ chunk.queue_update(updating_camera, update_delay_buffer)
+
+ var/list/new_chunks = list()
+ if(!isnull(new_turf) && updating_camera.can_use())
+ if(QDELETED(updating_camera))
+ stack_trace("Tried to add a qdeleting camera to the net")
+ else
+ var/x1 = max(1, new_turf.x - range_difference)
+ var/y1 = max(1, new_turf.y - range_difference)
+ var/x2 = min(world.maxx, new_turf.x + range_difference)
+ var/y2 = min(world.maxy, new_turf.y + range_difference)
+ for(var/x = x1; x <= x2; x += CHUNK_SIZE)
+ for(var/y = y1; y <= y2; y += CHUNK_SIZE)
+ var/datum/camerachunk/chunk = get_camera_chunk(x, y, new_turf.z)
+ if(isnull(chunk))
+ continue
+ new_chunks += chunk
+ // if we've moved, what we can see will have changed so queue er up
+ chunk.queue_update(updating_camera, update_delay_buffer)
+
+ // First, cut us from chunks who we can't see anymore
+ for(var/datum/camerachunk/lost as anything in old_chunks - new_chunks)
+ lost.cameras["[old_turf.z]"] -= updating_camera
+
+ // Then we add to turfs who we can newly see!
+ for(var/datum/camerachunk/found as anything in new_chunks - old_chunks)
+ found.cameras["[new_turf.z]"] |= updating_camera
/**
* Never access this proc directly!!!!
@@ -158,10 +200,11 @@ SUBSYSTEM_DEF(cameras)
if(isnull(chunk_turf))
return
- var/x1 = max(1, chunk_turf.x - (CHUNK_SIZE / 2))
- var/y1 = max(1, chunk_turf.y - (CHUNK_SIZE / 2))
- var/x2 = min(world.maxx, chunk_turf.x + (CHUNK_SIZE / 2))
- var/y2 = min(world.maxy, chunk_turf.y + (CHUNK_SIZE / 2))
+ var/range_difference = MAX_CAMERA_RANGE + 1
+ var/x1 = max(1, chunk_turf.x - range_difference)
+ var/y1 = max(1, chunk_turf.y - range_difference)
+ var/x2 = min(world.maxx, chunk_turf.x + range_difference)
+ var/y2 = min(world.maxy, chunk_turf.y + range_difference)
for(var/x = x1; x <= x2; x += CHUNK_SIZE)
for(var/y = y1; y <= y2; y += CHUNK_SIZE)
var/datum/camerachunk/chunk = get_camera_chunk(x, y, chunk_turf.z)
@@ -169,19 +212,20 @@ SUBSYSTEM_DEF(cameras)
continue
if(choice == REMOVE_CAMERA)
// Remove the camera.
- chunk.cameras[chunk_turf.z] -= center_or_camera
+ chunk.cameras["[chunk_turf.z]"] -= center_or_camera
if(choice == ADD_CAMERA)
// You can't have the same camera in the list twice.
- chunk.cameras[chunk_turf.z] |= center_or_camera
+ chunk.cameras["[chunk_turf.z]"] |= center_or_camera
chunk.queue_update(center_or_camera, update_delay_buffer)
/// A faster, turf only version of [/datum/controller/subsystem/cameras/proc/major_chunk_change]
/// For use in sensitive code, be careful with it
/datum/controller/subsystem/cameras/proc/bare_major_chunk_change(turf/changed)
- var/x1 = max(1, changed.x - (CHUNK_SIZE / 2))
- var/y1 = max(1, changed.y - (CHUNK_SIZE / 2))
- var/x2 = min(world.maxx, changed.x + (CHUNK_SIZE / 2))
- var/y2 = min(world.maxy, changed.y + (CHUNK_SIZE / 2))
+ var/range_difference = MAX_CAMERA_RANGE + 1
+ var/x1 = max(1, changed.x - range_difference)
+ var/y1 = max(1, changed.y - range_difference)
+ var/x2 = min(world.maxx, changed.x + range_difference)
+ var/y2 = min(world.maxy, changed.y + range_difference)
for(var/x = x1; x <= x2; x += CHUNK_SIZE)
for(var/y = y1; y <= y2; y += CHUNK_SIZE)
var/datum/camerachunk/chunk = get_camera_chunk(x, y, changed.z)
diff --git a/code/datums/components/simple_bodycam.dm b/code/datums/components/simple_bodycam.dm
index 1723a7a0a5a..f4c54ee22ee 100644
--- a/code/datums/components/simple_bodycam.dm
+++ b/code/datums/components/simple_bodycam.dm
@@ -29,7 +29,7 @@
RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, PROC_REF(rotate_cam))
RegisterSignals(bodycam, list(COMSIG_QDELETING, COMSIG_MOVABLE_MOVED), PROC_REF(camera_gone))
- do_update_cam()
+ do_update_cam(null)
/datum/component/simple_bodycam/Destroy()
if(QDELETED(bodycam))
@@ -51,12 +51,12 @@
/datum/component/simple_bodycam/proc/update_cam(datum/source, atom/old_loc, ...)
SIGNAL_HANDLER
+ do_update_cam(old_loc)
- if(get_turf(old_loc) != get_turf(parent))
- do_update_cam()
-
-/datum/component/simple_bodycam/proc/do_update_cam()
- SScameras.update_portable_camera(bodycam, camera_update_time)
+/datum/component/simple_bodycam/proc/do_update_cam(atom/old_loc)
+ if(!bodycam?.can_use())
+ return
+ SScameras.camera_moved(bodycam, get_turf(old_loc), get_turf(bodycam), camera_update_time)
/datum/component/simple_bodycam/proc/rotate_cam(datum/source, old_dir, new_dir)
SIGNAL_HANDLER
diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm
index 38bdd0e2f00..3b6e40c0d34 100644
--- a/code/game/machinery/camera/camera.dm
+++ b/code/game/machinery/camera/camera.dm
@@ -144,7 +144,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
/obj/machinery/camera/Destroy(force)
if(can_use())
toggle_cam(null, 0) //kick anyone viewing out and remove from the camera chunks
- SScameras.remove_camera_from_chunk(src)
SScameras.cameras -= src
cancelCameraAlarm()
if(isarea(myarea))
@@ -154,6 +153,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
QDEL_NULL(xray_module)
QDEL_NULL(emp_module)
QDEL_NULL(proximity_monitor)
+ // moveToNullspace will clear us from our current chunks
return ..()
/obj/machinery/camera/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
@@ -180,6 +180,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
emp_module = null
removeMotion()
+/obj/machinery/camera/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ SScameras.camera_moved(src, get_turf(old_loc), get_turf(loc))
+
/obj/machinery/camera/proc/create_prox_monitor()
if(!proximity_monitor)
proximity_monitor = new(src, 1)
@@ -271,6 +275,9 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
user.switchCamera(src)
/obj/machinery/camera/proc/setViewRange(num = 7)
+ if(num > MAX_CAMERA_RANGE)
+ CRASH("Attempted to set camera view range to something ([num]) greater then we support ([MAX_CAMERA_RANGE]).\
+ This would break chunk updating. If you really need to do this, update MAX_CAMERA_RANGE")
src.view_range = num
SScameras.update_visibility(src)
@@ -424,10 +431,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
// Haha datum var access KILL ME
for(var/turf/seen as anything in see)
if(check_lower)
- var/turf/below = GET_TURF_BELOW(seen)
+ var/turf/below = seen
while(below && istransparentturf(below))
- see += RANGE_TURFS(1, below)
below = GET_TURF_BELOW(below)
+ see += RANGE_TURFS(1, below)
if(check_higher)
var/turf/above = GET_TURF_ABOVE(seen)
while(above && istransparentturf(above))
diff --git a/code/modules/mob/living/silicon/ai/freelook/chunk.dm b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
index 5a9c093a31b..04281f0b762 100644
--- a/code/modules/mob/living/silicon/ai/freelook/chunk.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
@@ -9,8 +9,9 @@
///turfs our cameras can see inside our grid
var/list/visibleTurfs = list()
///cameras that can see into our grid
- ///indexed by the z level of the camera
- var/alist/cameras = alist()
+ ///indexed by the string z level of the camera
+ ///this could one day be an alist but vv doesn't work with it yet
+ var/list/list/cameras = list()
///list of all turfs, associative with that turf's static image
///turf -> /image
var/list/turfs = list()
@@ -26,6 +27,12 @@
/// List of atoms that caused the chunk to update - assoc atom ref() to opacity on queue
var/list/update_sources = list()
+ /// Are we currently being updated by the cameras subsystem?
+ var/currently_updating = FALSE
+ /// List of cameras that need to be processed. For use in yielding when being lazyupdated by the cameras subsystem
+ var/list/list/processing_cameras = list()
+ /// List of newly visible turfs that are currently being generated. For use in lazyupdating.
+ var/list/processing_visible_turfs = list()
/// Add a camera eye to the chunk, updating the chunk if necessary.
/datum/camerachunk/proc/add(mob/eye/camera/eye)
@@ -58,6 +65,7 @@
addtimer(CALLBACK(src, PROC_REF(update)), update_delay_buffer || 1, TIMER_UNIQUE)
return
+ reset_update()
// Only start queue if this is the first thing to queue an update
var/start_queue = !length(update_sources)
@@ -76,6 +84,7 @@
// So we can safely remove this atom as a "source of update"
else if(update_sources[update_key] != update_source.opacity)
update_sources -= update_key
+ _dequeue_update()
return
if(!start_queue)
@@ -91,7 +100,15 @@
// Something forced an update during the delay
if(!length(update_sources))
return
- SScameras.chunks_to_update[src] += 1
+ SScameras.chunks_to_update[src] = TRUE
+
+/datum/camerachunk/proc/_dequeue_update()
+ PRIVATE_PROC(TRUE)
+ // Whelp
+ if(length(update_sources))
+ return
+ SScameras.chunks_to_update -= src
+ SScameras.current_run -= src
/**
* Forces the chunk to update immediately
@@ -103,28 +120,84 @@
return
update()
-/// The actual updating. It gathers the visible turfs from cameras and puts them into the appropiate lists.
+/// Reset any in progress update
+/datum/camerachunk/proc/reset_update()
+ if(!currently_updating)
+ return
+
+ processing_visible_turfs = list()
+ processing_cameras = list()
+ currently_updating = FALSE
+ // Not allowed to stick around in the list forever, that'd be dumb
+ SScameras.current_run -= src
+ _queue_update()
+
+/// Updates our chunk in a lazy fashion, so large amounts of cameras don't lead to overtime spikes
+/// Returns FALSE if the update is unfinished, TRUE if it's complete
+/datum/camerachunk/proc/yield_update()
+ if(SScameras.disable_camera_updates)
+ return TRUE
+
+ if(!currently_updating)
+ currently_updating = TRUE
+ processing_cameras = list()
+ for(var/z_level in lower_z to upper_z)
+ processing_cameras["[z_level]"] = cameras["[z_level]"].Copy()
+
+ var/list/updated_visible_turfs = src.processing_visible_turfs
+ for(var/z_level in lower_z to upper_z)
+ var/list/processing = processing_cameras["[z_level]"]
+ while(length(processing))
+ var/obj/machinery/camera/current_camera = processing[length(processing)]
+ processing.len--
+ if(!current_camera?.can_use())
+ if(TICK_CHECK)
+ return FALSE
+ continue
+
+ var/turf/point = locate(src.x + (CHUNK_SIZE / 2), src.y + (CHUNK_SIZE / 2), z_level)
+ if(get_dist(point, current_camera) > MAX_CAMERA_RANGE + (CHUNK_SIZE / 2))
+ continue
+
+ for(var/turf/vis_turf as anything in current_camera.can_see() & turfs)
+ updated_visible_turfs[vis_turf] = vis_turf
+
+ if(TICK_CHECK)
+ return FALSE
+
+ update_with_turfs(updated_visible_turfs)
+ update_sources.Cut()
+ reset_update()
+ return TRUE
+
+/// Perfroms a full update of the chunk
/datum/camerachunk/proc/update()
if(SScameras.disable_camera_updates)
return
update_sources.Cut()
+ _dequeue_update()
+ reset_update()
var/list/updated_visible_turfs = list()
for(var/z_level in lower_z to upper_z)
- for(var/obj/machinery/camera/current_camera as anything in cameras[z_level])
- if(!current_camera || !current_camera.can_use())
+ for(var/obj/machinery/camera/current_camera as anything in cameras["[z_level]"])
+ if(!current_camera?.can_use())
continue
var/turf/point = locate(src.x + (CHUNK_SIZE / 2), src.y + (CHUNK_SIZE / 2), z_level)
- if(get_dist(point, current_camera) > CHUNK_SIZE + (CHUNK_SIZE / 2))
+ if(get_dist(point, current_camera) > MAX_CAMERA_RANGE + (CHUNK_SIZE / 2))
continue
// The return value of can_see being the left-hand operand here is a load-bearing performance pillar
for(var/turf/vis_turf as anything in current_camera.can_see() & turfs)
updated_visible_turfs[vis_turf] = vis_turf
+ update_with_turfs(updated_visible_turfs)
+
+/// Takes a list of newly visible turfs, updates our static images to match
+/datum/camerachunk/proc/update_with_turfs(list/updated_visible_turfs)
///new turfs that we couldnt see last update but can now
var/list/newly_visible_turfs = updated_visible_turfs - visibleTurfs
///turfs that we could see last update but cant see now
@@ -176,22 +249,18 @@
var/turf/upper_turf = get_highest_turf(locate(x, y, lower_z))
src.upper_z = upper_turf.z
+ var/list/cameras = src.cameras
+ var/list/turfs = src.turfs
+ var/list/visibleTurfs = src.visibleTurfs
+ var/list/obscuredTurfs = src.obscuredTurfs
+ var/list/active_static_images = src.active_static_images
+ var/lower_x = x
+ var/lower_y = y
+ var/upper_x = min(lower_x + CHUNK_SIZE - 1, world.maxx)
+ var/upper_y = min(lower_y + CHUNK_SIZE - 1, world.maxy)
+ var/list/stack = SSmapping.get_connected_levels(lower_z)
for(var/z_level in lower_z to upper_z)
- var/list/local_cameras = list()
- for(var/obj/machinery/camera/camera in urange(CHUNK_SIZE, locate(x + (CHUNK_SIZE / 2), y + (CHUNK_SIZE / 2), z_level)))
- if(camera.can_use())
- local_cameras += camera
-
- for(var/mob/living/silicon/sillycone in urange(CHUNK_SIZE, locate(x + (CHUNK_SIZE / 2), y + (CHUNK_SIZE / 2), z_level)))
- if(sillycone.builtInCamera?.can_use())
- local_cameras += sillycone.builtInCamera
-
- for(var/obj/vehicle/sealed/mecha/mech in urange(CHUNK_SIZE, locate(x + (CHUNK_SIZE / 2), y + (CHUNK_SIZE / 2), z_level)))
- if(mech.chassis_camera?.can_use())
- local_cameras += mech.chassis_camera
-
- cameras[z_level] = local_cameras
-
+ cameras["[z_level]"] = list()
var/image/mirror_from = SScameras.obscured_images[GET_Z_PLANE_OFFSET(z_level) + 1]
var/turf/chunk_corner = locate(x, y, z_level)
for(var/turf/lad as anything in CORNER_BLOCK(chunk_corner, CHUNK_SIZE, CHUNK_SIZE)) //we use CHUNK_SIZE for width and height here as it handles subtracting 1 from those two parameters by itself
@@ -199,15 +268,21 @@
our_image.loc = lad
turfs[lad] = our_image
- for(var/obj/machinery/camera/camera as anything in local_cameras)
- if(!camera)
- continue
+ for(var/obj/machinery/camera/camera as anything in SScameras.cameras)
+ var/turf/camera_loc = get_turf(camera)
+ // AABB
+ if(camera_loc.x + MAX_CAMERA_RANGE < lower_x || camera_loc.x - MAX_CAMERA_RANGE > upper_x)
+ continue
+ if(camera_loc.y + MAX_CAMERA_RANGE < lower_y || camera_loc.y - MAX_CAMERA_RANGE > upper_y)
+ continue
+ if(stack != SSmapping.get_connected_levels(camera_loc.z))
+ continue
+ if(!camera.can_use())
+ continue
- if(!camera.can_use())
- continue
-
- for(var/turf/vis_turf as anything in camera.can_see() & turfs)
- visibleTurfs[vis_turf] = vis_turf
+ cameras["[camera_loc.z]"] += camera
+ for(var/turf/vis_turf as anything in camera.can_see() & turfs)
+ visibleTurfs[vis_turf] = vis_turf
for(var/turf/obscured_turf as anything in turfs - visibleTurfs)
var/image/new_static = turfs[obscured_turf]
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index b22e81b8907..789dc1ef1d8 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -42,7 +42,6 @@
var/law_change_counter = 0
var/obj/machinery/camera/silicon/builtInCamera
- var/updating = FALSE //portable camera camerachunk update
///Whether we have been emagged
var/emagged = FALSE
var/hack_software = FALSE //Will be able to use hacking actions
diff --git a/code/modules/mob/living/silicon/silicon_movement.dm b/code/modules/mob/living/silicon/silicon_movement.dm
index 7ead122301b..353399d5ac9 100644
--- a/code/modules/mob/living/silicon/silicon_movement.dm
+++ b/code/modules/mob/living/silicon/silicon_movement.dm
@@ -1,24 +1,7 @@
//We only call a camera static update if we have successfully moved and the camera is present and working
/mob/living/silicon/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
- if(builtInCamera?.can_use())
- update_camera_location(old_loc)
-
-/mob/living/silicon/proc/update_camera_location(oldLoc)
- oldLoc = get_turf(oldLoc)
- if(!updating && oldLoc != get_turf(src))
- updating = TRUE
- do_camera_update(oldLoc)
-
-///The static update delay on movement of the camera in a borg we use
-#define SILICON_CAMERA_BUFFER 0.5 SECONDS
-
-/**
- * The actual update - also passes our unique update buffer. This makes our static update faster than stationary cameras,
- * helping us to avoid running out of the camera's FoV.
-*/
-/mob/living/silicon/proc/do_camera_update(oldLoc)
- if(oldLoc != get_turf(src))
- SScameras.update_portable_camera(builtInCamera, SILICON_CAMERA_BUFFER)
- updating = FALSE
-#undef SILICON_CAMERA_BUFFER
+ if(!builtInCamera?.can_use())
+ return
+ // Delay's a bit faster then standard cameras to "avoid running out of the camera's fov" whatever that means
+ SScameras.camera_moved(builtInCamera, get_turf(old_loc), get_turf(builtInCamera), 0.5 SECONDS)
diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm
index 491d1402512..c488e1d0ec6 100644
--- a/code/modules/modular_computers/file_system/programs/secureye.dm
+++ b/code/modules/modular_computers/file_system/programs/secureye.dm
@@ -172,7 +172,7 @@
var/datum/camerachunk/target_camerachunk = SScameras.get_turf_camera_chunk(target_turf)
if(!target_camerachunk)
CRASH("[src] was able to track [target] through /datum/trackable, but was not on a visible turf to cameras.")
- for(var/obj/machinery/camera/cameras as anything in target_camerachunk.cameras[target.z])
+ for(var/obj/machinery/camera/cameras as anything in target_camerachunk.cameras["[target.z]"])
// We need to find a particular camera that can see this turf
if(length(cameras.can_see() & list(target_turf)))
continue
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index 9155c307aef..4aa974f0de3 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -83,8 +83,6 @@
var/list/trackers = list()
///Camera installed into the mech
var/obj/machinery/camera/exosuit/chassis_camera
- ///Portable camera camerachunk update
- var/updating = FALSE
var/max_temperature = 25000
diff --git a/code/modules/vehicles/mecha/mecha_movement.dm b/code/modules/vehicles/mecha/mecha_movement.dm
index 46b49cc2ee5..5477f5f2581 100644
--- a/code/modules/vehicles/mecha/mecha_movement.dm
+++ b/code/modules/vehicles/mecha/mecha_movement.dm
@@ -186,25 +186,8 @@
//We only call a camera static update if we have successfully moved and have a camera installed
/obj/vehicle/sealed/mecha/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
- if(chassis_camera)
- update_camera_location(old_loc)
-
-/obj/vehicle/sealed/mecha/proc/update_camera_location(oldLoc)
- oldLoc = get_turf(oldLoc)
- if(!updating && oldLoc != get_turf(src))
- updating = TRUE
- do_camera_update(oldLoc)
-
-///The static update delay on movement of the camera in a mech we use
-#define MECH_CAMERA_BUFFER 0.5 SECONDS
-
-/**
- * The actual update - also passes our unique update buffer. This makes our static update faster than stationary cameras,
- * helping us to avoid running out of the camera's FoV. An EMPd mecha with a lowered view_range on its camera can still
- * sometimes run out into static before updating, however.
-*/
-/obj/vehicle/sealed/mecha/proc/do_camera_update(oldLoc)
- if(oldLoc != get_turf(src))
- SScameras.update_portable_camera(chassis_camera, MECH_CAMERA_BUFFER)
- updating = FALSE
-#undef MECH_CAMERA_BUFFER
+ if(!chassis_camera?.can_use())
+ return
+ // Delay's a bit faster then standard cameras to "avoid running out of the camera's fov" whatever that means
+ // An EMPd mecha with a lowered view_range on its camera can still sometimes run out into static before updating, however.
+ SScameras.camera_moved(chassis_camera, get_turf(old_loc), get_turf(chassis_camera), 0.5 SECONDS)
diff --git a/code/modules/wiremod/components/atom/remotecam.dm b/code/modules/wiremod/components/atom/remotecam.dm
index 070d164c01c..3a1bbd3be47 100644
--- a/code/modules/wiremod/components/atom/remotecam.dm
+++ b/code/modules/wiremod/components/atom/remotecam.dm
@@ -189,7 +189,9 @@
/obj/item/circuit_component/remotecam/proc/update_camera_location(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
SIGNAL_HANDLER
if(current_camera_state && current_cameranet_state)
- SScameras.update_portable_camera(shell_camera, 0.5 SECONDS)
+ if(!shell_camera?.can_use())
+ return
+ SScameras.camera_moved(shell_camera, get_turf(old_loc), get_turf(shell_camera), 0.5 SECONDS)
/**
* Add camera from global cameranet