mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-12 08:36:00 +01:00
5146cfd403
## 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... <img width="554" height="58" alt="467828777-eff3f0e5-49d6-4997-b4d7-05eff6432155" src="https://github.com/user-attachments/assets/c2d6a5f5-d958-463e-959f-116bd0dab475" /> ...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 🆑 Melbert refactor: Refactored the way camera updates are handled to hopefully reduce some lag. Report any oddities /🆑
429 lines
16 KiB
Plaintext
429 lines
16 KiB
Plaintext
|
|
#define CAMERA_PICTURE_SIZE_HARD_LIMIT 21
|
|
|
|
/obj/item/camera
|
|
name = "camera"
|
|
icon = 'icons/obj/art/camera.dmi'
|
|
desc = "A polaroid camera."
|
|
icon_state = "camera"
|
|
inhand_icon_state = "camera"
|
|
worn_icon_state = "camera"
|
|
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
|
|
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
|
|
light_system = OVERLAY_LIGHT_DIRECTIONAL //Used as a flash here.
|
|
light_range = 6
|
|
light_color = COLOR_WHITE
|
|
light_power = FLASH_LIGHT_POWER
|
|
light_on = FALSE
|
|
w_class = WEIGHT_CLASS_SMALL
|
|
obj_flags = CONDUCTS_ELECTRICITY
|
|
slot_flags = ITEM_SLOT_NECK
|
|
custom_materials = list(/datum/material/iron =SMALL_MATERIAL_AMOUNT*0.5, /datum/material/glass = SMALL_MATERIAL_AMOUNT*1.5)
|
|
custom_price = PAYCHECK_CREW * 2
|
|
|
|
/// Cooldown before we can take another picture.
|
|
var/cooldown = 6.4 SECONDS
|
|
/// Whether we are currently ready to take a picture.
|
|
var/on = TRUE
|
|
/// Whether we are still processing an image.
|
|
var/blending = FALSE
|
|
/// Our icon_state when ready to take a picture.
|
|
var/state_on = "camera"
|
|
/// Our icon_state when not ready to take a picture.
|
|
var/state_off = "camera_off"
|
|
|
|
/// The maximum amount of pictures we can take before needing new film.
|
|
var/pictures_max = 10
|
|
/// The amount of pictures we can still take before needing new film.
|
|
var/pictures_left = 10
|
|
/// Currently inserted holorecord disk.
|
|
var/obj/item/disk/holodisk/disk
|
|
|
|
/// Whether we flash upon taking a picture.
|
|
var/flash_enabled = TRUE
|
|
/// Whether we silence our picture taking and zoom adjusting sounds.
|
|
var/silent = FALSE
|
|
/// To what degree ghosts are visible in our pictures.
|
|
var/see_ghosts = CAMERA_NO_GHOSTS //for the spoop of it
|
|
/// Whether the camera should print pictures immediately when a picture is taken.
|
|
var/print_picture_on_snap = TRUE
|
|
/// Whether we allow setting picture label/desc/scribble when a picture is taken.
|
|
var/can_customise = TRUE
|
|
/// Picture name we default to when none is set manually.
|
|
var/default_picture_name
|
|
|
|
var/picture_size_x = 2
|
|
var/picture_size_y = 2
|
|
var/picture_size_x_min = 1
|
|
var/picture_size_y_min = 1
|
|
var/picture_size_x_max = 4
|
|
var/picture_size_y_max = 4
|
|
|
|
/obj/item/camera/Initialize(mapload)
|
|
. = ..()
|
|
AddComponent(/datum/component/shell, list(new /obj/item/circuit_component/camera, new /obj/item/circuit_component/remotecam/polaroid), SHELL_CAPACITY_SMALL)
|
|
register_context()
|
|
|
|
/obj/item/camera/examine(mob/user)
|
|
. = ..()
|
|
. += span_notice("It has [pictures_left] photos left.")
|
|
. += span_notice("Alt-click to change its focusing, allowing you to set how big of an area it will capture.")
|
|
|
|
if(isnull(disk))
|
|
. += span_notice("It has a slot for a holorecord disk.")
|
|
else
|
|
. += span_notice("It has \an [disk.name] inserted.")
|
|
|
|
/obj/item/camera/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
|
|
context[SCREENTIP_CONTEXT_ALT_LMB] = "Adjust Zoom"
|
|
|
|
if(istype(held_item, /obj/item/camera_film))
|
|
context[SCREENTIP_CONTEXT_LMB] = "Insert Film"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
if(istype(held_item, /obj/item/disk/holodisk))
|
|
context[SCREENTIP_CONTEXT_LMB] = disk ? "Swap Disks" : "Insert Disk"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
if((isnull(held_item) || (held_item == src)) && disk)
|
|
context[SCREENTIP_CONTEXT_LMB] = "Eject Disk"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
/obj/item/camera/proc/adjust_zoom(mob/user)
|
|
if(loc != user)
|
|
to_chat(user, span_warning("You must be holding the camera to continue!"))
|
|
return FALSE
|
|
var/desired_x = tgui_input_number(user, "How wide do you want the camera to shoot?", "Zoom", picture_size_x, picture_size_x_max, picture_size_x_min)
|
|
if(!desired_x || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH) || loc != user)
|
|
return FALSE
|
|
var/desired_y = tgui_input_number(user, "How high do you want the camera to shoot", "Zoom", picture_size_y, picture_size_y_max, picture_size_y_min)
|
|
if(!desired_y || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH) || loc != user)
|
|
return FALSE
|
|
picture_size_x = min(clamp(desired_x, picture_size_x_min, picture_size_x_max), CAMERA_PICTURE_SIZE_HARD_LIMIT)
|
|
picture_size_y = min(clamp(desired_y, picture_size_y_min, picture_size_y_max), CAMERA_PICTURE_SIZE_HARD_LIMIT)
|
|
return TRUE
|
|
|
|
/obj/item/camera/Exited(atom/movable/gone, direction)
|
|
. = ..()
|
|
if(gone == disk)
|
|
disk = null
|
|
|
|
/obj/item/camera/click_alt(mob/user)
|
|
if(!adjust_zoom(user))
|
|
return CLICK_ACTION_BLOCKING
|
|
if(silent) // Don't out your silent cameras
|
|
user.playsound_local(get_turf(src), 'sound/machines/click.ogg', 50, TRUE)
|
|
else
|
|
playsound(src, 'sound/machines/click.ogg', 50, TRUE)
|
|
return CLICK_ACTION_SUCCESS
|
|
|
|
/obj/item/camera/attack_self(mob/user)
|
|
if(isnull(disk))
|
|
return
|
|
playsound(src, 'sound/machines/card_slide.ogg', 50)
|
|
user.put_in_hands(disk)
|
|
disk = null
|
|
|
|
/obj/item/camera/attack(mob/living/carbon/human/M, mob/user)
|
|
return
|
|
|
|
/obj/item/camera/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
|
|
if(istype(tool, /obj/item/camera_film))
|
|
return camera_film_act(user, tool)
|
|
if(istype(tool, /obj/item/disk/holodisk))
|
|
return holodisk_act(user, tool)
|
|
|
|
/obj/item/camera/proc/camera_film_act(mob/living/user, obj/item/camera_film/new_film)
|
|
if(pictures_left)
|
|
balloon_alert(user, "isn't empty!")
|
|
return ITEM_INTERACT_BLOCKING
|
|
if(!user.temporarilyRemoveItemFromInventory(new_film))
|
|
return ITEM_INTERACT_BLOCKING
|
|
playsound(src, 'sound/machines/click.ogg', 50, TRUE)
|
|
qdel(new_film)
|
|
pictures_left = pictures_max
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
/obj/item/camera/proc/holodisk_act(mob/living/user, obj/item/disk/holodisk/new_disk)
|
|
if(!user.transferItemToLoc(new_disk, src))
|
|
balloon_alert(user, "stuck in hand!")
|
|
return TRUE
|
|
if(disk)
|
|
user.put_in_hands(disk)
|
|
balloon_alert(user, "disks swapped!")
|
|
else
|
|
balloon_alert(user, "disk inserted!")
|
|
playsound(src, 'sound/machines/card_slide.ogg', 50)
|
|
disk = new_disk
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
/// Attempt to take an image, optionally given a user.
|
|
/obj/item/camera/proc/attempt_picture(atom/target, atom/user)
|
|
if(!can_target(target, user))
|
|
return FALSE
|
|
if(!photo_taken(target, user))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Check whether we can take a picture of the target, optionally given a user.
|
|
/obj/item/camera/proc/can_target(atom/target, atom/user)
|
|
if(!on || blending || !pictures_left)
|
|
return FALSE
|
|
var/turf/target_turf = get_turf(target)
|
|
if(isnull(target_turf))
|
|
return FALSE
|
|
if(isAI(user))
|
|
return can_ai_target(target_turf)
|
|
if(ismob(user))
|
|
return can_mob_target(target_turf, user)
|
|
|
|
if(isliving(loc))
|
|
if(!(target_turf in view(world.view, loc)))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
// User is an atom or null
|
|
if(!(target_turf in view(world.view, user || src)))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Check whether an AI could take a picture of the target turf.
|
|
/obj/item/camera/proc/can_ai_target(turf/target_turf)
|
|
return SScameras.is_visible_by_cameras(target_turf)
|
|
|
|
/// Check whether a mob could take a picture of the target turf.
|
|
/obj/item/camera/proc/can_mob_target(turf/target_turf, mob/user)
|
|
return (target_turf in get_hear_turfs(user.client?.view || world.view, user.client?.eye || user))
|
|
|
|
/obj/item/camera/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
|
|
// Always skip on storage and tables
|
|
if(HAS_TRAIT(interacting_with, TRAIT_COMBAT_MODE_SKIP_INTERACTION))
|
|
return NONE
|
|
|
|
return ranged_interact_with_atom(interacting_with, user, modifiers)
|
|
|
|
/obj/item/camera/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
|
|
if(disk)
|
|
if(!ismob(interacting_with))
|
|
to_chat(user, span_warning("Invalid holodisk target."))
|
|
return ITEM_INTERACT_BLOCKING
|
|
if(disk.record)
|
|
QDEL_NULL(disk.record)
|
|
|
|
disk.record = new
|
|
var/mob/recorded_mob = interacting_with
|
|
disk.record.caller_name = recorded_mob.name
|
|
disk.record.set_caller_image(recorded_mob)
|
|
|
|
return attempt_picture(interacting_with, user) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
|
|
|
|
/obj/item/camera/proc/photo_taken(atom/target, mob/user)
|
|
|
|
on = FALSE
|
|
addtimer(CALLBACK(src, PROC_REF(cooldown)), cooldown)
|
|
|
|
icon_state = state_off
|
|
|
|
INVOKE_ASYNC(src, PROC_REF(captureimage), target, user, picture_size_x - 1, picture_size_y - 1)
|
|
return TRUE
|
|
|
|
/obj/item/camera/proc/cooldown()
|
|
UNTIL(!blending)
|
|
icon_state = state_on
|
|
on = TRUE
|
|
|
|
/obj/item/camera/proc/show_picture(mob/user, datum/picture/selection)
|
|
var/obj/item/photo/P = new(src, selection)
|
|
P.show(user)
|
|
to_chat(user, P.desc)
|
|
qdel(P)
|
|
|
|
/obj/item/camera/proc/captureimage(atom/target, mob/user, size_x = 1, size_y = 1)
|
|
if(flash_enabled)
|
|
set_light_on(TRUE)
|
|
addtimer(CALLBACK(src, PROC_REF(flash_end)), FLASH_LIGHT_DURATION, TIMER_OVERRIDE|TIMER_UNIQUE)
|
|
blending = TRUE
|
|
var/turf/target_turf = get_turf(target)
|
|
if(!isturf(target_turf))
|
|
blending = FALSE
|
|
return FALSE
|
|
size_x = clamp(size_x, 0, CAMERA_PICTURE_SIZE_HARD_LIMIT)
|
|
size_y = clamp(size_y, 0, CAMERA_PICTURE_SIZE_HARD_LIMIT)
|
|
var/list/desc = list("This is a photo of an area of [size_x+1] meters by [size_y+1] meters.")
|
|
var/list/mobs_spotted = list()
|
|
var/list/dead_spotted = list()
|
|
var/list/viewlist = getviewsize(user?.client?.view || world.view)
|
|
var/view_range = max(viewlist[1], viewlist[2]) + max(size_x, size_y)
|
|
var/viewer = user?.client?.eye || user || target // not sure why target is a fallback
|
|
var/list/seen = get_hear_turfs(view_range, viewer)
|
|
var/list/turfs = list()
|
|
var/list/mobs = list()
|
|
var/blueprints = FALSE
|
|
var/clone_area = SSmapping.request_turf_block_reservation(size_x * 2 + 1, size_y * 2 + 1, 1)
|
|
///list of human names taken on picture
|
|
var/list/names = list()
|
|
var/cameranet_user = isAI(user) || istype(viewer, /mob/eye/camera)
|
|
|
|
var/width = size_x * 2 + 1
|
|
var/height = size_y * 2 + 1
|
|
for(var/turf/seen_placeholder as anything in CORNER_BLOCK_OFFSET(target_turf, width, height, -size_x, -size_y))
|
|
if(isnull(seen_placeholder))
|
|
continue
|
|
|
|
if(cameranet_user && !SScameras.is_visible_by_cameras(seen_placeholder))
|
|
continue
|
|
if(!cameranet_user && !(seen_placeholder in seen))
|
|
continue
|
|
|
|
//Multi-z photography
|
|
var/turf/target_placeholder = seen_placeholder
|
|
while(!isnull(target_placeholder))
|
|
turfs += target_placeholder
|
|
for(var/mob/mob_there in target_placeholder)
|
|
mobs += mob_there
|
|
if(locate(/obj/item/blueprints) in target_placeholder)
|
|
blueprints = TRUE
|
|
|
|
if(isopenspaceturf(target_placeholder) || istype(target_placeholder, /turf/open/floor/glass))
|
|
target_placeholder = GET_TURF_BELOW(target_placeholder)
|
|
else
|
|
break
|
|
|
|
// do this before picture is taken so we can reveal revenants for the photo
|
|
steal_souls(mobs)
|
|
|
|
for(var/mob/mob as anything in mobs)
|
|
mobs_spotted += mob
|
|
if(mob.stat == DEAD)
|
|
dead_spotted += mob
|
|
var/info = mob.get_photo_description(src)
|
|
if(!isnull(info))
|
|
desc += info
|
|
|
|
var/psize_x = (size_x * 2 + 1) * ICON_SIZE_X
|
|
var/psize_y = (size_y * 2 + 1) * ICON_SIZE_Y
|
|
var/icon/get_icon = camera_get_icon(turfs, target_turf, psize_x, psize_y, clone_area, size_x, size_y, (size_x * 2 + 1), (size_y * 2 + 1))
|
|
qdel(clone_area)
|
|
get_icon.Blend("#000", ICON_UNDERLAY)
|
|
for(var/mob/living/carbon/human/person in mobs)
|
|
if(person.obscured_slots & HIDEFACE)
|
|
continue
|
|
names += "[person.name]"
|
|
|
|
var/datum/picture/picture = new("picture", desc.Join("<br>"), mobs_spotted, dead_spotted, names, get_icon, null, psize_x, psize_y, blueprints, can_see_ghosts = see_ghosts)
|
|
after_picture(user, picture)
|
|
SEND_SIGNAL(src, COMSIG_CAMERA_IMAGE_CAPTURED, target, user, picture)
|
|
blending = FALSE
|
|
return picture
|
|
|
|
/obj/item/camera/proc/flash_end()
|
|
set_light_on(FALSE)
|
|
|
|
/obj/item/camera/proc/steal_souls(list/victims)
|
|
return
|
|
|
|
/obj/item/camera/proc/after_picture(mob/user, datum/picture/picture)
|
|
if(!silent)
|
|
playsound(loc, SFX_POLAROID, 75, TRUE, -3)
|
|
|
|
if(print_picture_on_snap)
|
|
printpicture(user, picture)
|
|
|
|
|
|
/obj/item/camera/proc/printpicture(mob/user, datum/picture/picture) //Normal camera proc for creating photos
|
|
pictures_left--
|
|
var/obj/item/photo/new_photo = new(get_turf(src), picture)
|
|
if(user)
|
|
if(in_range(new_photo, user) && user.put_in_hands(new_photo)) //needed because of TK
|
|
to_chat(user, span_notice("[pictures_left] photos left."))
|
|
|
|
var/name_customized = FALSE
|
|
if(can_customise)
|
|
var/customise = user.is_holding(new_photo) && tgui_alert(user, "Do you want to customize the photo?", "Customization", list("Yes", "No"))
|
|
if(customise == "Yes")
|
|
var/name1 = user.is_holding(new_photo) && tgui_input_text(user, "Set a name for this photo, or leave blank.", "Name", max_length = 32)
|
|
var/desc1 = user.is_holding(new_photo) && tgui_input_text(user, "Set a description to add to photo, or leave blank.", "Description", max_length = 128)
|
|
var/caption = user.is_holding(new_photo) && tgui_input_text(user, "Set a caption for this photo, or leave blank.", "Caption", max_length = 256)
|
|
if(name1)
|
|
picture.picture_name = name1
|
|
name_customized = TRUE
|
|
if(desc1)
|
|
picture.picture_desc = "[desc1] - [picture.picture_desc]"
|
|
if(caption)
|
|
picture.caption = caption
|
|
if(!name_customized && default_picture_name)
|
|
picture.picture_name = default_picture_name
|
|
|
|
else if(isliving(loc))
|
|
var/mob/living/holder = loc
|
|
if(holder.put_in_hands(new_photo))
|
|
to_chat(holder, span_notice("[pictures_left] photos left."))
|
|
|
|
new_photo.set_picture(picture, TRUE, TRUE)
|
|
if(CONFIG_GET(flag/picture_logging_camera))
|
|
picture.log_to_file()
|
|
|
|
/obj/item/circuit_component/camera
|
|
display_name = "Camera"
|
|
desc = "A polaroid camera that takes pictures when triggered. The picture coordinate ports are relative to the position of the camera."
|
|
circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
|
|
|
|
/// The atom that was photographed from either user click or trigger input.
|
|
var/datum/port/output/photographed_atom
|
|
/// The item that was added/removed.
|
|
var/datum/port/output/picture_taken
|
|
/// If set, the trigger input will target this atom.
|
|
var/datum/port/input/picture_target
|
|
/// If the above is unset, these coordinates will be used.
|
|
var/datum/port/input/picture_coord_x
|
|
var/datum/port/input/picture_coord_y
|
|
/// Adjusts the picture_size_x variable of the camera.
|
|
var/datum/port/input/adjust_size_x
|
|
/// Idem but for picture_size_y.
|
|
var/datum/port/input/adjust_size_y
|
|
|
|
/// The camera this circut is attached to.
|
|
var/obj/item/camera/camera
|
|
|
|
/obj/item/circuit_component/camera/populate_ports()
|
|
picture_taken = add_output_port("Picture Taken", PORT_TYPE_SIGNAL)
|
|
photographed_atom = add_output_port("Photographed Entity", PORT_TYPE_ATOM)
|
|
|
|
picture_target = add_input_port("Picture Target", PORT_TYPE_ATOM)
|
|
picture_coord_x = add_input_port("Picture Coordinate X", PORT_TYPE_NUMBER)
|
|
picture_coord_y = add_input_port("Picture Coordinate Y", PORT_TYPE_NUMBER)
|
|
adjust_size_x = add_input_port("Picture Size X", PORT_TYPE_NUMBER, trigger = PROC_REF(sanitize_picture_size))
|
|
adjust_size_y = add_input_port("Picture Size Y", PORT_TYPE_NUMBER, trigger = PROC_REF(sanitize_picture_size))
|
|
|
|
/obj/item/circuit_component/camera/register_shell(atom/movable/shell)
|
|
. = ..()
|
|
camera = shell
|
|
RegisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
|
|
|
|
/obj/item/circuit_component/camera/unregister_shell(atom/movable/shell)
|
|
UnregisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED)
|
|
camera = null
|
|
return ..()
|
|
|
|
/obj/item/circuit_component/camera/proc/sanitize_picture_size()
|
|
camera.picture_size_x = clamp(adjust_size_x.value, camera.picture_size_x_min, camera.picture_size_x_max)
|
|
camera.picture_size_y = clamp(adjust_size_y.value, camera.picture_size_y_min, camera.picture_size_y_max)
|
|
|
|
/obj/item/circuit_component/camera/proc/on_image_captured(obj/item/camera/source, atom/target, mob/user)
|
|
SIGNAL_HANDLER
|
|
photographed_atom.set_output(target)
|
|
picture_taken.set_output(COMPONENT_SIGNAL)
|
|
|
|
/obj/item/circuit_component/camera/input_received(datum/port/input/port)
|
|
var/atom/target = picture_target.value
|
|
if(!target)
|
|
var/turf/our_turf = get_location()
|
|
target = locate(our_turf.x + picture_coord_x.value, our_turf.y + picture_coord_y.value, our_turf.z)
|
|
if(!target)
|
|
return
|
|
INVOKE_ASYNC(camera, TYPE_PROC_REF(/obj/item/camera, attempt_picture), target)
|
|
|
|
#undef CAMERA_PICTURE_SIZE_HARD_LIMIT
|