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 12:41:39 -04:00
committed by GitHub
co-authored by ArcaneMusic
parent 11c7893e8e
commit de1f01fe76
16 changed files with 555 additions and 430 deletions
+5 -1
View File
@@ -1 +1,5 @@
#define CAMERA_PICTURE_SIZE_HARD_LIMIT 21
///Converts camera half dimension aperture into full meters
///We first do size = value - 1 which is the picture size & then 2 * size + 1 to give us meters
#define APERTURE_TO_METERS(value)(2 * value - 1)
///Max size of an photograph in square dimensions
#define CAMERA_PICTURE_SIZE_HARD_LIMIT 4
-1
View File
@@ -1,4 +1,3 @@
/// Helper define that can only be used in /obj/item/circuit_component/input_received()
#define COMPONENT_TRIGGERED_BY(trigger, port) (trigger.value && trigger == port)
/// Define to be placed at any proc that is triggered by a port.
@@ -19,28 +19,29 @@
overlay_icon_state = "bg_default_border"
cooldown_time = 30 SECONDS
///camera we use to take photos
var/obj/item/camera/ability_camera
var/obj/item/camera/internal_camera
/datum/action/cooldown/mob_cooldown/capture_photo/Grant(mob/grant_to)
. = ..()
if(isnull(owner))
return
ability_camera = new(owner)
ability_camera.print_picture_on_snap = FALSE
RegisterSignal(ability_camera, COMSIG_PREQDELETED, PROC_REF(on_camera_delete))
internal_camera = new(owner)
internal_camera.print_picture_on_snap = FALSE
internal_camera.cooldown = 1 SECONDS
RegisterSignal(internal_camera, COMSIG_PREQDELETED, PROC_REF(on_camera_delete))
/datum/action/cooldown/mob_cooldown/capture_photo/Activate(atom/target)
if(isnull(ability_camera))
if(isnull(internal_camera))
return FALSE
ability_camera.captureimage(target, owner)
internal_camera.attempt_picture(target, owner)
StartCooldown()
return TRUE
/datum/action/cooldown/mob_cooldown/capture_photo/proc/on_camera_delete(datum/source)
SIGNAL_HANDLER
UnregisterSignal(ability_camera, COMSIG_PREQDELETED)
ability_camera = null
UnregisterSignal(internal_camera, COMSIG_PREQDELETED)
internal_camera = null
/datum/action/cooldown/mob_cooldown/capture_photo/Destroy()
QDEL_NULL(ability_camera)
QDEL_NULL(internal_camera)
return ..()
@@ -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))
+251 -243
View File
@@ -3,11 +3,12 @@
icon = 'icons/obj/art/camera.dmi'
desc = "A polaroid camera."
icon_state = "camera"
base_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_system = NONE
light_range = 6
light_color = COLOR_WHITE
light_power = FLASH_LIGHT_POWER
@@ -24,52 +25,55 @@
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
var/see_ghosts = CAMERA_NO_GHOSTS
/// 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
///Width of the picture
var/picture_size_x = 2
///height of the picture
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
///Internal holder to apply camera light on
VAR_PRIVATE/atom/movable/light_holder
/// Special type of component so it does not intefer with the modular computer default lighting system if any
/datum/component/overlay_lighting/camera
dupe_mode = COMPONENT_DUPE_SOURCES
/obj/item/camera/Initialize(mapload)
. = ..()
//we do this so if this camera is used as an internal component, the flash will still be visible
if(flash_enabled)
var/atom/movable/parent = loc
light_holder = src
while(!(isnull(parent) || ismob(parent) || isturf(parent)))
light_holder = parent
parent = light_holder.loc
light_holder.AddComponentFrom(REF(src), /datum/component/overlay_lighting/camera, light_range, light_power, light_color, FALSE, TRUE, FALSE, TRUE)
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/Destroy(force)
if(light_holder)
light_holder.RemoveComponentSource(REF(src), /datum/component/overlay_lighting/camera)
light_holder = null
return ..()
/obj/item/camera/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
context[SCREENTIP_CONTEXT_ALT_LMB] = "Adjust Zoom"
@@ -88,187 +92,161 @@
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|ALLOW_PAI) || 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|ALLOW_PAI) || 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/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.")
. += span_notice("The present dimensions of the picture are [EXAMINE_HINT("[APERTURE_TO_METERS(picture_size_x)]x[APERTURE_TO_METERS(picture_size_y)]")]")
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/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
/**
* Adjusts the zoom of this camera
* Arguments
*
* * desired_x - the x zoom value to use
* * desired_y - the y zoom value to use
* * mob/user - the optional user who is taking the photo. Passing the mob will ask for input and ignore the above params
*/
/obj/item/camera/proc/adjust_zoom(desired_x = picture_size_x, desired_y = picture_size_y, mob/user)
SHOULD_NOT_OVERRIDE(TRUE)
/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)))
if(user)
if(loc != user)
to_chat(user, span_warning("You must be holding the camera to continue!"))
return FALSE
desired_x = tgui_input_number(user, "Set camera half width Aperture", "Zoom", picture_size_x, CAMERA_PICTURE_SIZE_HARD_LIMIT, 2)
if(!desired_x || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH|ALLOW_PAI) || loc != user)
return FALSE
desired_y = tgui_input_number(user, "Set camera half height Aperture", "Zoom", picture_size_y, CAMERA_PICTURE_SIZE_HARD_LIMIT, 2)
if(!desired_y || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH|ALLOW_PAI) || loc != user)
return FALSE
return TRUE
// User is an atom or null
if(!(target_turf in view(world.view, user || src)))
return FALSE
picture_size_x = clamp(desired_x, 2, CAMERA_PICTURE_SIZE_HARD_LIMIT)
picture_size_y = clamp(desired_y, 2, CAMERA_PICTURE_SIZE_HARD_LIMIT)
if(user)
to_chat(user, span_notice("The dimensions of the picture will be [EXAMINE_HINT("[APERTURE_TO_METERS(picture_size_x)]x[APERTURE_TO_METERS(picture_size_y)]")]"))
return TRUE
/// Resets flash to be used again
/obj/item/camera/proc/cooldown()
PRIVATE_PROC(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)
UNTIL(!blending)
icon_state = base_icon_state
on = TRUE
/// 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))
/// Turns the light/flash off
/obj/item/camera/proc/flash_end()
PRIVATE_PROC(TRUE)
/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
light_holder.set_light_on(FALSE)
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)
/**
* Turns the flash quickly on and off when picture is taken
* Arguments
*
* * atom/target - the target we are trying to take a photo of
* * mob/user - the optional user who is taking the photo
*/
/obj/item/camera/proc/on_flash(atom/target, mob/user)
PROTECTED_PROC(TRUE)
SHOULD_CALL_PARENT(TRUE)
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)
icon_state = "[base_icon_state]_off"
if(flash_enabled)
set_light_on(TRUE)
light_holder.set_light_on(TRUE)
addtimer(CALLBACK(src, PROC_REF(flash_end)), FLASH_LIGHT_DURATION, TIMER_OVERRIDE|TIMER_UNIQUE)
blending = TRUE
/**
* Steal souls from all mobs captured in the image
* Arguments
*
* * list/victims - list of all mobs captured in the image
*/
/obj/item/camera/proc/steal_souls(list/mob/victims)
PROTECTED_PROC(TRUE)
return
/**
* Attempts to take an image of the target and all its surrounding tiles
* Arguments
*
* * atom/target - the target we are trying to take a photo of
* * mob/user - the optional user who is taking the photo
*/
/obj/item/camera/proc/attempt_picture(atom/target, mob/user)
SHOULD_NOT_OVERRIDE(TRUE)
if(!on)
if(user)
user.balloon_alert(user, "flash still charging!")
return
if(blending)
if(user)
user.balloon_alert(user, "image still blending!")
return
INVOKE_ASYNC(src, PROC_REF(capture_image), target, user)
/**
* Renders an image of the target and all its surrounding tiles
* Arguments passed from attempt_picture()
*/
/obj/item/camera/proc/capture_image(atom/target, mob/user)
PRIVATE_PROC(TRUE)
//Checking if we can target
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()
if(isnull(target_turf))
return
if(isAI(user) && !SScameras.is_visible_by_cameras(target_turf))
return
if(isliving(loc) && !(target_turf in view(world.view, loc)))
return
if(!(target_turf in view(world.view, user || src)))
return
//These vars will be reused later on
var/size_x = picture_size_x - 1
var/size_y = picture_size_y - 1
var/list/viewlist = getviewsize(user?.client?.view || world.view)
var/view_range = max(viewlist[1], viewlist[2]) + max(size_x, size_y)
var/viewer = get_turf(user?.client?.eye || user || target) // not sure why target is a fallback
var/list/seen = get_hear_turfs(view_range, viewer)
if(!(target_turf in seen))
return
//taking the actual picture
on_flash(target, user)
blending = TRUE
var/list/mobs_spotted = list()
var/list/dead_spotted = list()
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)
var/width = APERTURE_TO_METERS(picture_size_x)
var/height = APERTURE_TO_METERS(picture_size_y)
///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
var/datum/turf_reservation/clone_area = SSmapping.request_turf_block_reservation(width, height, 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))
@@ -291,6 +269,7 @@
// do this before picture is taken so we can reveal revenants for the photo
steal_souls(mobs)
var/list/desc = list("This is a photo of an area of [width] meters by [height] meters.")
for(var/mob/mob as anything in mobs)
mobs_spotted += mob
if(mob.stat == DEAD)
@@ -299,50 +278,64 @@
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)
var/icon/get_icon = camera_get_icon(turfs, target_turf, clone_area)
get_icon.Blend("#000", ICON_UNDERLAY)
qdel(clone_area)
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)
var/datum/picture/picture = new("picture", desc.Join("<br>"), mobs_spotted, dead_spotted, names, get_icon, null, width * ICON_SIZE_X, height * ICON_SIZE_X, 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
/**
* Action to take after the picture is taken
*
* Arguments
*
* * mob/user - the user who took the picture
* * datum/picture/picture - the picture taken
*/
/obj/item/camera/proc/after_picture(mob/user, datum/picture/picture)
PROTECTED_PROC(TRUE)
if(!silent)
playsound(loc, SFX_POLAROID, 75, TRUE, -3)
if(print_picture_on_snap)
printpicture(user, picture)
/**
* Print the picture tkane on film
*
* Arguments
*
* * mob/user - the user who took the picture
* * datum/picture/picture - the picture taken
*/
/obj/item/camera/proc/printpicture(mob/user, datum/picture/picture)
SHOULD_NOT_OVERRIDE(TRUE)
/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)
var/obj/item/photo/new_photo
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."))
if(!pictures_left)
to_chat(user, span_warning("No film left."))
return
new_photo = new(src, picture)
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"))
var/customise = 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)
var/name1 = tgui_input_text(user, "Set a name for this photo, or leave blank.", "Name", max_length = 32)
var/desc1 = tgui_input_text(user, "Set a description to add to photo, or leave blank.", "Description", max_length = 128)
var/caption = 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
@@ -355,69 +348,84 @@
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."))
if(!pictures_left)
to_chat(holder, span_warning("No film left."))
return
new_photo = new(get_turf(src), picture)
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
pictures_left--
/// 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
user.put_in_hands(new_photo)
/// The camera this circut is attached to.
var/obj/item/camera/camera
/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/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)
/obj/item/camera/attack(mob/living/carbon/human/M, mob/user)
return
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/camera/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(istype(tool, /obj/item/camera_film))
if(pictures_left)
balloon_alert(user, "isn't empty!")
return ITEM_INTERACT_BLOCKING
if(!user.temporarilyRemoveItemFromInventory(tool))
return ITEM_INTERACT_BLOCKING
playsound(src, 'sound/machines/click.ogg', 50, TRUE)
qdel(tool)
pictures_left = pictures_max
return ITEM_INTERACT_SUCCESS
/obj/item/circuit_component/camera/register_shell(atom/movable/shell)
. = ..()
camera = shell
RegisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
if(istype(tool, /obj/item/disk/holodisk))
if(!user.transferItemToLoc(tool, 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 = tool
return ITEM_INTERACT_SUCCESS
/obj/item/circuit_component/camera/unregister_shell(atom/movable/shell)
UnregisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED)
camera = null
return ..()
/obj/item/camera/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(HAS_TRAIT(interacting_with, TRAIT_COMBAT_MODE_SKIP_INTERACTION))
return NONE
return ranged_interact_with_atom(interacting_with, user, modifiers)
/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/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)
/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)
disk.record = new
var/mob/recorded_mob = interacting_with
disk.record.caller_name = recorded_mob.name
disk.record.set_caller_image(recorded_mob)
/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)
attempt_picture(interacting_with, user)
return ITEM_INTERACT_SUCCESS
/obj/item/camera/click_alt(mob/user)
if(!adjust_zoom(user = 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
@@ -14,7 +14,9 @@
#define PHYSICAL_POSITION(atom) ((atom.y * ICON_SIZE_Y) + (atom.pixel_y))
/obj/item/camera/proc/camera_get_icon(list/turfs, turf/center, psize_x = 96, psize_y = 96, datum/turf_reservation/clone_area, size_x, size_y, total_x, total_y)
/obj/item/camera/proc/camera_get_icon(list/turfs, turf/center, datum/turf_reservation/clone_area)
PRIVATE_PROC(TRUE)
var/list/atoms = list()
var/list/lighting = list()
var/skip_normal = FALSE
@@ -24,35 +26,34 @@
backdrop.blend_mode = BLEND_OVERLAY
backdrop.color = "#292319"
if(istype(clone_area) && total_x == clone_area.width && total_y == clone_area.height && size_x >= 0 && size_y > 0)
var/turf/bottom_left = clone_area.bottom_left_turfs[1]
var/cloned_center_x = round(bottom_left.x + ((total_x - 1) / 2))
var/cloned_center_y = round(bottom_left.y + ((total_y - 1) / 2))
for(var/t in turfs)
var/turf/T = t
var/offset_x = T.x - center.x
var/offset_y = T.y - center.y
var/turf/newT = locate(cloned_center_x + offset_x, cloned_center_y + offset_y, bottom_left.z)
if(!(newT in clone_area.reserved_turfs)) //sanity check so we don't overwrite other areas somehow
continue
atoms += new /obj/effect/appearance_clone(newT, T)
if(T.loc.icon_state)
atoms += new /obj/effect/appearance_clone(newT, T.loc)
if(T.lighting_object)
var/obj/effect/appearance_clone/lighting_overlay = new(newT)
lighting_overlay.appearance = T.lighting_object.current_underlay
lighting_overlay.underlays += backdrop
lighting_overlay.blend_mode = BLEND_MULTIPLY
lighting += lighting_overlay
for(var/atom/found_atom as anything in T.contents)
if(HAS_TRAIT(found_atom, TRAIT_INVISIBLE_TO_CAMERA))
if(see_ghosts)
atoms += new /obj/effect/appearance_clone(newT, found_atom)
else if(!found_atom.invisibility || (see_ghosts && isobserver(found_atom)))
var/turf/bottom_left = clone_area.bottom_left_turfs[1]
var/cloned_center_x = round(bottom_left.x + ((clone_area.width - 1) / 2))
var/cloned_center_y = round(bottom_left.y + ((clone_area.height - 1) / 2))
for(var/t in turfs)
var/turf/T = t
var/offset_x = T.x - center.x
var/offset_y = T.y - center.y
var/turf/newT = locate(cloned_center_x + offset_x, cloned_center_y + offset_y, bottom_left.z)
if(!(newT in clone_area.reserved_turfs)) //sanity check so we don't overwrite other areas somehow
continue
atoms += new /obj/effect/appearance_clone(newT, T)
if(T.loc.icon_state)
atoms += new /obj/effect/appearance_clone(newT, T.loc)
if(T.lighting_object)
var/obj/effect/appearance_clone/lighting_overlay = new(newT)
lighting_overlay.appearance = T.lighting_object.current_underlay
lighting_overlay.underlays += backdrop
lighting_overlay.blend_mode = BLEND_MULTIPLY
lighting += lighting_overlay
for(var/atom/found_atom as anything in T.contents)
if(HAS_TRAIT(found_atom, TRAIT_INVISIBLE_TO_CAMERA))
if(see_ghosts)
atoms += new /obj/effect/appearance_clone(newT, found_atom)
skip_normal = TRUE
wipe_atoms = TRUE
center = locate(cloned_center_x, cloned_center_y, bottom_left.z)
else if(!found_atom.invisibility || (see_ghosts && isobserver(found_atom)))
atoms += new /obj/effect/appearance_clone(newT, found_atom)
skip_normal = TRUE
wipe_atoms = TRUE
center = locate(cloned_center_x, cloned_center_y, bottom_left.z)
if(!skip_normal)
for(var/i in turfs)
@@ -71,8 +72,11 @@
atoms += A
CHECK_TICK
var/psize_x = clone_area.width * ICON_SIZE_X
var/psize_y = clone_area.height * ICON_SIZE_Y
var/icon/res = icon('icons/blanks/96x96.dmi', "nothing")
res.Scale(psize_x, psize_y)
atoms += lighting
var/list/sorted = list()
@@ -2,6 +2,7 @@
/obj/item/camera/siliconcam
name = "silicon photo camera"
resistance_flags = INDESTRUCTIBLE
cooldown = 2 SECONDS
/// List of all pictures taken by this camera.
var/list/datum/picture/stored = list()
@@ -15,7 +16,7 @@
if(!can_take_picture(clicker))
return
clicker.face_atom(clicked_on)
INVOKE_ASYNC(src, PROC_REF(captureimage), clicked_on, clicker, picture_size_x - 1, picture_size_y - 1)
attempt_picture(clicked_on, clicker)
toggle_camera_mode(clicker, sound = FALSE)
/// Toggles the camera mode on or off.
@@ -56,7 +57,10 @@
/obj/item/camera/siliconcam/proc/viewpictures(mob/user)
var/datum/picture/selection = selectpicture(user)
if(istype(selection))
show_picture(user, selection)
var/obj/item/photo/P = new(src, selection)
P.show(user)
to_chat(user, P.desc)
qdel(P)
/obj/item/camera/siliconcam/ai_camera
name = "AI photo camera"
@@ -45,11 +45,8 @@ Slimecrossing Items
ret[part.body_zone] = saved_part
return ret
/obj/item/camera/rewind/photo_taken(atom/target, mob/user)
/obj/item/camera/rewind/on_flash(atom/target, mob/user)
. = ..()
if(!.)
return
if(user == target)
to_chat(user, span_notice("You take a selfie!"))
else
@@ -66,10 +63,8 @@ Slimecrossing Items
pictures_left = 1
pictures_max = 1
/obj/item/camera/timefreeze/photo_taken(atom/target, mob/user)
/obj/item/camera/timefreeze/on_flash(atom/target, mob/user)
. = ..()
if(!.)
return
new /obj/effect/timestop(get_turf(target), 2, 50, list(user))
//Hypercharged slime cell - Charged Yellow
+2
View File
@@ -267,6 +267,8 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
/obj/structure/holosign/robot_seat,
//Singleton
/mob/dview,
//Template type
/obj/item/bodypart,
//This is meant to fail extremely loud every single time it occurs in any environment in any context, and it falsely alarms when this unit test iterates it. Let's not spawn it in.
/obj/merge_conflict_marker,
//Not meant to spawn without the machine wand
@@ -0,0 +1,110 @@
/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 ..()
///Adjuts the zoom of the camera
/obj/item/circuit_component/camera/proc/sanitize_picture_size()
camera.adjust_zoom(adjust_size_x.value, adjust_size_y.value)
/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
camera.attempt_picture(target)
/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
cam.internal_camera.attempt_picture(target)
/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)
+2 -1
View File
@@ -5753,7 +5753,6 @@
#include "code\modules\modular_computers\file_system\data.dm"
#include "code\modules\modular_computers\file_system\image_file.dm"
#include "code\modules\modular_computers\file_system\program.dm"
#include "code\modules\modular_computers\file_system\program_circuit.dm"
#include "code\modules\modular_computers\file_system\programs\airestorer.dm"
#include "code\modules\modular_computers\file_system\programs\alarm.dm"
#include "code\modules\modular_computers\file_system\programs\arcade.dm"
@@ -6723,6 +6722,7 @@
#include "code\modules\wiremod\components\abstract\list_variable.dm"
#include "code\modules\wiremod\components\abstract\module.dm"
#include "code\modules\wiremod\components\abstract\variable.dm"
#include "code\modules\wiremod\components\action\camera.dm"
#include "code\modules\wiremod\components\action\equpiment_action.dm"
#include "code\modules\wiremod\components\action\laserpointer.dm"
#include "code\modules\wiremod\components\action\light.dm"
@@ -6832,6 +6832,7 @@
#include "code\modules\wiremod\core\integrated_circuit.dm"
#include "code\modules\wiremod\core\marker.dm"
#include "code\modules\wiremod\core\port.dm"
#include "code\modules\wiremod\core\program_circuit.dm"
#include "code\modules\wiremod\core\usb_cable.dm"
#include "code\modules\wiremod\core\variable.dm"
#include "code\modules\wiremod\datatypes\any.dm"
+46 -8
View File
@@ -13,7 +13,9 @@ import { useBackend } from '../backend';
import { NtosWindow } from '../layouts';
type NtosCameraCommonData = {
size: number;
width: number;
height: number;
size: string;
minSize: number;
maxSize: number;
maxNameLength: number;
@@ -48,6 +50,8 @@ export const NtosCamera = (props) => {
export const NtosCameraContent = (props) => {
const { act, data } = useBackend<NtosCameraData>();
const {
width,
height,
size,
minSize,
maxSize,
@@ -74,7 +78,7 @@ export const NtosCameraContent = (props) => {
maxWidth="100%"
height="auto"
fixErrors
src={photo}
src={`data:image/jpeg;base64,${photo}`}
/>
</Stack.Item>
<Stack.Item>
@@ -86,7 +90,11 @@ export const NtosCameraContent = (props) => {
disabled={!canEditMetadata}
value={name}
maxLength={maxNameLength}
onChange={(value) => act('setName', { value })}
onChange={(value) =>
act('setName', {
value: value,
})
}
/>
</Stack.Item>
<Stack.Item>
@@ -98,7 +106,11 @@ export const NtosCameraContent = (props) => {
disabled={!canEditMetadata}
value={desc}
maxLength={maxDescLength}
onChange={(value) => act('setDesc', { value })}
onChange={(value) =>
act('setDesc', {
value: value,
})
}
/>
</Stack.Item>
<Stack.Item>
@@ -111,7 +123,11 @@ export const NtosCameraContent = (props) => {
disabled={!canEditMetadata}
value={caption}
maxLength={maxCaptionLength}
onChange={(value) => act('setCaption', { value })}
onChange={(value) =>
act('setCaption', {
value: value,
})
}
/>
</Stack.Item>
<Stack.Item align="center">
@@ -138,17 +154,39 @@ export const NtosCameraContent = (props) => {
</NoticeBox>
</Stack.Item>
)}
<Stack.Item align="center">{size}</Stack.Item>
<Stack.Item align="center">
<Box bold inline textColor="label" mr="0.5rem">
Photo Size:
Half Width Aperture:
</Box>
<Slider
inline
value={size}
value={width}
minValue={minSize}
maxValue={maxSize}
step={1}
onChange={(value) => act('adjustSize', { value })}
onChange={(e, value) =>
act('adjustWidth', {
value: value,
})
}
/>
</Stack.Item>
<Stack.Item align="center">
<Box bold inline textColor="label" mr="0.5rem">
Half Height Aperture:
</Box>
<Slider
inline
value={height}
minValue={minSize}
maxValue={maxSize}
step={1}
onChange={(e, value) =>
act('adjustHeight', {
value: value,
})
}
/>
</Stack.Item>
</Stack>
@@ -90,7 +90,7 @@ const PrintDialog = (props: PrintDialogProps) => {
left="5%"
>
<Stack fill justify="space-between">
<Stack.Item maxWidth="40%">
<Stack.Item maxWidth="70%">
<Section title="Formats">
<Stack vertical overflowY="scroll">
{printTypes.map((type, i) => {