From 5146cfd4031984ac0ef7403a845b7e9ede1ac426 Mon Sep 17 00:00:00 2001
From: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Date: Fri, 7 Nov 2025 18:43:48 -0600
Subject: [PATCH] Moves camera update handling to background subsystem, (maybe)
fixing lag (#92208)
## About The Pull Request
When a camera update is triggered, it is instead added to a queue on a
background subsystem
An AI entering a camera chunk which is queued to update will force the
update immediately (bypassing the queue)
While the root problem of this is, ultimately, not addressed...
...the change will ultimately prevent update spam from consuming all of
the server's resources - instead allocating updates to the backburner in
times of high server stress (or on multi-z maps)
## Changelog
:cl: Melbert
refactor: Refactored the way camera updates are handled to hopefully
reduce some lag. Report any oddities
/:cl:
---
code/__DEFINES/subsystems.dm | 1 +
code/__HELPERS/spatial_info.dm | 9 +
code/_onclick/ai.dm | 2 +-
code/controllers/subsystem/cameras.dm | 296 ++++++++++++++++++
code/controllers/subsystem/statpanel.dm | 1 -
code/datums/components/simple_bodycam.dm | 2 +-
code/game/machinery/_machinery.dm | 2 +-
code/game/machinery/camera/camera.dm | 44 ++-
code/game/machinery/camera/presets.dm | 1 +
code/game/machinery/computer/camera.dm | 4 +-
.../machinery/computer/camera_advanced.dm | 10 +-
code/game/machinery/doors/airlock.dm | 4 +-
code/game/machinery/doors/door.dm | 14 +-
code/game/machinery/doors/windowdoor.dm | 6 +-
.../effects/effect_system/effect_system.dm | 8 -
code/game/objects/items/devices/multitool.dm | 4 +-
code/game/objects/objs.dm | 4 +
code/game/objects/structures.dm | 2 -
code/game/turfs/change_turf.dm | 2 +-
code/game/turfs/turf.dm | 3 -
code/modules/admin/verbs/debug.dm | 2 +-
code/modules/admin/verbs/mapping.dm | 12 +-
.../antagonists/abductor/machinery/camera.dm | 4 +-
.../antagonists/malf_ai/malf_ai_modules.dm | 8 +-
code/modules/events/camerafailure.dm | 2 +-
code/modules/lighting/lighting_atom.dm | 2 +
.../mining_loot/megafauna/the_thing.dm | 2 +-
code/modules/mob/eye/camera/camera.dm | 2 +-
code/modules/mob/living/living.dm | 2 +-
code/modules/mob/living/silicon/ai/ai.dm | 18 +-
.../living/silicon/ai/freelook/cameranet.dm | 253 ---------------
.../mob/living/silicon/ai/freelook/chunk.dm | 110 ++++---
.../mob/living/silicon/ai/freelook/eye.dm | 2 +-
.../mob/living/silicon/silicon_movement.dm | 2 +-
.../file_system/programs/secureye.dm | 13 +-
code/modules/photography/camera/camera.dm | 23 +-
.../research/xenobiology/xenobio_camera.dm | 2 +-
.../mobile_port/shuttle_move_callbacks.dm | 4 +-
code/modules/transport/tram/tram_doors.dm | 2 -
code/modules/vehicles/mecha/mecha_movement.dm | 2 +-
.../wiremod/components/atom/remotecam.dm | 10 +-
tgstation.dme | 2 +-
42 files changed, 472 insertions(+), 426 deletions(-)
create mode 100644 code/controllers/subsystem/cameras.dm
delete mode 100644 code/modules/mob/living/silicon/ai/freelook/cameranet.dm
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index a002b4bef93..9521983acd8 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -158,6 +158,7 @@
#define FIRE_PRIORITY_PARALLAX 65
#define FIRE_PRIORITY_INSTRUMENTS 80
#define FIRE_PRIORITY_FLUIDS 80
+#define FIRE_PRIORITY_CAMERAS 85
#define FIRE_PRIORITY_PRIORITY_EFFECTS 90
#define FIRE_PRIORITY_MOBS 100
#define FIRE_PRIORITY_TGUI 110
diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm
index a8100f6a7a0..1135c93b055 100644
--- a/code/__HELPERS/spatial_info.dm
+++ b/code/__HELPERS/spatial_info.dm
@@ -403,6 +403,15 @@
. = view(range, source)
source.luminosity = lum
+/// 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
+ . = list()
+ for(var/turf/turf in view(range, source))
+ . += turf
+ source.luminosity = lum
+
///Returns the open turf next to the center in a specific direction
/proc/get_open_turf_in_dir(atom/center, dir)
var/turf/open/get_turf = get_step(center, dir)
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index 1f5c943d454..394fe4cd206 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -294,4 +294,4 @@
//
/mob/living/silicon/ai/TurfAdjacent(turf/target_turf)
- return (GLOB.cameranet && GLOB.cameranet.checkTurfVis(target_turf))
+ return (SScameras.is_visible_by_cameras(target_turf))
diff --git a/code/controllers/subsystem/cameras.dm b/code/controllers/subsystem/cameras.dm
new file mode 100644
index 00000000000..5f5e9a2ec1b
--- /dev/null
+++ b/code/controllers/subsystem/cameras.dm
@@ -0,0 +1,296 @@
+/// Manages the security cameras and camera chunks
+SUBSYSTEM_DEF(cameras)
+ name = "Cameras"
+ flags = SS_BACKGROUND
+ priority = FIRE_PRIORITY_CAMERAS
+ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+ wait = 2 MINUTES
+ dependencies = list(
+ // Required to get plane offset for static images
+ /datum/controller/subsystem/mapping,
+ )
+
+ /// The cameras on the map, no matter if they work or not.
+ /// Updated in obj/machinery/camera.dm in Initialize() and Destroy().
+ var/list/obj/machinery/camera/cameras = list()
+ /// The chunks of the map, mapping the areas that the cameras can see.
+ var/list/chunks = list()
+ /// Chunks that must be updated
+ var/list/chunks_to_update = list()
+ /// List of images cloned by all chunk static images put onto turfs cameras cant see
+ /// Indexed by the plane offset to use
+ var/list/image/obscured_images = list()
+ /// Primarily for debugging, outright prevents all camera chunk updates
+ var/disable_camera_updates = FALSE
+ /// Tracks current subsystem run
+ var/list/current_run = list()
+
+/datum/controller/subsystem/cameras/Initialize()
+ update_offsets(SSmapping.max_plane_offset)
+ RegisterSignal(SSmapping, COMSIG_PLANE_OFFSET_INCREASE, PROC_REF(on_offset_growth))
+ return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/cameras/fire(resumed = FALSE)
+ if(!resumed)
+ src.current_run = chunks_to_update.Copy()
+ chunks_to_update = list()
+
+ 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(MC_TICK_CHECK)
+ break
+
+/datum/controller/subsystem/cameras/stat_entry(msg)
+ msg = "Cams: [length(cameras)] | Chunks: [length(chunks)] | Updating: [length(chunks_to_update)]"
+ return ..()
+
+/// Updates the images for new plane offsets
+/datum/controller/subsystem/cameras/proc/update_offsets(new_offset)
+ for(var/i in length(obscured_images) to new_offset)
+ var/image/obscured = new('icons/effects/cameravis.dmi')
+ SET_PLANE_W_SCALAR(obscured, CAMERA_STATIC_PLANE, i)
+ obscured.appearance_flags = RESET_TRANSFORM | RESET_ALPHA | RESET_COLOR | KEEP_APART
+ obscured.override = TRUE
+ obscured_images += obscured
+
+/datum/controller/subsystem/cameras/proc/on_offset_growth(datum/source, old_offset, new_offset)
+ SIGNAL_HANDLER
+ update_offsets(new_offset)
+
+/// Checks if a chunk has been generated in x, y, z.
+/datum/controller/subsystem/cameras/proc/get_camera_chunk(x, y, z)
+ x = GET_CHUNK_COORD(x)
+ y = GET_CHUNK_COORD(y)
+ if(GET_LOWEST_STACK_OFFSET(z) != 0)
+ var/turf/lowest = get_lowest_turf(locate(x, y, z))
+ return chunks["[x],[y],[lowest.z]"]
+
+ return chunks["[x],[y],[z]"]
+
+// Returns the chunk in the x, y, z.
+// If there is no chunk, it creates a new chunk and returns that.
+/datum/controller/subsystem/cameras/proc/generate_chunk(x, y, z)
+ x = GET_CHUNK_COORD(x)
+ y = GET_CHUNK_COORD(y)
+ var/turf/lowest = get_lowest_turf(locate(x, y, z))
+ var/key = "[x],[y],[lowest.z]"
+ . = chunks[key]
+ if(!.)
+ . = new /datum/camerachunk(x, y, lowest.z)
+ chunks[key] = .
+
+/// Updates what the camera eye can see.
+/// It is recommended you use this when a camera eye moves or its location is set.
+/datum/controller/subsystem/cameras/proc/update_eye_chunk(mob/eye/camera/eye)
+ var/list/visibleChunks = list()
+ //Get the eye's turf in case its located in an object like a mecha
+ var/turf/eye_turf = get_turf(eye)
+ if(eye.loc)
+ var/static_range = eye.static_visibility_range
+ var/x1 = max(1, eye_turf.x - static_range)
+ var/y1 = max(1, eye_turf.y - static_range)
+ var/x2 = min(world.maxx, eye_turf.x + static_range)
+ var/y2 = min(world.maxy, eye_turf.y + static_range)
+
+ for(var/x = x1; x <= x2; x += CHUNK_SIZE)
+ for(var/y = y1; y <= y2; y += CHUNK_SIZE)
+ visibleChunks |= generate_chunk(x, y, eye_turf.z)
+
+ var/list/remove = eye.visibleCameraChunks - visibleChunks
+ var/list/add = visibleChunks - eye.visibleCameraChunks
+
+ for(var/datum/camerachunk/chunk as anything in remove)
+ chunk.remove(eye)
+
+ for(var/datum/camerachunk/chunk as anything in add)
+ chunk.add(eye)
+
+/// Used in [/proc/major_chunk_change] - indicates the camera should be removed from the chunk list.
+#define REMOVE_CAMERA 0
+/// Used in [/proc/major_chunk_change] - indicates the camera should be added to the chunk list.
+#define ADD_CAMERA 1
+/// Used in [/proc/major_chunk_change] - indicates the chunk should be updated without adding/removing a camera.
+#define IGNORE_CAMERA 2
+
+/// Updates the chunks that the turf is located in. Use this when obstacles are destroyed or when doors open.
+/datum/controller/subsystem/cameras/proc/update_visibility(atom/relevant_atom)
+ if(!SSticker)
+ return
+ major_chunk_change(relevant_atom, IGNORE_CAMERA)
+
+/// Removes a camera from a chunk.
+/datum/controller/subsystem/cameras/proc/remove_camera_from_chunk(obj/machinery/camera/old_cam)
+ major_chunk_change(old_cam, REMOVE_CAMERA)
+
+/// Add a camera to a chunk.
+/datum/controller/subsystem/cameras/proc/add_camera_to_chunk(obj/machinery/camera/new_cam)
+ if(new_cam.can_use())
+ 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.
+*/
+/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)
+
+/**
+ * Never access this proc directly!!!!
+ * This will update the chunk and all the surrounding chunks.
+ * It will also add the atom to the cameras list if you set the choice to 1.
+ * Setting the choice to 0 will remove the camera from the chunks.
+ * If you want to update the chunks around an object, without adding/removing a camera, use choice 2.
+ * update_delay_buffer is passed all the way to queue_update() from portable camera updates on movement
+ * to change the time between static updates.
+ */
+/datum/controller/subsystem/cameras/proc/major_chunk_change(atom/center_or_camera, choice = IGNORE_CAMERA, update_delay_buffer = 0)
+ PROTECTED_PROC(TRUE)
+
+ if(QDELETED(center_or_camera) && choice == ADD_CAMERA)
+ CRASH("Tried to add a qdeleting camera to the net")
+
+ var/turf/chunk_turf = get_turf(center_or_camera)
+ 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))
+ 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)
+ if(isnull(chunk))
+ continue
+ if(choice == REMOVE_CAMERA)
+ // Remove the 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.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))
+ 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)
+ chunk?.queue_update(changed, 0)
+
+/// Will check if an atom is on a viewable turf.
+/// Returns TRUE if the atom is visible by any camera, FALSE otherwise.
+/datum/controller/subsystem/cameras/proc/is_visible_by_cameras(atom/target)
+ return turf_visible_by_cameras(get_turf(target))
+
+/// Checks if the passed turf is visible by any camera.
+/// Returns TRUE if the turf is visible by any camera, FALSE otherwise.
+/datum/controller/subsystem/cameras/proc/turf_visible_by_cameras(turf/position)
+ PRIVATE_PROC(TRUE)
+ if(isnull(position))
+ return FALSE
+ var/datum/camerachunk/chunk = generate_chunk(position.x, position.y, position.z)
+ if(isnull(chunk))
+ return FALSE
+ chunk.force_update(only_if_necessary = TRUE) // Update NOW if necessary
+ if(chunk.visibleTurfs[position])
+ return TRUE
+ return FALSE
+
+/// Gets the camera chunk the passed turf is in.
+/// Returns the chunk if it exists and is visible, null otherwise.
+/datum/controller/subsystem/cameras/proc/get_turf_camera_chunk(turf/position)
+ RETURN_TYPE(/datum/camerachunk)
+ var/datum/camerachunk/chunk = generate_chunk(position.x, position.y, position.z)
+ if(!chunk)
+ return null
+ chunk.force_update(only_if_necessary = TRUE) // Update NOW if necessary
+ if(chunk.visibleTurfs[position])
+ return chunk
+ return null
+
+/// Returns list of available cameras, ready to use for UIs displaying list of them
+/// The format is: list("name" = "camera.c_tag", ref = REF(camera))
+/datum/controller/subsystem/cameras/proc/get_available_cameras_data(list/networks_available, list/z_levels_available)
+ var/list/available_cameras_data = list()
+ for(var/obj/machinery/camera/camera as anything in get_filtered_and_sorted_cameras(networks_available, z_levels_available))
+ available_cameras_data += list(list(
+ name = camera.c_tag,
+ ref = REF(camera),
+ ))
+
+ return available_cameras_data
+
+/**
+ * get_available_camera_by_tag_list
+ *
+ * Builds a list of all available cameras that can be seen to networks_available and in z_levels_available.
+ * Entries are stored in `c_tag[camera.can_use() ? null : " (Deactivated)"]` => `camera` format
+ * Args:
+ * networks_available - List of networks that we use to see which cameras are visible to it.
+ * z_levels_available - List of z levels to filter camera by. If empty, all z levels are considered valid.
+ * sort_by_ctag - If the resulting list should be sorted by `c_tag`.
+ */
+/datum/controller/subsystem/cameras/proc/get_available_camera_by_tag_list(list/networks_available, list/z_levels_available)
+ var/list/available_cameras_by_tag = list()
+ for(var/obj/machinery/camera/camera as anything in get_filtered_and_sorted_cameras(networks_available, z_levels_available))
+ available_cameras_by_tag["[camera.c_tag][camera.can_use() ? null : " (Deactivated)"]"] = camera
+
+ return available_cameras_by_tag
+
+/// Returns list of all cameras that passed `is_camera_available` filter and sorted by `cmp_camera_ctag_asc`
+/datum/controller/subsystem/cameras/proc/get_filtered_and_sorted_cameras(list/networks_available, list/z_levels_available)
+ PRIVATE_PROC(TRUE)
+
+ var/list/filtered_cameras = list()
+ for(var/obj/machinery/camera/camera as anything in cameras)
+ if(!is_camera_available(camera, networks_available, z_levels_available))
+ continue
+
+ filtered_cameras += camera
+
+ return sortTim(filtered_cameras, GLOBAL_PROC_REF(cmp_camera_ctag_asc))
+
+/// Checks if the `camera_to_check` meets the requirements of availability.
+/datum/controller/subsystem/cameras/proc/is_camera_available(obj/machinery/camera/camera_to_check, list/networks_available, list/z_levels_available)
+ PRIVATE_PROC(TRUE)
+
+ if(!camera_to_check.c_tag)
+ return FALSE
+
+ if(length(z_levels_available) && !(camera_to_check.z in z_levels_available))
+ return FALSE
+
+ return length(camera_to_check.network & networks_available) > 0
+
+#undef ADD_CAMERA
+#undef REMOVE_CAMERA
+#undef IGNORE_CAMERA
+
+/obj/effect/overlay/camera_static
+ name = "static"
+ icon = null
+ icon_state = null
+ anchored = TRUE // should only appear in vis_contents, but to be safe
+ appearance_flags = RESET_TRANSFORM | TILE_BOUND | LONG_GLIDE
+ // this combination makes the static block clicks to everything below it,
+ // without appearing in the right-click menu for non-AI clients
+ mouse_opacity = MOUSE_OPACITY_ICON
+ invisibility = INVISIBILITY_ABSTRACT
+
+ plane = CAMERA_STATIC_PLANE
+
+ADMIN_VERB(pause_camera_updates, R_ADMIN, "Toggle Camera Updates", "Stop security cameras from updating, meaning what they see now is what they will see forever.", ADMIN_CATEGORY_DEBUG)
+ SScameras.disable_camera_updates = !SScameras.disable_camera_updates
+ log_admin("[key_name_admin(user)] [SScameras.disable_camera_updates ? "disabled" : "enabled"] camera updates.")
+ message_admins("Admin [key_name_admin(user)] has [SScameras.disable_camera_updates ? "disabled" : "enabled"] camera updates.")
+ BLACKBOX_LOG_ADMIN_VERB("Toggle Camera Updates")
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index d7d4b38793d..53e724b4543 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -220,7 +220,6 @@ SUBSYSTEM_DEF(statpanels)
#endif
for(var/datum/controller/subsystem/sub_system as anything in Master.subsystems)
mc_data[++mc_data.len] = list("\[[sub_system.state_letter()]][sub_system.name]", sub_system.stat_entry(), text_ref(sub_system))
- mc_data[++mc_data.len] = list("Camera Net", "Cameras: [GLOB.cameranet.cameras.len] | Chunks: [GLOB.cameranet.chunks.len]", text_ref(GLOB.cameranet))
///immediately update the active statpanel tab of the target client
/datum/controller/subsystem/statpanels/proc/immediate_send_stat_data(client/target)
diff --git a/code/datums/components/simple_bodycam.dm b/code/datums/components/simple_bodycam.dm
index 9d653f38a78..1723a7a0a5a 100644
--- a/code/datums/components/simple_bodycam.dm
+++ b/code/datums/components/simple_bodycam.dm
@@ -56,7 +56,7 @@
do_update_cam()
/datum/component/simple_bodycam/proc/do_update_cam()
- GLOB.cameranet.updatePortableCamera(bodycam, camera_update_time)
+ SScameras.update_portable_camera(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/_machinery.dm b/code/game/machinery/_machinery.dm
index 2023722ea35..6abdd7900ed 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -738,7 +738,7 @@
var/mob/user = ui.user
add_fingerprint(user)
update_last_used(user)
- if(isAI(user) && !GLOB.cameranet.checkTurfVis(get_turf(src))) //We check if they're an AI specifically here, so borgs/adminghosts/human wand can still access off-camera stuff.
+ if(isAI(user) && !SScameras.is_visible_by_cameras(get_turf(src))) //We check if they're an AI specifically here, so borgs/adminghosts/human wand can still access off-camera stuff.
to_chat(user, span_warning("You can no longer connect to this device!"))
return FALSE
return ..()
diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm
index 6d214d6781b..f323ddc30ac 100644
--- a/code/game/machinery/camera/camera.dm
+++ b/code/game/machinery/camera/camera.dm
@@ -110,12 +110,12 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
network -= network_name
network += LOWER_TEXT(network_name)
- GLOB.cameranet.cameras += src
+ SScameras.cameras += src
myarea = get_room_area()
if(camera_enabled)
- GLOB.cameranet.addCamera(src)
+ SScameras.add_camera_to_chunk(src)
LAZYADD(myarea.cameras, src)
#ifdef MAP_TEST
update_appearance()
@@ -133,8 +133,8 @@ 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
- GLOB.cameranet.removeCamera(src)
- GLOB.cameranet.cameras -= src
+ SScameras.remove_camera_from_chunk(src)
+ SScameras.cameras -= src
cancelCameraAlarm()
if(isarea(myarea))
LAZYREMOVE(myarea.cameras, src)
@@ -219,7 +219,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
if(!prob(150 / severity))
return
network = list()
- GLOB.cameranet.removeCamera(src)
+ SScameras.remove_camera_from_chunk(src)
set_machine_stat(machine_stat | EMPED)
set_light(0)
emped++ //Increase the number of consecutive EMP's
@@ -246,7 +246,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
set_machine_stat(machine_stat & ~EMPED)
update_appearance()
if(can_use())
- GLOB.cameranet.addCamera(src)
+ SScameras.add_camera_to_chunk(src)
emped = 0 //Resets the consecutive EMP count
addtimer(CALLBACK(src, PROC_REF(cancelCameraAlarm)), 10 SECONDS)
@@ -259,7 +259,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
/obj/machinery/camera/proc/setViewRange(num = 7)
src.view_range = num
- GLOB.cameranet.updateVisibility(src, 0)
+ SScameras.update_visibility(src)
/obj/machinery/camera/proc/shock(mob/living/user)
if(!istype(user))
@@ -336,7 +336,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
/obj/machinery/camera/proc/toggle_cam(mob/user, displaymessage = TRUE)
camera_enabled = !camera_enabled
if(can_use())
- GLOB.cameranet.addCamera(src)
+ SScameras.add_camera_to_chunk(src)
if (isturf(loc))
myarea = get_area(src)
LAZYADD(myarea.cameras, src)
@@ -344,12 +344,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
myarea = null
else
set_light(0)
- GLOB.cameranet.removeCamera(src)
+ SScameras.remove_camera_from_chunk(src)
if (isarea(myarea))
LAZYREMOVE(myarea.cameras, src)
// We are not guarenteed that the camera will be on a turf. account for that
- var/turf/our_turf = get_turf(src)
- GLOB.cameranet.updateChunk(our_turf.x, our_turf.y, our_turf.z)
var/change_msg = "deactivates"
if(camera_enabled)
change_msg = "reactivates"
@@ -389,6 +387,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
return FALSE
return TRUE
+/// Returns a list of turfs in this camera's view.
+/// This includes turfs that are "obscured by darkness" from the camera's POV.
/obj/machinery/camera/proc/can_see()
var/list/see = null
var/turf/pos = get_turf(src)
@@ -397,26 +397,22 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/camera/xray, 0)
var/check_higher = directly_above && istransparentturf(directly_above) && (pos != get_highest_turf(pos))
if(isXRay())
- see = range(view_range, pos)
+ see = RANGE_TURFS(view_range, pos)
else
- see = get_hear(view_range, pos)
+ see = get_hear_turfs(view_range, pos)
+
if(check_lower || check_higher)
// Haha datum var access KILL ME
- for(var/turf/seen in see)
+ for(var/turf/seen as anything in see)
if(check_lower)
- var/turf/visible = seen
- while(visible && istransparentturf(visible))
- var/turf/below = GET_TURF_BELOW(visible)
- for(var/turf/adjacent in range(1, below))
- see += adjacent
- see += adjacent.contents
- visible = below
+ var/turf/below = GET_TURF_BELOW(seen)
+ while(below && istransparentturf(below))
+ see += RANGE_TURFS(1, below)
+ below = GET_TURF_BELOW(below)
if(check_higher)
var/turf/above = GET_TURF_ABOVE(seen)
while(above && istransparentturf(above))
- for(var/turf/adjacent in range(1, above))
- see += adjacent
- see += adjacent.contents
+ see += RANGE_TURFS(1, above)
above = GET_TURF_ABOVE(above)
return see
diff --git a/code/game/machinery/camera/presets.dm b/code/game/machinery/camera/presets.dm
index 89af3fc1192..7383db15f66 100644
--- a/code/game/machinery/camera/presets.dm
+++ b/code/game/machinery/camera/presets.dm
@@ -185,6 +185,7 @@
camera_upgrade_bitflags |= CAMERA_UPGRADE_XRAY
update_appearance()
+ SScameras.update_visibility(src)
/obj/machinery/camera/proc/removeXRay(ignore_malf_upgrades)
if(!ignore_malf_upgrades) //don't downgrade it if malf software is forced onto it.
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index 8725bda22f5..f33cff0c7ea 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -89,7 +89,7 @@
var/list/data = list()
data["network"] = network
data["mapRef"] = cam_screen.assigned_map
- data["cameras"] = GLOB.cameranet.get_available_cameras_data(network)
+ data["cameras"] = SScameras.get_available_cameras_data(network)
return data
/obj/machinery/computer/security/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
@@ -99,7 +99,7 @@
if(action == "switch_camera")
active_camera?.on_stop_watching(src)
- var/obj/machinery/camera/selected_camera = locate(params["camera"]) in GLOB.cameranet.cameras
+ var/obj/machinery/camera/selected_camera = locate(params["camera"]) in SScameras.cameras
active_camera = selected_camera
if(isnull(active_camera))
diff --git a/code/game/machinery/computer/camera_advanced.dm b/code/game/machinery/computer/camera_advanced.dm
index bb034b1a322..c364afa9ec3 100644
--- a/code/game/machinery/computer/camera_advanced.dm
+++ b/code/game/machinery/computer/camera_advanced.dm
@@ -183,10 +183,10 @@
else
camera_location = myturf
else
- if((!consider_zlock || (myturf.z in z_lock)) && GLOB.cameranet.checkTurfVis(myturf))
+ if((!consider_zlock || (myturf.z in z_lock)) && SScameras.is_visible_by_cameras(myturf))
camera_location = myturf
else
- for(var/obj/machinery/camera/C as anything in GLOB.cameranet.cameras)
+ for(var/obj/machinery/camera/C as anything in SScameras.cameras)
if(!C.can_use() || consider_zlock && !(C.z in z_lock))
continue
var/list/network_overlap = networks & C.network
@@ -229,7 +229,7 @@
var/mob/eye/camera/remote/remote_eye = owner.remote_control
var/obj/machinery/computer/camera_advanced/origin = remote_eye.origin_ref.resolve()
- var/list/cameras_by_tag = GLOB.cameranet.get_available_camera_by_tag_list(origin.networks, origin.z_lock)
+ var/list/cameras_by_tag = SScameras.get_available_camera_by_tag_list(origin.networks, origin.z_lock)
playsound(origin, 'sound/machines/terminal/terminal_prompt.ogg', 25, FALSE)
var/camera = tgui_input_list(usr, "Camera to view", "Cameras", cameras_by_tag)
@@ -403,7 +403,7 @@
var/turf/eye_turf = get_turf(source)
if(!eye_turf)
return
- if(!GLOB.cameranet.checkTurfVis(eye_turf))
+ if(!SScameras.is_visible_by_cameras(eye_turf))
return
eye_x.set_output(source.x)
eye_y.set_output(source.y)
@@ -490,7 +490,7 @@
var/turf/target_turf = get_turf(target)
if(!target_turf)
return
- if(!GLOB.cameranet.checkTurfVis(target_turf))
+ if(!SScameras.is_visible_by_cameras(target_turf))
return
if(TIMER_COOLDOWN_RUNNING(parent.shell, COOLDOWN_CIRCUIT_TARGET_INTERCEPT))
return
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index aea2ac558f7..65ce845a44b 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -1356,10 +1356,9 @@
set_airlock_state(AIRLOCK_OPENING, animated = TRUE, force_type = forced)
var/transparent_delay = animation_segment_delay(AIRLOCK_OPENING_TRANSPARENT)
sleep(transparent_delay)
- set_opacity(0)
+ set_opacity(FALSE)
if(multi_tile)
filler.set_opacity(FALSE)
- update_freelook_sight()
var/passable_delay = animation_segment_delay(AIRLOCK_OPENING_PASSABLE) - transparent_delay
sleep(passable_delay)
set_density(FALSE)
@@ -1446,7 +1445,6 @@
set_opacity(TRUE)
if(multi_tile)
filler.set_opacity(TRUE)
- update_freelook_sight()
var/close_delay = animation_segment_delay(AIRLOCK_CLOSING_FINISHED) - unpassable_delay - opaque_delay
sleep(close_delay)
set_airlock_state(AIRLOCK_CLOSED, animated = FALSE)
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 697de876023..daecb318bb8 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -98,7 +98,6 @@
set_bounds()
set_filler()
update_overlays()
- update_freelook_sight()
air_update_turf(TRUE, TRUE)
register_context()
if(elevator_mode)
@@ -163,7 +162,6 @@
layer = initial(layer)
/obj/machinery/door/Destroy()
- update_freelook_sight()
if(elevator_mode)
GLOB.elevator_doors -= src
if(spark_system)
@@ -508,7 +506,7 @@
operating = TRUE
use_energy(active_power_usage)
run_animation(DOOR_OPENING_ANIMATION)
- set_opacity(0)
+ set_opacity(FALSE)
var/passable_delay = animation_segment_delay(DOOR_OPENING_PASSABLE)
SLEEP_NOT_DEL(passable_delay)
set_density(FALSE)
@@ -517,10 +515,9 @@
SLEEP_NOT_DEL(open_delay)
layer = initial(layer)
update_appearance()
- set_opacity(0)
+ set_opacity(FALSE)
operating = FALSE
air_update_turf(TRUE, FALSE)
- update_freelook_sight()
if(autoclose)
autoclose_in(DOOR_CLOSE_WAIT)
return TRUE
@@ -556,10 +553,9 @@
SLEEP_NOT_DEL(close_delay)
update_appearance()
if(visible && !glass)
- set_opacity(1)
+ set_opacity(TRUE)
operating = FALSE
air_update_turf(TRUE, TRUE)
- update_freelook_sight()
if(!can_crush)
return TRUE
@@ -618,10 +614,6 @@
/obj/machinery/door/proc/hasPower()
return !(machine_stat & NOPOWER)
-/obj/machinery/door/proc/update_freelook_sight()
- if(!glass && GLOB.cameranet)
- GLOB.cameranet.updateVisibility(src, 0)
-
/obj/machinery/door/block_superconductivity() // All non-glass airlocks block heat, this is intended.
if(opacity || heat_proof)
return 1
diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm
index 8ede2092a4c..7579756590f 100644
--- a/code/game/machinery/doors/windowdoor.dm
+++ b/code/game/machinery/doors/windowdoor.dm
@@ -256,7 +256,6 @@
var/open_delay = animation_segment_delay(DOOR_OPENING_FINISHED) - passable_delay
sleep(open_delay)
air_update_turf(TRUE, FALSE)
- update_freelook_sight()
if(operating == 1) //emag again
operating = FALSE
@@ -299,7 +298,6 @@
sleep(unpassable_delay)
set_density(TRUE)
air_update_turf(TRUE, TRUE)
- update_freelook_sight()
var/close_delay = animation_segment_delay(DOOR_CLOSING_FINISHED) - unpassable_delay
sleep(close_delay)
@@ -549,8 +547,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/brigdoor/security/holding
operating = TRUE
set_density(FALSE)
+ set_opacity(FALSE)
air_update_turf(TRUE, FALSE)
- update_freelook_sight()
operating = FALSE
update_appearance()
@@ -564,8 +562,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/brigdoor/security/holding
operating = TRUE
set_density(TRUE)
+ set_opacity(TRUE)
air_update_turf(TRUE, TRUE)
- update_freelook_sight()
operating = FALSE
update_appearance()
diff --git a/code/game/objects/effects/effect_system/effect_system.dm b/code/game/objects/effects/effect_system/effect_system.dm
index 6ddd65f12cf..73ffc52a2bd 100644
--- a/code/game/objects/effects/effect_system/effect_system.dm
+++ b/code/game/objects/effects/effect_system/effect_system.dm
@@ -12,14 +12,6 @@ would spawn and follow the beaker, even if it is carried or thrown.
pass_flags = PASSTABLE | PASSGRILLE
anchored = TRUE
-/obj/effect/particle_effect/Initialize(mapload)
- . = ..()
- GLOB.cameranet.updateVisibility(src)
-
-/obj/effect/particle_effect/Destroy()
- GLOB.cameranet.updateVisibility(src)
- return ..()
-
// Prevents effects from getting registered for SSnewtonian_movement
/obj/effect/particle_effect/newtonian_move(inertia_angle, instant = FALSE, start_delay = 0, drift_force = 0, controlled_cap = null)
return TRUE
diff --git a/code/game/objects/items/devices/multitool.dm b/code/game/objects/items/devices/multitool.dm
index e7244491423..626c42d65a4 100644
--- a/code/game/objects/items/devices/multitool.dm
+++ b/code/game/objects/items/devices/multitool.dm
@@ -295,10 +295,10 @@
for(var/x = x1; x <= x2; x += CHUNK_SIZE)
for(var/y = y1; y <= y2; y += CHUNK_SIZE)
- var/datum/camerachunk/chunk = GLOB.cameranet.getCameraChunk(x, y, epicenter.z)
+ var/datum/camerachunk/chunk = SScameras.generate_chunk(x, y, epicenter.z)
// removing cameras in build mode didnt affect it and i guess it needs an AI eye to update so we have to do this manually
// unless we only want to see static in a jank manner only if an eye updates it
- chunk?.update() // UPDATE THE FUCK NOW
+ chunk?.force_update(only_if_necessary = FALSE) // UPDATE THE FUCK NOW
. |= chunk
/obj/item/multitool/ai_detect/proc/cleanup_static()
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 10b500824e5..cc7cf0f2ccd 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -64,10 +64,14 @@ GLOBAL_LIST_EMPTY(objects_by_id_tag)
if (id_tag)
GLOB.objects_by_id_tag[id_tag] = src
+ if(opacity)
+ SScameras.update_visibility(src)
/obj/Destroy(force)
if(!ismachinery(src))
STOP_PROCESSING(SSobj, src) // TODO: Have a processing bitflag to reduce on unnecessary loops through the processing lists
+ if(opacity)
+ SScameras.update_visibility(src)
SStgui.close_uis(src)
GLOB.objects_by_id_tag -= id_tag
. = ..()
diff --git a/code/game/objects/structures.dm b/code/game/objects/structures.dm
index 3e7e13c1df2..9d4442238ac 100644
--- a/code/game/objects/structures.dm
+++ b/code/game/objects/structures.dm
@@ -23,10 +23,8 @@
if(smoothing_flags & USES_SMOOTHING)
QUEUE_SMOOTH(src)
QUEUE_SMOOTH_NEIGHBORS(src)
- GLOB.cameranet.updateVisibility(src)
/obj/structure/Destroy(force)
- GLOB.cameranet.updateVisibility(src)
if(smoothing_flags & USES_SMOOTHING)
QUEUE_SMOOTH_NEIGHBORS(src)
return ..()
diff --git a/code/game/turfs/change_turf.dm b/code/game/turfs/change_turf.dm
index 347a3574769..49ebdb577a9 100644
--- a/code/game/turfs/change_turf.dm
+++ b/code/game/turfs/change_turf.dm
@@ -193,7 +193,7 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
space_tile.enable_starlight()
if(old_opacity != opacity && SSticker)
- GLOB.cameranet.bareMajorChunkChange(src)
+ SScameras.bare_major_chunk_change(src)
// We will only run this logic if the tile is not on the prime z layer, since we use area overlays to cover that
if(SSmapping.z_level_to_plane_offset[z])
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 0243b866d82..53f75673d87 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -553,9 +553,6 @@ GLOBAL_LIST_EMPTY(station_turfs)
/turf/proc/can_lay_cable()
return can_have_cabling() && underfloor_accessibility >= UNDERFLOOR_INTERACTABLE
-/turf/proc/visibilityChanged()
- GLOB.cameranet.updateVisibility(src)
-
/turf/proc/burn_tile()
return
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index a981a5c3e74..47098fd8a3f 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -320,7 +320,7 @@ ADMIN_VERB(cmd_admin_areatest, R_DEBUG, "Test Areas", "Tests the areas for vario
areas_with_intercom.Add(A.type)
CHECK_TICK
- for(var/obj/machinery/camera/C as anything in GLOB.cameranet.cameras)
+ for(var/obj/machinery/camera/C as anything in SScameras.cameras)
var/area/A = get_area(C)
if(!A)
dat += "Skipped over [C] in invalid location, [C.loc].
"
diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm
index dcb9d902f31..8adfd07671e 100644
--- a/code/modules/admin/verbs/mapping.dm
+++ b/code/modules/admin/verbs/mapping.dm
@@ -8,11 +8,11 @@ ADMIN_VERB(camera_view, R_DEBUG, "Camera Range Display", "Shows the range of cam
if(!on)
var/list/seen = list()
- for(var/obj/machinery/camera/C as anything in GLOB.cameranet.cameras)
- for(var/turf/T in C.can_see())
- seen[T]++
- for(var/turf/T in seen)
- T.maptext = MAPTEXT(seen[T])
+ for(var/obj/machinery/camera/cam as anything in SScameras.cameras)
+ for(var/turf/cam_turf as anything in cam.can_see())
+ seen[cam]++
+ for(var/turf/seen_turf as anything in seen)
+ seen_turf.maptext = MAPTEXT(seen[seen_turf])
BLACKBOX_LOG_ADMIN_VERB("Show Camera Range")
#ifdef TESTING
@@ -34,7 +34,7 @@ ADMIN_VERB_VISIBILITY(sec_camera_report, ADMIN_VERB_VISIBLITY_FLAG_MAPPING_DEBUG
ADMIN_VERB(sec_camera_report, R_DEBUG, "Camera Report", "Get a printout of all camera issues.", ADMIN_CATEGORY_MAPPING)
var/list/obj/machinery/camera/CL = list()
- for(var/obj/machinery/camera/C as anything in GLOB.cameranet.cameras)
+ for(var/obj/machinery/camera/C as anything in SScameras.cameras)
CL += C
var/output = {"Camera Abnormalities Report