mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-10 15:45:05 +01:00
21b4095dfd
Upstream 04/17/2026 fixes https://github.com/Bubberstation/Bubberstation/issues/5549 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com> Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com> Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com> Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com> Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com> Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com> Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com> Co-authored-by: loganuk <fakeemail123@aol.com> Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com> Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com> Co-authored-by: Lucy <lucy@absolucy.moe> Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com> Co-authored-by: Isratosh <Isratosh@hotmail.com> Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com> Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com> Co-authored-by: Alexander V. <volas@ya.ru> Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com> Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> Co-authored-by: Cameron Lennox <killer65311@gmail.com> Co-authored-by: Tim <timothymtorres@gmail.com> Co-authored-by: Iamgoofball <iamgoofball@gmail.com> Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com> Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com> Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com> Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com> Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com> Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com> Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com> Co-authored-by: Josh <josh.adam.powell@gmail.com> Co-authored-by: Josh Powell <josh.powell@softwire.com> Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com> Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com> Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com> Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
341 lines
14 KiB
Plaintext
341 lines
14 KiB
Plaintext
/// Manages the security cameras and camera chunks
|
|
SUBSYSTEM_DEF(cameras)
|
|
name = "Cameras"
|
|
ss_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]
|
|
if(chunk.yield_update())
|
|
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 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/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!!!!
|
|
* 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/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)
|
|
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/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)
|
|
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")
|