General maintenance for hand held camera (#95295)

## About The Pull Request
- Merged a lot of procs into procs `attempt_picture()` and
`interact_with_atom()` reducing proc overhead
- Taking pictures will now always use the cameras internal
`picture_size_x` & `picture_size_y`. It no longer accepts variable sizes
as parameters
- All camera operations are asynchronous and respects checks on if the
target can be captured on camera or not.
- A bunch of other code rearrangement that makes readability easier
- Fixes the following camera bugs. They fall under the same category
  - Fixes #95286
  - Fixes #95256

## Changelog
🆑
fix: camera devices work again, camera flash turns on in most instances
refactor: camera code has been refactored. Report bugs on github
/🆑

---------

Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
This commit is contained in:
SyncIt21
2026-04-04 22:11:39 +05:30
committed by GitHub
parent 11c7893e8e
commit de1f01fe76
16 changed files with 555 additions and 430 deletions
@@ -5,25 +5,26 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
for(var/obj/item/canvas/canvas_type as anything in typesof(/obj/item/canvas))
var/width = canvas_type::width
var/height = canvas_type::height
LAZYADDASSOC(print_types, "[width]x[height]", list("[canvas_type]" = list(
LAZYADDASSOC(print_types, "[width]x[height]", list(
"displayText" = "Canvas ([width]x[height])",
"typepath" = canvas_type,
"width" = width,
"height" = height,
"check_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(check_can_print_canvas)),
"prepare_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(prepare_canvas_from_file)),
)))
for(var/size in 1 to /obj/item/camera::picture_size_x_max)
var/width = ICON_SIZE_X*(size*2-1)
var/height = ICON_SIZE_Y*(size*2-1)
LAZYADDASSOC(print_types, "[width]x[height]", list("[/obj/item/photo]" = list(
"displayText" = "Photo Paper ([size*2-1]m focal length)",
))
for(var/size in 1 to CAMERA_PICTURE_SIZE_HARD_LIMIT)
var/meters = APERTURE_TO_METERS(size)
var/width = ICON_SIZE_X * meters
var/height = ICON_SIZE_Y * meters
LAZYADDASSOC(print_types, "[width]x[height]", list(
"displayText" = "Photo Paper ([meters]m focal length)",
"typepath" = /obj/item/photo,
"width" = width,
"height" = height,
"check_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(check_can_print_photo)),
"prepare_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(prepare_photo_from_file)),
)))
))
return print_types
/proc/check_can_print_canvas(_typepath, _image_file, obj/item/modular_computer/computer, mob/user)
@@ -177,11 +178,8 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
try_print(picture, params["width"], params["height"], params["offsetX"], params["offsetY"], params["typepath"], usr)
/datum/computer_file/program/filemanager/proc/try_print(datum/computer_file/image/picture, width, height, offset_x, offset_y, typepath, mob/user)
var/list/print_types_for_dimensions = GLOB.print_types["[width]x[height]"]
if(!length(print_types_for_dimensions))
return
var/list/print_type = print_types_for_dimensions[typepath]
if(!print_type)
var/list/print_type = GLOB.print_types["[width]x[height]"]
if(!length(print_type))
return
var/image_width = picture.stored_icon.Width()
var/image_height = picture.stored_icon.Height()
@@ -203,10 +201,16 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
/datum/computer_file/program/filemanager/ui_static_data(mob/user)
var/list/print_types = list()
for(var/dimensions in GLOB.print_types)
var/list/types_for_dimensions = GLOB.print_types[dimensions]
for(var/print_typepath in types_for_dimensions)
print_types += list(types_for_dimensions[print_typepath])
for(var/dimensions, types_for_dimensions in GLOB.print_types)
var/list/typepath = types_for_dimensions["typepath"]
if(ispath(typepath, /obj/item/canvas) && !(computer.hardware_flag & PROGRAM_CONSOLE))
continue
print_types += list(list(
displayText = types_for_dimensions["displayText"],
typepath = "[typepath]",
width = types_for_dimensions["width"],
height = types_for_dimensions["height"]
))
return list("printTypes" = print_types)
/datum/computer_file/program/filemanager/ui_data(mob/user)
@@ -14,8 +14,8 @@
var/obj/item/camera/app/internal_camera
/// Latest picture taken by the app.
var/datum/picture/internal_picture
/// A mutable_appearance of the latest picture, for getting an appeance reference for the UI.
var/mutable_appearance/picture_appearance
/// base64 of the current taken picture
var/internal_picture_icon
/// How many pictures were taken already, used for the camera's TGUI photo display
var/picture_number = 1
/// Can we edit the metadata of the latest picture?
@@ -29,24 +29,20 @@
// Special type of camera for this exact usecase to prevent harddels
/obj/item/camera/app
name = "internal camera"
desc = "Specialized internal camera protected from the hellish depths of SSWardrobe. \
Yell at coders if you somehow manage to see this"
print_picture_on_snap = FALSE
cooldown = 1 SECONDS
light_range = 3
/datum/computer_file/program/maintenance/camera/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user)
. = ..()
internal_camera = new(computer)
internal_camera.print_picture_on_snap = FALSE
picture_appearance = new()
RegisterSignal(internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
/datum/computer_file/program/maintenance/camera/Destroy()
QDEL_NULL(internal_camera)
internal_picture = null
QDEL_NULL(picture_appearance)
QDEL_NULL(internal_picture)
return ..()
/datum/computer_file/program/maintenance/camera/tap(atom/tapped_atom, mob/living/user, list/modifiers)
. = ..()
take_picture(user, get_turf(tapped_atom))
@@ -57,7 +53,7 @@
/datum/computer_file/program/maintenance/camera/kill_program(mob/user)
. = ..()
UnregisterSignal(computer, COMSIG_RANGED_ITEM_INTERACTING_WITH_ATOM_SECONDARY)
internal_picture = null
QDEL_NULL(internal_picture)
/datum/computer_file/program/maintenance/camera/background_program(mob/user)
. = ..()
@@ -68,47 +64,21 @@
take_picture(user, get_turf(target))
/datum/computer_file/program/maintenance/camera/proc/take_picture(mob/user, turf/target)
if(internal_camera.blending)
user.balloon_alert(user, "still blending!")
return
var/spooky_camera = locate(/datum/computer_file/program/maintenance/spectre_meter) in computer.stored_files
internal_camera.see_ghosts = spooky_camera ? CAMERA_SEE_GHOSTS_BASIC : CAMERA_NO_GHOSTS
INVOKE_ASYNC(internal_camera, TYPE_PROC_REF(/obj/item/camera, captureimage), target, user, internal_camera.picture_size_x - 1, internal_camera.picture_size_y - 1)
internal_camera.see_ghosts = (locate(/datum/computer_file/program/maintenance/spectre_meter) in computer.stored_files) ? CAMERA_SEE_GHOSTS_BASIC : CAMERA_NO_GHOSTS
internal_camera.attempt_picture(target, user)
/datum/computer_file/program/maintenance/camera/proc/on_image_captured(cam, target, user, datum/picture/picture)
SIGNAL_HANDLER
QDEL_NULL(internal_picture)
internal_picture = picture
picture_appearance.icon = internal_picture.picture_image
internal_picture_icon = icon2base64(internal_picture.picture_image)
current_picture_name = null
current_picture_desc = null
current_picture_caption = null
can_edit_metadata = TRUE
picture_number++
/datum/computer_file/program/maintenance/camera/proc/save_picture(mob/user)
var/datum/computer_file/image/photo_file = new(
internal_picture.picture_image,
display_name = internal_picture.picture_name || "photo[picture_number]",
source_photo_or_painting = internal_picture
)
if(computer.store_file(photo_file, user))
return FALSE
commit_metadata()
return TRUE
/datum/computer_file/program/maintenance/camera/proc/print_picture(mob/user)
if(computer.stored_paper < PHOTO_PAPER_COST)
return
commit_metadata()
var/obj/item/photo/new_photo = new(computer.physical.drop_location())
new_photo.set_picture(internal_picture, TRUE, TRUE)
user?.put_in_hands(new_photo)
playsound(computer.physical, 'sound/machines/printer.ogg', 100, TRUE)
computer.stored_paper--
computer.visible_message(span_notice("\The [computer] prints out a paper."))
/datum/computer_file/program/maintenance/camera/proc/commit_metadata()
if(can_edit_metadata)
internal_picture.picture_name = current_picture_name
@@ -117,105 +87,90 @@
can_edit_metadata = FALSE
/datum/computer_file/program/maintenance/camera/ui_static_data(mob/user)
return list("maxNameLength" = 32, "maxDescLength" = 128, "maxCaptionLength" = 256, "printCost" = 1)
return list(
"maxNameLength" = 32,
"maxDescLength" = 128,
"maxCaptionLength" = 256,
"printCost" = 1,
"minSize" = 2,
"maxSize" = CAMERA_PICTURE_SIZE_HARD_LIMIT
)
/datum/computer_file/program/maintenance/camera/ui_data(mob/user)
var/list/data = list()
if(!isnull(internal_picture))
data["photo"] = REF(picture_appearance.appearance)
data["photo"] = internal_picture_icon
data["canEditMetadata"] = can_edit_metadata
data["name"] = current_picture_name
data["desc"] = current_picture_desc
data["caption"] = current_picture_caption
data["storedPaper"] = computer.stored_paper
data["size"] = internal_camera.picture_size_x
data["minSize"] = internal_camera.picture_size_x_min
data["maxSize"] = min(internal_camera.picture_size_x_max, CAMERA_PICTURE_SIZE_HARD_LIMIT)
data["width"] = internal_camera.picture_size_x
data["height"] = internal_camera.picture_size_y
data["size"] = "[APERTURE_TO_METERS(internal_camera.picture_size_x)]x[APERTURE_TO_METERS(internal_camera.picture_size_y)]"
return data
/datum/computer_file/program/maintenance/camera/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("adjustSize")
var/new_size = round(params["value"], 1)
if(!ISINRANGE(new_size, internal_camera.picture_size_x_min, min(CAMERA_PICTURE_SIZE_HARD_LIMIT, internal_camera.picture_size_x_max)))
if("adjustWidth")
var/new_size = text2num(params["value"])
if(!new_size)
return
internal_camera.picture_size_x = new_size
internal_camera.picture_size_y = new_size
return internal_camera.adjust_zoom(desired_x = new_size)
if("adjustHeight")
var/new_size = text2num(params["value"])
if(!new_size)
return
return internal_camera.adjust_zoom(desired_y = new_size)
if("setName")
if(!(internal_picture && can_edit_metadata))
return
return FALSE
current_picture_name = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(32))
return TRUE
if("setDesc")
if(!(internal_picture && can_edit_metadata))
return
current_picture_desc = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(128))
return TRUE
if("setCaption")
if(!(internal_picture && can_edit_metadata))
return
current_picture_caption = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(256))
return TRUE
if("savePhoto")
if(!internal_picture)
return
save_picture(ui.user)
var/datum/computer_file/image/photo_file = new(
internal_picture.picture_image,
display_name = internal_picture.picture_name || "photo[picture_number]",
source_photo_or_painting = internal_picture
)
if(computer.store_file(photo_file, ui.user))
return FALSE
commit_metadata()
return TRUE
if("printPhoto")
if(!internal_picture)
return
print_picture(ui.user)
return TRUE
/obj/item/circuit_component/mod_program/camera
associated_program = /datum/computer_file/program/maintenance/camera
circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
///A target to take a picture of.
var/datum/port/input/picture_target
///The size of the photo to take.
var/datum/port/input/picture_size
///The photographed target
var/datum/port/output/photographed
/**
* Pinged when the image has been captured.
* I'm not using the default trigger output here because the process is asynced,
* even though I'm mostly sure it only sleeps if there's a set user.
*/
var/datum/port/output/photo_taken
/obj/item/circuit_component/mod_program/camera/populate_ports()
. = ..()
picture_target = add_input_port("Picture Target", PORT_TYPE_ATOM)
picture_size = add_input_port("Picture Size", PORT_TYPE_NUMBER)
photographed = add_output_port("Photographed Entity", PORT_TYPE_ATOM)
photo_taken = add_output_port("Photo Taken", PORT_TYPE_SIGNAL)
/obj/item/circuit_component/mod_program/camera/register_shell(atom/movable/shell)
. = ..()
var/datum/computer_file/program/maintenance/camera/cam = associated_program
RegisterSignal(cam.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
/obj/item/circuit_component/mod_program/camera/unregister_shell()
var/datum/computer_file/program/maintenance/camera/cam = associated_program
UnregisterSignal(cam.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED)
return ..()
/obj/item/circuit_component/mod_program/camera/input_received(datum/port/input/port)
if(!COMPONENT_TRIGGERED_BY(port, trigger_input))
return
var/atom/target = picture_target.value
if(!target)
var/turf/our_turf = get_location()
target = locate(our_turf.x, our_turf.y, our_turf.z)
if(!target)
return
var/datum/computer_file/program/maintenance/camera/cam = associated_program
if(!cam.internal_camera.can_target(target))
return
var/pic_size = clamp(round(picture_size.value), 1, cam.internal_camera.picture_size_x_max)-1
INVOKE_ASYNC(cam.internal_camera, TYPE_PROC_REF(/obj/item/camera, captureimage), target, null, pic_size, pic_size)
/obj/item/circuit_component/mod_program/camera/proc/on_image_captured(obj/item/camera/source, atom/target, mob/user)
SIGNAL_HANDLER
photographed.set_output(target)
photo_taken.set_output(COMPONENT_SIGNAL)
return FALSE
if(computer.stored_paper < PHOTO_PAPER_COST)
return FALSE
commit_metadata()
var/obj/item/photo/new_photo = new(computer.physical.drop_location())
new_photo.set_picture(internal_picture, TRUE, TRUE)
ui.user.put_in_hands(new_photo)
playsound(computer.physical, 'sound/machines/printer.ogg', 100, TRUE)
computer.stored_paper--
computer.visible_message(span_notice("\The [computer] prints out a paper."))
return TRUE
@@ -330,7 +330,7 @@ GLOBAL_LIST_EMPTY(virtual_pets_list)
var/datum/action/cooldown/mob_cooldown/capture_photo/photo_ability = new(pet)
photo_ability.Grant(pet)
pet.ai_controller?.set_blackboard_key(BB_PHOTO_ABILITY, photo_ability)
RegisterSignal(photo_ability.ability_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_photo_captured))
RegisterSignal(photo_ability.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_photo_captured))
/datum/computer_file/program/virtual_pet/proc/announce_global_updates(message)
if(isnull(message))