diff --git a/code/__DEFINES/camera.dm b/code/__DEFINES/camera.dm new file mode 100644 index 00000000000..645037f68b4 --- /dev/null +++ b/code/__DEFINES/camera.dm @@ -0,0 +1 @@ +#define CAMERA_PICTURE_SIZE_HARD_LIMIT 21 diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm index 8b730ef4659..7feed196c75 100644 --- a/code/__DEFINES/logging.dm +++ b/code/__DEFINES/logging.dm @@ -115,6 +115,7 @@ #define LOG_CATEGORY_TRANSPORT "transport" #define LOG_CATEGORY_VIRUS "virus" #define LOG_CATEGORY_CAVE_GENERATION "cave-generation" +#define LOG_CATEGORY_IMAGE "image" // Admin categories #define LOG_CATEGORY_ADMIN "admin" diff --git a/code/__DEFINES/modular_computer.dm b/code/__DEFINES/modular_computer.dm index dcae3bc689a..16b0996a78f 100644 --- a/code/__DEFINES/modular_computer.dm +++ b/code/__DEFINES/modular_computer.dm @@ -83,6 +83,12 @@ /// The maximum length of the ringtone of the Messenger app. #define MESSENGER_RINGTONE_MAX_LENGTH 20 +///how much paper it takes from the printer to create a canvas. +#define CANVAS_PAPER_COST 10 + +///how much paper it takes from the printer to create a photo. +#define PHOTO_PAPER_COST 1 + /** * PDA Themes * For these to work, the defines must be defined in tgui/styles/themes/[define].scss diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 4f23df1d0aa..2f2f4f2d238 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1402,7 +1402,10 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects) var/height = I.Height() for(var/x in 1 to width) for(var/y in 1 to height) - grid[y][x] = I.GetPixel(x,height+1-y) + var/pixel = I.GetPixel(x,height+1-y) + if(length(pixel) == 7) + pixel += "ff" + grid[y][x] = pixel // Given a number of frames for an icon state, and the dimensions of the icon, returns the ideal dimensions for a DMI file /proc/calculate_optimal_icon_grid_dimensions(width, height, count) @@ -1435,7 +1438,7 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects) for(var/y in 1 to length(frame)) var/list/row = jointext(frame[y], "") var/splice_start = (row_index+y-1)*file_height + column*icon_width + 1 - linear_pixels = splicetext(splice_start*9, (splice_start+icon_width)*9, row) + linear_pixels = splicetext(linear_pixels, (splice_start-1)*9+1, (splice_start+icon_width-1)*9+1, row) var/zero_alpha_regex = regex(@@#(?:(?!a0a0a0)([0-9]|[a-f]){6}00)@, "gi") linear_pixels = replacetext(linear_pixels, zero_alpha_regex, COLOR_DMI_MASK) return linear_pixels diff --git a/code/__HELPERS/logging/image.dm b/code/__HELPERS/logging/image.dm new file mode 100644 index 00000000000..e0389f1a516 --- /dev/null +++ b/code/__HELPERS/logging/image.dm @@ -0,0 +1,21 @@ +/proc/log_image(text, list/data) + logger.Log(LOG_CATEGORY_IMAGE, text, data) + logger.Log(LOG_CATEGORY_COMPAT_GAME, "IMAGE: [text]") + +/// Log the creation of an image by a player +/proc/log_player_image_creation(message, mob/author = usr, icon/created_icon) + if(!CONFIG_GET(flag/log_image)) + return + GLOB.sprite_auditor.add_entry(created_icon, author) + var/datum/log_category/category = logger.log_categories[LOG_CATEGORY_IMAGE] + var/output_directory + if(category.secret) + output_directory = "[GLOB.log_directory]/secret/[category.category]" + else + output_directory = "[GLOB.log_directory]/[category.category]" + var/filename = "[copytext(md5("\icon[created_icon]"), 1, 6)].dmi" + fcopy(created_icon, "[output_directory]/[filename]") + log_image(message, list( + "ckey" = author.ckey, + "file_name" = filename, + )) diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index 584f0fb25c1..b88fad4fe84 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -156,8 +156,9 @@ * * strict - return null immediately instead of filtering out * * allow_numbers - allows numbers and common special characters - used for silicon/other weird things names * * cap_after_symbols - words like Bob's will be capitalized to Bob'S by default. False is good for titles. + * * cap_at_start - capitalize the start of words. False is good for modular computer file names. */ -/proc/reject_bad_name(t_in, allow_numbers = FALSE, max_length = MAX_NAME_LEN, ascii_only = TRUE, strict = FALSE, cap_after_symbols = TRUE) +/proc/reject_bad_name(t_in, allow_numbers = FALSE, max_length = MAX_NAME_LEN, ascii_only = TRUE, strict = FALSE, cap_after_symbols = TRUE, cap_at_start = TRUE) if(!t_in) return //Rejects the input if it is null @@ -184,7 +185,7 @@ // a .. z if(97 to 122) //Lowercase Letters - if(last_char_group == NO_CHARS_DETECTED || last_char_group == SPACES_DETECTED || cap_after_symbols && last_char_group == SYMBOLS_DETECTED) //start of a word + if(((last_char_group == NO_CHARS_DETECTED || last_char_group == SPACES_DETECTED) && cap_at_start) || (cap_after_symbols && last_char_group == SYMBOLS_DETECTED)) //start of a word char = uppertext(char) number_of_alphanumeric++ last_char_group = LETTERS_DETECTED diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 6808205734a..f6f3456cc7e 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -166,6 +166,9 @@ /// log shuttle related actions, ie shuttle computers, shuttle manipulator, emergency console /datum/config_entry/flag/log_shuttle +/// log image authoring, such as using the modular computer paint app +/datum/config_entry/flag/log_image + /// logs all timers in buckets on automatic bucket reset (Useful for timer debugging) /datum/config_entry/flag/log_timers_on_bucket_reset diff --git a/code/controllers/subsystem/persistent_paintings.dm b/code/controllers/subsystem/persistent_paintings.dm index a1f0fd8e723..e9a057432fe 100644 --- a/code/controllers/subsystem/persistent_paintings.dm +++ b/code/controllers/subsystem/persistent_paintings.dm @@ -91,6 +91,33 @@ new_data["frame_type"] = frame_type return new_data +/datum/painting/proc/get_icon() + return icon("data/paintings/images/[md5].png") + +/datum/painting/proc/spawn_canvas(spawn_loc) + var/icon/art_icon = get_icon() + var/art_width = art_icon.Width() + var/art_height = art_icon.Height() + var/obj/item/canvas/printed_canvas + for(var/obj/item/canvas/canvas_type as anything in typesof(/obj/item/canvas)) + if(canvas_type::width == art_width && canvas_type::height == art_height) + printed_canvas = new canvas_type(spawn_loc) + if(!printed_canvas) + return null + fill_canvas(printed_canvas, art_icon) + return printed_canvas + +/datum/painting/proc/fill_canvas(obj/item/canvas/canvas, icon = get_icon()) + canvas.painting_metadata = src + canvas.fill_grid_from_icon(icon) + canvas.generated_icon = icon + canvas.icon_generated = TRUE + canvas.finalized = TRUE + canvas.name = "painting - [title]" + ///this is a copy of something that is already in the database- it should not be able to be saved. + canvas.no_save = TRUE + canvas.update_icon() + SUBSYSTEM_DEF(persistent_paintings) name = "Persistent Paintings" flags = SS_NO_FIRE diff --git a/code/modules/admin/verbs/sprite_auditor.dm b/code/modules/admin/verbs/sprite_auditor.dm new file mode 100644 index 00000000000..64022618c28 --- /dev/null +++ b/code/modules/admin/verbs/sprite_auditor.dm @@ -0,0 +1,39 @@ +GLOBAL_DATUM_INIT(sprite_auditor, /datum/sprite_auditor, new) + +/// A global singleton providing a convenient UI for quickly viewing sprites created by players +/datum/sprite_auditor + var/list/entries + +/datum/sprite_auditor/proc/add_entry(icon/created_icon, mob/author) + var/mutable_appearance/icon_appearance = mutable_appearance(created_icon) + LAZYADD(entries, list(list( + "ref" = REF(icon_appearance.appearance), + "name" = author.real_name, + "ckey" = author.ckey, + "appearance" = icon_appearance, + "timestamp" = gameTimestamp(), + ))) + SStgui.update_uis(src) + +/datum/sprite_auditor/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "SpriteAuditor") + ui.open() + +/datum/sprite_auditor/ui_state(mob/user) + return ADMIN_STATE(R_ADMIN) + +/datum/sprite_auditor/ui_data(mob/user) + return list("entries" = entries) + +/datum/sprite_auditor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + switch(action) + if("playerPanel") + SSadmin_verbs.dynamic_invoke_verb(usr, /datum/admin_verb/show_player_panel, get_mob_by_ckey(params["ckey"])) + +ADMIN_VERB(sprite_auditor, R_ADMIN, "Audit Player-made Sprites", "View sprites created by players this round.", ADMIN_CATEGORY_MAIN) + GLOB.sprite_auditor.ui_interact(user.mob) diff --git a/code/modules/art/paintings.dm b/code/modules/art/paintings.dm index 45dfa4f179e..ecb5120fd12 100644 --- a/code/modules/art/paintings.dm +++ b/code/modules/art/paintings.dm @@ -2,7 +2,7 @@ GLOBAL_LIST_INIT(canvas_dimensions, init_canvas_dimensions()) /proc/init_canvas_dimensions() . = list() - for(var/obj/item/canvas/canvas_type in typesof(/obj/item/canvas)) + for(var/obj/item/canvas/canvas_type as anything in typesof(/obj/item/canvas)) var/width = canvas_type::width var/height = canvas_type::height .["[width]x[height]"] = list(width, height) @@ -387,18 +387,18 @@ GLOBAL_LIST_INIT(canvas_dimensions, init_canvas_dimensions()) return if(istype(painting_implement, /obj/item/paint_palette)) var/obj/item/paint_palette/palette = painting_implement - return palette.current_color + return LOWER_TEXT(palette.current_color) if(istype(painting_implement, /obj/item/toy/crayon)) var/obj/item/toy/crayon/crayon = painting_implement - return crayon.paint_color + return LOWER_TEXT(crayon.paint_color) else if(istype(painting_implement, /obj/item/pen)) var/obj/item/pen/pen = painting_implement - return pen.colour + return LOWER_TEXT(pen.colour) else if (istype(painting_implement, /obj/item/airlock_painter/decal)) var/obj/item/airlock_painter/decal/painter = painting_implement - return painter.selected_custom_color + return LOWER_TEXT(painter.selected_custom_color) else if(istype(painting_implement, /obj/item/soap) || istype(painting_implement, /obj/item/rag)) - return canvas_color + return LOWER_TEXT(canvas_color) /obj/item/canvas/proc/get_paint_tool_palette(obj/item/painting_implement) if(!painting_implement) diff --git a/code/modules/logging/categories/log_category_misc.dm b/code/modules/logging/categories/log_category_misc.dm index a723cee1a6f..6ebe7e59745 100644 --- a/code/modules/logging/categories/log_category_misc.dm +++ b/code/modules/logging/categories/log_category_misc.dm @@ -72,3 +72,8 @@ /datum/log_category/fishing category = LOG_CATEGORY_FISHING config_flag = /datum/config_entry/flag/fishing + +/datum/log_category/image + category = LOG_CATEGORY_IMAGE + config_flag = /datum/config_entry/flag/log_image + secret = TRUE diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 0e26f82c657..9607ac1e506 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -711,6 +711,7 @@ active_program = program program.alert_pending = FALSE idle_threads.Remove(program) + program.on_made_active_program(user) if(open_ui) INVOKE_ASYNC(src, PROC_REF(update_tablet_open_uis), user) update_appearance(UPDATE_ICON) @@ -736,6 +737,7 @@ active_program = program program.alert_pending = FALSE + program.on_made_active_program(user) if(open_ui) INVOKE_ASYNC(src, PROC_REF(update_tablet_open_uis), user) update_appearance(UPDATE_ICON) @@ -966,7 +968,9 @@ return ITEM_INTERACT_SUCCESS /obj/item/modular_computer/proc/photo_act(mob/user, obj/item/photo/scanned_photo) - if(!store_file(new /datum/computer_file/picture(scanned_photo.picture), user)) + var/datum/picture/source_picture = scanned_photo.picture + var/datum/computer_file/image/image_file = new /datum/computer_file/image(source_picture.picture_image, display_name = source_picture.picture_name, source_photo_or_painting = source_picture) + if(!store_file(image_file, user)) balloon_alert(user, "no space!") return ITEM_INTERACT_BLOCKING balloon_alert(user, "photo scanned") diff --git a/code/modules/modular_computers/computers/item/computer_files.dm b/code/modules/modular_computers/computers/item/computer_files.dm index 027802c8cb9..849be3ee174 100644 --- a/code/modules/modular_computers/computers/item/computer_files.dm +++ b/code/modules/modular_computers/computers/item/computer_files.dm @@ -88,6 +88,26 @@ return file return null +/** + * find_file_by_full_name + * + * Will check all applications in a tablet for files and, if they have \ + * the same filename AND extension, will return it. + * If a computer disk is passed instead, it will check the disk over the computer. + */ +/obj/item/modular_computer/proc/find_file_by_full_name(full_path, obj/item/disk/computer/target_disk) + if(!istext(full_path)) + return null + if(isnull(target_disk)) + for(var/datum/computer_file/file as anything in stored_files) + if("[file.filename].[file.filetype]" == full_path) + return file + else + for(var/datum/computer_file/file as anything in target_disk.stored_files) + if("[file.filename].[file.filetype]" == full_path) + return file + return null + /** * find_file_by_uid * diff --git a/code/modules/modular_computers/file_system/data.dm b/code/modules/modular_computers/file_system/data.dm index 892f9967708..7721d50b763 100644 --- a/code/modules/modular_computers/file_system/data.dm +++ b/code/modules/modular_computers/file_system/data.dm @@ -86,4 +86,26 @@ temp.gas_record = gas_record return temp +/datum/computer_file/data/paint_project + filetype = "NPNT" + /// The sprite editor workspace stored in the file + var/datum/sprite_editor_workspace/workspace + /// The unmodified photo or painting this is a digital copy of. + var/source_photo_or_painting + +/datum/computer_file/data/paint_project/New(datum/sprite_editor_workspace/workspace, source_photo_or_painting) + ..() + src.workspace = workspace + src.source_photo_or_painting = source_photo_or_painting + +/datum/computer_file/data/paint_project/clone(rename) + var/datum/computer_file/data/paint_project/temp = ..() + temp.workspace = workspace.copy() + temp.source_photo_or_painting = source_photo_or_painting + return temp + +/// Assign this file's backing datum +/datum/computer_file/data/paint_project/proc/set_source(new_source) + source_photo_or_painting = new_source + #undef BLOCK_SIZE diff --git a/code/modules/modular_computers/file_system/image_file.dm b/code/modules/modular_computers/file_system/image_file.dm new file mode 100644 index 00000000000..f029793701d --- /dev/null +++ b/code/modules/modular_computers/file_system/image_file.dm @@ -0,0 +1,70 @@ +/** + * PNG file type + * Stores an image which can be used by other programs. + */ +/datum/computer_file/image + filetype = "PNG" // the superior filetype + size = 1 + /// The instance of the stored image. + var/icon/stored_icon + /// The mutable appearance used to provide an appearance reference to uis. + /// Dynamic icons refs do not work for this purpose, so they must be wrapped in mutable appearances. + var/mutable_appearance/ref_appearance + /// The name of the asset cache item. + /// This will be initialized after assign_path() is called. + var/image_name + /// The unmodified photo or painting this is a digital copy of. + var/source_photo_or_painting + /// The ckey of the user who last modified this image, applied to printed photos. + var/author_ckey + +/datum/computer_file/image/New(icon/stored_icon, image_name, display_name, source_photo_or_painting) + ..() + if(isnull(stored_icon)) + return + src.filename = "[display_name] ([uid])" + src.stored_icon = stored_icon + src.image_name = image_name + set_source(source_photo_or_painting) + +/datum/computer_file/image/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) + . = ..() + assign_path() + assign_ref_appearance() + +/// Assigns an asset path to the stored image, for use in the UI. +/datum/computer_file/image/proc/assign_path() + if(isnull(stored_icon)) + return + if(!isnull(image_name)) + return + image_name = SSmodular_computers.get_next_picture_name() + SSassets.transport.register_asset(image_name, stored_icon) + +/datum/computer_file/image/proc/assign_ref_appearance() + if(!isnull(ref_appearance)) + return + ref_appearance = mutable_appearance(stored_icon) + +/datum/computer_file/image/proc/get_image_ref() + return REF(ref_appearance.appearance) + +/datum/computer_file/image/clone(rename = FALSE) + var/datum/computer_file/image/temp = ..() + temp.stored_icon = stored_icon + temp.ref_appearance = ref_appearance + temp.image_name = image_name + temp.source_photo_or_painting = source_photo_or_painting + temp.author_ckey = author_ckey + return temp + +/// Assign this file's backing datum +/datum/computer_file/image/proc/set_source(new_source) + source_photo_or_painting = new_source + if(istype(new_source, /datum/picture)) + var/datum/picture/source_picture = new_source + author_ckey = source_picture.author_ckey + if(istype(new_source, /datum/painting)) + var/datum/painting/source_painting = new_source + author_ckey = source_painting.creator_ckey + diff --git a/code/modules/modular_computers/file_system/picture_file.dm b/code/modules/modular_computers/file_system/picture_file.dm deleted file mode 100644 index 8bd0cfb1171..00000000000 --- a/code/modules/modular_computers/file_system/picture_file.dm +++ /dev/null @@ -1,37 +0,0 @@ -/** - * PNG file type - * Stores a picture which can be used by other programs. - */ -/datum/computer_file/picture - filetype = "PNG" // the superior filetype - size = 1 - /// The instance of the stored picture. - var/datum/picture/stored_picture - /// The name of the asset cache item. - /// This will be initialized after assign_path() is called. - var/picture_name - -/datum/computer_file/picture/New(datum/picture/stored_picture, picture_name) - ..() - if(isnull(stored_picture)) - return - src.filename = "[stored_picture.picture_name] ([uid])" - src.stored_picture = stored_picture - src.picture_name = picture_name - -/datum/computer_file/picture/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) - . = ..() - assign_path() - -/// Assigns an asset path to the stored image, for use in the UI. -/datum/computer_file/picture/proc/assign_path() - if(!isnull(picture_name)) - return - picture_name = SSmodular_computers.get_next_picture_name() - SSassets.transport.register_asset(picture_name, stored_picture.picture_image) - -/datum/computer_file/picture/clone(rename = FALSE) - var/datum/computer_file/picture/temp = ..() - temp.stored_picture = stored_picture - temp.picture_name = picture_name - return temp diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm index b3ca1b0990d..215849b02c1 100644 --- a/code/modules/modular_computers/file_system/program.dm +++ b/code/modules/modular_computers/file_system/program.dm @@ -252,3 +252,7 @@ INVOKE_ASYNC(computer, TYPE_PROC_REF(/obj/item/modular_computer, update_tablet_open_uis), user) computer.update_appearance(UPDATE_ICON) return TRUE + +///Called when the program is made the active program. +/datum/computer_file/program/proc/on_made_active_program(mob/user) + return diff --git a/code/modules/modular_computers/file_system/programs/file_browser.dm b/code/modules/modular_computers/file_system/programs/file_browser.dm index 7e0d0679b9d..d33c34a348b 100644 --- a/code/modules/modular_computers/file_system/programs/file_browser.dm +++ b/code/modules/modular_computers/file_system/programs/file_browser.dm @@ -1,3 +1,88 @@ +GLOBAL_LIST_INIT(print_types, init_print_types()) + +/proc/init_print_types() + var/list/print_types = list() + 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( + "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)", + "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) + if(!(computer.hardware_flag & PROGRAM_CONSOLE)) + to_chat(user, span_notice("Printing error: Canvas printing is only supported on stationary consoles.")) + return FALSE + if(computer.stored_paper < CANVAS_PAPER_COST) + to_chat(user, span_notice("Printing error: Your printer needs at least [CANVAS_PAPER_COST] paper to print a canvas.")) + return FALSE + return TRUE + +/proc/prepare_canvas_from_file(obj/item/canvas/canvas, datum/computer_file/image/image_file, obj/item/modular_computer/computer, width, height, x, y) + computer.stored_paper -= CANVAS_PAPER_COST + if(istype(image_file.source_photo_or_painting, /datum/painting)) + var/datum/painting/source_painting = image_file.source_photo_or_painting + var/icon/painting_icon = source_painting.get_icon() + if(width == painting_icon.Width() && height == painting_icon.Height() && !x && !y) + source_painting.fill_canvas(canvas) + return + var/datum/icon_transformer/transformer = new() + var/temp_file = "tmp/[copytext(REF(image_file.stored_icon), 2, -1)].dmi" + fcopy(image_file.stored_icon, temp_file) + transformer.scale(width, height) + transformer.blend_color("#ffffff", ICON_OVERLAY) + transformer.blend_icon(uni_icon(temp_file, ""), ICON_OVERLAY, x+1, y+1) + var/datum/universal_icon/blank = uni_icon('icons/blanks/32x32.dmi', "nothing", transform = transformer) + canvas.fill_grid_from_icon(blank.to_icon()) + fdel(temp_file) + canvas.painting_metadata.medium = "Digital Art" + canvas.used = TRUE + canvas.update_icon() + +/proc/check_can_print_photo(_typepath, _image_file, obj/item/modular_computer/computer, mob/user) + if(computer.stored_paper < PHOTO_PAPER_COST) + to_chat(user, span_notice("Printing error: Your printer needs at least [PHOTO_PAPER_COST] paper to print a photo.")) + return FALSE + return TRUE + +/proc/prepare_photo_from_file(obj/item/photo/photo, datum/computer_file/image/image_file, obj/item/modular_computer/computer, width, height, x, y) + computer.stored_paper -= PHOTO_PAPER_COST + var/icon/photo_image = image_file.stored_icon + var/image_width = photo_image.Width() + var/image_height = photo_image.Height() + if(istype(image_file.source_photo_or_painting, /datum/picture) && width == image_width && height == image_height && !x && !y) + var/datum/picture/source_photo = image_file.source_photo_or_painting + photo.set_picture(source_photo, TRUE, TRUE) + return + var/datum/icon_transformer/transformer = new() + var/temp_file = "tmp/[copytext(REF(photo_image), 2, -1)].dmi" + fcopy(photo_image, temp_file) + transformer.scale(width, height) + transformer.blend_color("#ffffff", ICON_OVERLAY) + transformer.blend_icon(uni_icon(temp_file, ""), ICON_OVERLAY, x+1, y+1) + var/datum/universal_icon/blank = uni_icon('icons/blanks/32x32.dmi', "nothing", transform = transformer) + var/datum/picture/new_photo = new(image_file.filename, desc = "A printout of a digital image.", image = blank.to_icon(), size_x = width, size_y = height, autogenerate_icon = TRUE, author_ckey_ = image_file.author_ckey) + fdel(temp_file) + image_file.source_photo_or_painting = new_photo + photo.set_picture(new_photo, TRUE, TRUE) + /datum/computer_file/program/filemanager filename = "filemanager" filedesc = "File Manager" @@ -33,7 +118,7 @@ var/datum/computer_file/file = computer.find_file_by_name(params["name"]) if(!file) return - var/newname = reject_bad_name(params["new_name"]) + var/newname = reject_bad_name(params["new_name"], allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE) if(!newname || newname != params["new_name"]) playsound(computer, 'sound/machines/terminal/terminal_error.ogg', 25, FALSE) return @@ -45,7 +130,7 @@ var/datum/computer_file/file = computer.find_file_by_name(params["name"], computer.inserted_disk) if(!file) return - var/newname = reject_bad_name(params["new_name"]) + var/newname = reject_bad_name(params["new_name"], allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE) if(!newname || newname != params["new_name"]) playsound(computer, 'sound/machines/terminal/terminal_error.ogg', 25, FALSE) return @@ -78,6 +163,51 @@ if(!binary || !istype(binary)) return binary.alert_silenced = !binary.alert_silenced + if("PRG_print") + var/datum/computer_file/image/picture = computer.find_file_by_name(params["name"]) + if(!istype(picture)) + return + try_print(picture, params["width"], params["height"], params["offsetX"], params["offsetY"], params["typepath"], usr) + if("PRG_usbprint") + if(!computer.inserted_disk) + return + var/datum/computer_file/image/picture = computer.find_file_by_name(params["name"], computer.inserted_disk) + if(!istype(picture)) + return + 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) + return + var/image_width = picture.stored_icon.Width() + var/image_height = picture.stored_icon.Height() + var/min_offset_x = min(width - image_width, 0) + var/max_offset_x = max(width - image_width, 0) + var/min_offset_y = min(height - image_height, 0) + var/max_offset_y = max(height - image_height, 0) + if(!ISINRANGE(offset_x, min_offset_x, max_offset_x) || !ISINRANGE(offset_y, min_offset_y, max_offset_y)) + return + typepath = text2path(typepath) + var/datum/callback/check_callback = print_type["check_callback"] + if(!check_callback.Invoke(typepath, picture, computer, user)) + return + var/obj/item/printed_item = new typepath(computer.physical.drop_location()) + var/datum/callback/prepare_callback = print_type["prepare_callback"] + prepare_callback.Invoke(printed_item, picture, computer, width, height, offset_x, offset_y) + user?.put_in_hands(printed_item) + playsound(computer.physical, 'sound/machines/printer.ogg', 100, TRUE) + +/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]) + return list("printTypes" = print_types) /datum/computer_file/program/filemanager/ui_data(mob/user) var/list/data = list() @@ -90,10 +220,20 @@ for(var/datum/computer_file/F as anything in computer.stored_files) var/noisy = FALSE var/silenced = FALSE + var/printable = FALSE + var/image_width = 0 + var/image_height = 0 + var/image_ref var/datum/computer_file/program/binary = F if(istype(binary)) noisy = binary.alert_able silenced = binary.alert_silenced + var/datum/computer_file/image/picture_file = F + if(istype(picture_file)) + printable = TRUE + image_width = picture_file.stored_icon.Width() + image_height = picture_file.stored_icon.Height() + image_ref = picture_file.get_image_ref() files += list(list( "name" = F.filename, "type" = F.filetype, @@ -101,17 +241,35 @@ "undeletable" = F.undeletable, "alert_able" = noisy, "alert_silenced" = silenced, + "printable" = printable, + "image_ref" = image_ref, + "image_width" = image_width, + "image_height" = image_height, )) data["files"] = files if(computer.inserted_disk) data["usbconnected"] = TRUE var/list/usbfiles = list() for(var/datum/computer_file/F as anything in computer.inserted_disk.stored_files) + var/printable = FALSE + var/image_width = 0 + var/image_height = 0 + var/image_ref + var/datum/computer_file/image/picture_file = F + if(istype(picture_file)) + printable = TRUE + image_width = picture_file.stored_icon.Width() + image_height = picture_file.stored_icon.Height() + image_ref = picture_file.get_image_ref() usbfiles += list(list( "name" = F.filename, "type" = F.filetype, "size" = F.size, "undeletable" = F.undeletable, + "printable" = printable, + "image_ref" = image_ref, + "image_width" = image_width, + "image_height" = image_height, )) data["usbfiles"] = usbfiles diff --git a/code/modules/modular_computers/file_system/programs/maintenance/camera.dm b/code/modules/modular_computers/file_system/programs/maintenance/camera.dm index 5615df56925..4ff763e1d5f 100644 --- a/code/modules/modular_computers/file_system/programs/maintenance/camera.dm +++ b/code/modules/modular_computers/file_system/programs/maintenance/camera.dm @@ -14,8 +14,18 @@ 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 /// 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? + var/can_edit_metadata = TRUE + /// The name we will give to the picture when we first save it + var/current_picture_name + /// The description we will give to the picture when we first save it + var/current_picture_desc + /// The caption we will give to the picture when we first save it + var/current_picture_caption // Special type of camera for this exact usecase to prevent harddels /obj/item/camera/app @@ -27,54 +37,134 @@ . = ..() internal_camera = new(computer) internal_camera.print_picture_on_snap = FALSE - RegisterSignal(internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(save_picture)) + 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) - QDEL_NULL(internal_picture) + internal_picture = null + QDEL_NULL(picture_appearance) return ..() + /datum/computer_file/program/maintenance/camera/tap(atom/tapped_atom, mob/living/user, list/modifiers) . = ..() + take_picture(user, get_turf(tapped_atom)) - QDEL_NULL(internal_picture) +/datum/computer_file/program/maintenance/camera/on_made_active_program(user) + RegisterSignal(computer, COMSIG_RANGED_ITEM_INTERACTING_WITH_ATOM_SECONDARY, PROC_REF(on_computer_ranged_interact)) + +/datum/computer_file/program/maintenance/camera/kill_program(mob/user) + . = ..() + UnregisterSignal(computer, COMSIG_RANGED_ITEM_INTERACTING_WITH_ATOM_SECONDARY) + internal_picture = null + +/datum/computer_file/program/maintenance/camera/background_program(mob/user) + . = ..() + UnregisterSignal(computer, COMSIG_RANGED_ITEM_INTERACTING_WITH_ATOM_SECONDARY) + +/datum/computer_file/program/maintenance/camera/proc/on_computer_ranged_interact(_source, mob/user, atom/target, list/modifiers) + SIGNAL_HANDLER + 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/turf/our_turf = get_turf(tapped_atom) 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), our_turf, user, internal_camera.picture_size_x + 1, internal_camera.picture_size_y + 1) + 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) -/datum/computer_file/program/maintenance/camera/proc/save_picture(cam, target, user, datum/picture/picture) +/datum/computer_file/program/maintenance/camera/proc/on_image_captured(cam, target, user, datum/picture/picture) SIGNAL_HANDLER internal_picture = picture + picture_appearance.icon = internal_picture.picture_image + current_picture_name = null + current_picture_desc = null + current_picture_caption = null + can_edit_metadata = TRUE picture_number++ - computer.save_photo(internal_picture.picture_image) + +/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 + internal_picture.picture_desc = "[current_picture_desc] - [internal_picture.picture_desc]" + internal_picture.caption = current_picture_caption + 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) /datum/computer_file/program/maintenance/camera/ui_data(mob/user) var/list/data = list() if(!isnull(internal_picture)) - user << browse_rsc(internal_picture.picture_image, "tmp_photo[picture_number].png") - data["photo"] = "tmp_photo[picture_number].png" - - data["paper_left"] = computer.stored_paper + data["photo"] = REF(picture_appearance.appearance) + 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) return data /datum/computer_file/program/maintenance/camera/ui_act(action, params, datum/tgui/ui, datum/ui_state/state) . = ..() switch(action) - if("print_photo") - if(computer.stored_paper <= 0) - to_chat(ui.user, span_notice("Hardware error: Printer out of paper.")) + 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))) return - internal_camera.printpicture(usr, internal_picture) - computer.stored_paper-- - computer.visible_message(span_notice("\The [computer] prints out a paper.")) + internal_camera.picture_size_x = new_size + internal_camera.picture_size_y = new_size + if("setName") + if(!(internal_picture && can_edit_metadata)) + return + current_picture_name = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(32)) + if("setDesc") + if(!(internal_picture && can_edit_metadata)) + return + current_picture_desc = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(128)) + if("setCaption") + if(!(internal_picture && can_edit_metadata)) + return + current_picture_caption = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(256)) + if("savePhoto") + if(!internal_picture) + return + save_picture(ui.user) + 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 @@ -82,6 +172,8 @@ ///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 /** @@ -94,6 +186,7 @@ /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) @@ -108,6 +201,8 @@ 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() @@ -117,9 +212,8 @@ var/datum/computer_file/program/maintenance/camera/cam = associated_program if(!cam.internal_camera.can_target(target)) return - var/pic_size_x = cam.internal_camera.picture_size_x - 1 - var/pic_size_y = cam.internal_camera.picture_size_y - 1 - INVOKE_ASYNC(cam.internal_camera, TYPE_PROC_REF(/obj/item/camera, captureimage), target, null, pic_size_x, pic_size_y) + 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 diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm index 48e19516dad..dc83a7a76ae 100644 --- a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm +++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm @@ -52,19 +52,19 @@ /datum/computer_file/program/messenger/on_install() . = ..() RegisterSignal(computer, COMSIG_MODULAR_COMPUTER_FILE_STORE, PROC_REF(check_new_photo)) - RegisterSignal(computer, COMSIG_MODULAR_COMPUTER_FILE_DELETE, PROC_REF(check_photo_removed)) + RegisterSignal(computer, COMSIG_MODULAR_COMPUTER_FILE_DELETE, PROC_REF(check_image_removed)) RegisterSignal(computer, COMSIG_MODULAR_PDA_IMPRINT_UPDATED, PROC_REF(on_imprint_added)) RegisterSignal(computer, COMSIG_MODULAR_PDA_IMPRINT_RESET, PROC_REF(on_imprint_reset)) -/datum/computer_file/program/messenger/proc/check_new_photo(sender, datum/computer_file/picture/storing_picture) +/datum/computer_file/program/messenger/proc/check_new_photo(sender, datum/computer_file/image/storing_image) SIGNAL_HANDLER - if(!istype(storing_picture)) + if(!istype(storing_image)) return update_pictures_for_all() -/datum/computer_file/program/messenger/proc/check_photo_removed(sender, datum/computer_file/picture/photo_removed) +/datum/computer_file/program/messenger/proc/check_image_removed(sender, datum/computer_file/image/image_removed) SIGNAL_HANDLER - if(istype(photo_removed) && selected_image == photo_removed.picture_name) + if(istype(image_removed) && selected_image == image_removed.image_name) selected_image = null /datum/computer_file/program/messenger/proc/on_imprint_added(sender) @@ -109,12 +109,12 @@ /datum/computer_file/program/messenger/proc/can_send_everyone_message() return COOLDOWN_FINISHED(src, last_text) && COOLDOWN_FINISHED(src, last_text_everyone) -/// Gets all currently relevant photo asset keys +/// Gets all currently relevant image asset keys /datum/computer_file/program/messenger/proc/get_picture_assets() var/list/data = list() - for(var/datum/computer_file/picture/photo in computer.stored_files) - data |= photo.picture_name + for(var/datum/computer_file/image/image_file in computer.stored_files) + data |= image_file.image_name if(viewing_messages_of in saved_chats) var/datum/pda_chat/chat = LAZYACCESS(saved_chats, viewing_messages_of) @@ -309,12 +309,12 @@ var/photo_uid = text2num(params["uid"]) - var/datum/computer_file/picture/selected_photo = computer.find_file_by_uid(photo_uid) + var/datum/computer_file/image/selected_image_file = computer.find_file_by_uid(photo_uid) - if(!istype(selected_photo)) + if(!istype(selected_image_file)) return FALSE - selected_image = selected_photo.picture_name + selected_image = selected_image_file.image_name return TRUE if("PDA_siliconSelectPhoto") @@ -367,10 +367,10 @@ // silicons handle selecting photos a bit differently for now if(!issilicon(user)) var/list/stored_photos = list() - for(var/datum/computer_file/picture/photo_file in computer.stored_files) + for(var/datum/computer_file/image/image_file in computer.stored_files) stored_photos += list(list( - "uid" = photo_file.uid, - "path" = SSassets.transport.get_asset_url(photo_file.picture_name) + "uid" = image_file.uid, + "path" = SSassets.transport.get_asset_url(image_file.image_name) )) data["stored_photos"] = stored_photos data["selected_photo_path"] = !isnull(selected_image) ? SSassets.transport.get_asset_url(selected_image) : null diff --git a/code/modules/modular_computers/file_system/programs/nanopaint.dm b/code/modules/modular_computers/file_system/programs/nanopaint.dm new file mode 100644 index 00000000000..6e2d4bd49ab --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/nanopaint.dm @@ -0,0 +1,313 @@ +#define PALETTE_SIZE 32 +#define SANE_PHOTO_EDITING_SIZE_LIMIT 96 // I really don't think the server can handle someone editing large images, but a photo taken with the default camera dimensions shouldn't be too awful. + +GLOBAL_LIST_INIT(nanopaint_supported_filetypes, zebra_typecacheof(list(\ + /datum/computer_file/data/paint_project = /datum/computer_file/data/paint_project,\ + /datum/computer_file/image = /datum/computer_file/image,\ +))) + +/datum/computer_file/program/nanopaint + filename = "nanopaint" + filedesc = "NanoPaint" + downloader_category = PROGRAM_CATEGORY_DEVICE + program_open_overlay = "generic" + extended_desc = "Draw pictures on your device." + tgui_id = "NtosNanopaint" + program_icon = "paintbrush" + size = 5 + can_run_on_flags = PROGRAM_ALL + /// A weak reference to the data file containing the workspace currently being worked on. + var/datum/weakref/backing_file + /// The name of the file that was opened, in case we are trying to save a file that is no longer accessible. + var/opened_file_name + /// The typepath of the file that was opened, in case we are trying to save a file that is no longer accessible. + var/datum/computer_file/opened_file_type + var/datum/sprite_editor_workspace/current_workspace + /// If the opened file is an unmodified photo or painting, this is a reference to it. + var/source_photo_or_painting + /// If we have modified this project, store whatever unmodified photo or painting we were made from here. + var/source_on_undo_all + /// The current color we are painting with + var/current_color = "#ffffffff" + /// A list of colors we have quick access to + var/list/palette = list() + /// UI data for a modal dialog to display + var/list/dialog + +/datum/computer_file/program/nanopaint/ui_static_data(mob/user) + return list( + "templateSizes" = GLOB.canvas_dimensions, + "saveableTypes" = list( + list( + "displayText" = "NanoPaint Project (.[/datum/computer_file/data/paint_project::filetype])", + "typepath" = /datum/computer_file/data/paint_project, + "extension" = /datum/computer_file/data/paint_project::filetype, + ), + list( + "displayText" = "PNG Image (.[/datum/computer_file/image::filetype])", + "typepath" = /datum/computer_file/image, + "extension" = /datum/computer_file/image::filetype, + ), + ), + "minSize" = 1, + "maxSize" = SANE_PHOTO_EDITING_SIZE_LIMIT, + ) + +/datum/computer_file/program/nanopaint/ui_data(mob/user) + var/list/data = list() + data["dialog"] = dialog + var/list/editor_data = list() + editor_data["serverSelectedColor"] = current_color + editor_data["serverPalette"] = palette + editor_data["maxServerColors"] = PALETTE_SIZE + editor_data["onSelectServerColor"] = "onSelectColor" + editor_data["onAddServerColor"] = "onAddPaletteColor" + editor_data["onRemoveServerColor"] = "onRemovePaletteColor" + if(current_workspace) + editor_data += current_workspace.sprite_editor_ui_data() + data["editorData"] = editor_data + data["workspaceOpen"] = !!current_workspace + data["diskInserted"] = !!computer.inserted_disk + var/list/all_files = computer.get_files(TRUE) + var/list/drive_files = list() + var/list/disk_files = list() + for(var/datum/computer_file/file as anything in all_files) + var/base_supported_type = is_type_in_typecache(file, GLOB.nanopaint_supported_filetypes) + if(!base_supported_type) + continue + var/list/file_data = list("name" = file.filename, "extension" = file.filetype, "uid" = file.uid, "baseType" = base_supported_type) + if(file.computer) + drive_files += list(file_data) + else + disk_files += list(file_data) + data["driveFiles"] = drive_files + data["diskFiles"] = disk_files + return data + +/datum/computer_file/program/nanopaint/proc/check_dialog(act, modal_type) + return dialog && dialog["type"] == modal_type && (!act || dialog["action"] == act) + +/datum/computer_file/program/nanopaint/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + var/mob/user = ui.user + switch(action) + if("spriteEditorCommand") + if(!current_workspace) + return + var/command = params["command"] + switch(command) + if("transaction") + current_workspace.new_transaction(params["transaction"]) + if(!source_on_undo_all && source_photo_or_painting) + source_on_undo_all = source_photo_or_painting + source_photo_or_painting = null + if("toggleVisible") + current_workspace.toggle_layer_visible(params["layer"]) + if("undo") + current_workspace.undo() + if(!length(current_workspace.undo_stack)) + source_photo_or_painting = source_on_undo_all + source_on_undo_all = null + if("redo") + current_workspace.redo() + if(!source_on_undo_all && source_photo_or_painting) + source_on_undo_all = source_photo_or_painting + source_photo_or_painting = null + return TRUE + if("onSelectColor") + current_color = params["color"] + return TRUE + if("onAddPaletteColor") + if(length(palette) >= PALETTE_SIZE) + return + palette += params["color"] + if("onRemovePaletteColor") + var/index = params["index"] + palette.Cut(index, index+1) + return TRUE + if("closeDialog") + dialog = null + return TRUE + if("newDialog") + dialog = list("type" = "new") + return TRUE + if("new") + if(!check_dialog(null, "new")) + return + var/width = params["width"] + if(!ISINRANGE(width, 1, SANE_PHOTO_EDITING_SIZE_LIMIT)) + return + var/height = params["height"] + if(!ISINRANGE(height, 1, SANE_PHOTO_EDITING_SIZE_LIMIT)) + return + dialog = null + close_workspace() + INVOKE_ASYNC(src, PROC_REF(new_workspace), width, height) + return TRUE + if("openDialog") + dialog = list("type" = "select", "title" = "Open File", "confirmText" = "Open", "action" = "open") + return TRUE + if("open") + if(!check_dialog("open", "select")) + return + dialog = null + INVOKE_ASYNC(src, PROC_REF(open_file), user, params["uid"], params["onDisk"], params["name"], text2path(params["type"])) + return TRUE + if("save") + var/datum/computer_file/actual_file = backing_file?.resolve() + if(!actual_file) + if(!opened_file_name) + dialog = list("type" = "select", "title" = "Save As", "confirmText" = "Save", "action" = "saveAs") + return TRUE + actual_file = computer.find_file_by_full_name("[opened_file_name].[opened_file_type::filetype]") + if(actual_file && actual_file.computer != computer && actual_file.disk_host != computer.inserted_disk) + actual_file = null + if(actual_file) + INVOKE_ASYNC(src, PROC_REF(write_to_file), user, actual_file, actual_file.disk_host) + else + INVOKE_ASYNC(src, PROC_REF(save_file), user, opened_file_name, opened_file_type) + return TRUE + if("saveAsDialog") + dialog = list("type" = "select", "title" = "Save As", "confirmText" = "Save", "action" = "saveAs") + return TRUE + if("saveAs", "overwrite") + if(!check_dialog(action, action == "saveAs" ? "select" : "confirm")) + return + dialog = null + var/uid = params["uid"] + var/new_file_name = params["name"] + var/saving_to_disk = params["onDisk"] + var/datum/computer_file/new_file_type = text2path(params["typepath"]) + var/extension = new_file_type::filetype + var/datum/computer_file/existing_file + if(saving_to_disk) + if(!computer.inserted_disk) + dialog = list("type" = "error", "message" = "[new_file_name] - The disk has been removed.") + return TRUE + if(uid) + existing_file = computer.find_file_by_uid(uid, computer.inserted_disk) + else + existing_file = computer.find_file_by_full_name("[new_file_name].[extension]", computer.inserted_disk) + else + if(uid) + existing_file = computer.find_file_by_uid(uid) + else + existing_file = computer.find_file_by_full_name("[new_file_name].[extension]") + if(existing_file) + if(action == "saveAs") + dialog = list( + "type" = "confirm", + "title" = "Confirm Save As", + "message" = "[new_file_name] already exists. Do you want to overwrite this file?", + "action" = "overwrite", + "params" = list("uid" = uid, + "name" = new_file_name, + "onDisk" = saving_to_disk, + "typepath" = new_file_type), + ) + else + INVOKE_ASYNC(src, PROC_REF(write_to_file), user, existing_file) + return TRUE + INVOKE_ASYNC(src, PROC_REF(save_file), user, new_file_name, new_file_type, saving_to_disk && computer.inserted_disk) + return TRUE + +/datum/computer_file/program/nanopaint/proc/new_workspace(width, height) + current_workspace = new(width, height) + +/datum/computer_file/program/nanopaint/proc/open_file(mob/user, uid, on_disk, file_name, datum/computer_file/file_type) + var/datum/computer_file/file_being_opened + var/full_file_name = file_name + file_type::filetype + if(on_disk) + if(!computer.inserted_disk) + dialog = list("type" = "error", "message" = "[full_file_name] - The disk has been removed.") + return + if(uid) + file_being_opened = computer.find_file_by_uid(uid, computer.inserted_disk) + else + file_being_opened = computer.find_file_by_full_name(full_file_name, computer.inserted_disk) + else + if(uid) + file_being_opened = computer.find_file_by_uid(uid) + else + file_being_opened = computer.find_file_by_full_name(full_file_name) + if(!file_being_opened) + dialog = list("type" = "error", "message" = "[full_file_name] - The selected file could not be found") + return + var/base_supported_type = is_type_in_typecache(file_being_opened, GLOB.nanopaint_supported_filetypes) + if(!base_supported_type) + dialog = list("type" = "error", "message" = "[full_file_name] - Unsupported format") + return + close_workspace() + switch(base_supported_type) + if(/datum/computer_file/data/paint_project) + var/datum/computer_file/data/paint_project/project_file = file_being_opened + current_workspace = project_file.workspace.copy() + backing_file = WEAKREF(file_being_opened) + opened_file_name = project_file.filename + source_photo_or_painting = project_file.source_photo_or_painting + if(/datum/computer_file/image) + var/datum/computer_file/image/image_file = file_being_opened + var/icon/image = image_file.stored_icon + var/image_width = image.Width() + var/image_height = image.Height() + if(image_width <= 0 || image_height <= 0) + dialog = list("type" = "error", "message" = "[file_name] - Invalid dimensions") + return + if(image_width > SANE_PHOTO_EDITING_SIZE_LIMIT || image_height > SANE_PHOTO_EDITING_SIZE_LIMIT) + dialog = list("type" = "error", "message" = "[file_name] - Too large") + return + current_workspace = new(image_width, image_height) + fill_grid_from_icon(current_workspace.get_first_layer_pixel_data(), image) + source_photo_or_painting = image_file.source_photo_or_painting + opened_file_type = base_supported_type + +/datum/computer_file/program/nanopaint/proc/write_to_file(mob/user, datum/computer_file/file) + switch(file.type) + if(/datum/computer_file/data/paint_project) + var/datum/computer_file/data/paint_project/project_file = file + project_file.workspace = current_workspace.copy() + project_file.set_source(source_photo_or_painting) + backing_file = WEAKREF(project_file) + if(/datum/computer_file/image) + var/datum/computer_file/image/image_file = file + image_file.stored_icon = current_workspace.to_icon() + image_file.image_name = null + image_file.assign_path() + image_file.ref_appearance = null + image_file.assign_ref_appearance() + image_file.set_source(source_photo_or_painting) + if(!source_photo_or_painting) + image_file.author_ckey = user.ckey + message_admins("[ADMIN_LOOKUP(user)] has saved a custom image to [computer] as [file.filename].[file.filetype].") + log_player_image_creation("[key_name(user)] has saved a custom image to [computer] as [file.filename].[file.filetype]", user, image_file.stored_icon) + +/datum/computer_file/program/nanopaint/proc/save_file(mob/user, name, file_type, obj/item/disk/computer/target_disk) + var/datum/computer_file/file = new file_type() + file.filename = reject_bad_name(name, allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE) + var/file_stored + if(target_disk) + file_stored = target_disk.add_file(file) + else + file_stored = computer.store_file(file) + if(file_stored) + write_to_file(user, file) + else + dialog = list("type" = "error", "message" = "[name] - Unable to save file") + SStgui.update_uis(computer) + +/datum/computer_file/program/nanopaint/proc/close_workspace() + backing_file = null + opened_file_name = null + opened_file_type = null + current_workspace = null + source_photo_or_painting = null + source_on_undo_all = null + palette = list() + current_color = "#ffffffff" + +/datum/computer_file/program/nanopaint/kill_program(mob/user) + close_workspace() + return ..() + +#undef SANE_PHOTO_EDITING_SIZE_LIMIT +#undef PALETTE_SIZE diff --git a/code/modules/modular_computers/file_system/programs/portrait_printer.dm b/code/modules/modular_computers/file_system/programs/portrait_printer.dm index 224e83bb8fd..fec09828f54 100644 --- a/code/modules/modular_computers/file_system/programs/portrait_printer.dm +++ b/code/modules/modular_computers/file_system/programs/portrait_printer.dm @@ -1,8 +1,3 @@ - -///how much paper it takes from the printer to create a canvas. -#define CANVAS_PAPER_COST 10 - - /** * ## the art gallery viewer/printer! * @@ -57,8 +52,10 @@ search_mode = search_mode == PAINTINGS_FILTER_SEARCH_TITLE ? PAINTINGS_FILTER_SEARCH_CREATOR : PAINTINGS_FILTER_SEARCH_TITLE generate_matching_paintings_list() . = TRUE - if("select") + if("print") print_painting(params["selected"]) + if("download") + download_painting(params["selected"]) /datum/computer_file/program/portrait_printer/proc/generate_matching_paintings_list() matching_paintings = null @@ -72,34 +69,23 @@ if(computer.stored_paper < CANVAS_PAPER_COST) to_chat(usr, span_notice("Printing error: Your printer needs at least [CANVAS_PAPER_COST] paper to print a canvas.")) return - computer.stored_paper -= CANVAS_PAPER_COST //canvas printing! var/datum/painting/chosen_portrait = locate(selected_painting) in SSpersistent_paintings.paintings - var/png = "data/paintings/images/[chosen_portrait.md5].png" - var/icon/art_icon = new(png) - var/obj/item/canvas/printed_canvas - var/art_width = art_icon.Width() - var/art_height = art_icon.Height() - for(var/canvas_type in typesof(/obj/item/canvas)) - printed_canvas = canvas_type - if(initial(printed_canvas.width) == art_width && initial(printed_canvas.height) == art_height) - printed_canvas = new canvas_type(get_turf(computer.physical)) - break - printed_canvas = null - if(!printed_canvas) + var/obj/item/canvas/new_canvas = chosen_portrait.spawn_canvas(get_turf(computer.physical)) + if(!new_canvas) + to_chat(usr, span_notice("Printing error: An unknown error has occurred.")) return - printed_canvas.painting_metadata = chosen_portrait - printed_canvas.fill_grid_from_icon(art_icon) - printed_canvas.generated_icon = art_icon - printed_canvas.icon_generated = TRUE - printed_canvas.finalized = TRUE - printed_canvas.name = "painting - [chosen_portrait.title]" - ///this is a copy of something that is already in the database- it should not be able to be saved. - printed_canvas.no_save = TRUE - printed_canvas.update_icon() + computer.stored_paper -= CANVAS_PAPER_COST to_chat(usr, span_notice("You have printed [chosen_portrait.title] onto a new canvas.")) - playsound(computer.physical, 'sound/items/poster/poster_being_created.ogg', 100, TRUE) + playsound(computer.physical, 'sound/machines/printer.ogg', 100, TRUE) -#undef CANVAS_PAPER_COST +/datum/computer_file/program/portrait_printer/proc/download_painting(selected_painting) + var/datum/painting/chosen_portrait = locate(selected_painting) in SSpersistent_paintings.paintings + var/icon/portrait_icon = chosen_portrait.get_icon() + var/datum/computer_file/image/image_file = new(portrait_icon, display_name = chosen_portrait.title, source_photo_or_painting = chosen_portrait) + if(!computer.store_file(image_file, usr)) + to_chat(usr, span_notice("Unable to download [chosen_portrait.title].[/datum/computer_file/image::filetype].")) + return + to_chat(usr, span_notice("Downloaded [chosen_portrait.title].[/datum/computer_file/image::filetype].")) diff --git a/code/modules/modular_computers/file_system/programs/virtual_pet.dm b/code/modules/modular_computers/file_system/programs/virtual_pet.dm index 3705c5d92a9..f36a4e407e6 100644 --- a/code/modules/modular_computers/file_system/programs/virtual_pet.dm +++ b/code/modules/modular_computers/file_system/programs/virtual_pet.dm @@ -195,7 +195,7 @@ GLOBAL_LIST_EMPTY(virtual_pets_list) if(isnull(photo)) return - computer.store_file(new /datum/computer_file/picture(photo)) + computer.store_file(new /datum/computer_file/image(photo.picture_image, display_name = photo.picture_name)) /datum/computer_file/program/virtual_pet/proc/set_hat_offsets(new_dir) var/direction_text = dir2text(new_dir) diff --git a/code/modules/photography/_pictures.dm b/code/modules/photography/_pictures.dm index 3a6aa1ffc64..a59435f5549 100644 --- a/code/modules/photography/_pictures.dm +++ b/code/modules/photography/_pictures.dm @@ -17,8 +17,10 @@ var/id //this var is NOT protected because the worst you can do with this that you couldn't do otherwise is overwrite photos, and photos aren't going to be used as attack logs/investigations anytime soon. ///Was this image capable of seeing ghosts? var/see_ghosts = CAMERA_NO_GHOSTS + ///The ckey of the player who printed this photo from an edited computer image. + var/author_ckey -/datum/picture/New(name, desc, mobs_spotted, dead_spotted, names, image, icon, size_x, size_y, bp, caption_, autogenerate_icon, can_see_ghosts) +/datum/picture/New(name, desc, mobs_spotted, dead_spotted, names, image, icon, size_x, size_y, bp, caption_, autogenerate_icon, can_see_ghosts, author_ckey_) if(!isnull(name)) picture_name = name if(!isnull(desc)) @@ -48,6 +50,8 @@ regenerate_small_icon() if(can_see_ghosts) see_ghosts = can_see_ghosts + if(!isnull(author_ckey)) + author_ckey = author_ckey /datum/picture/proc/get_small_icon(iconstate) if(!picture_icon) @@ -73,13 +77,21 @@ .["pixel_size_x"] = psize_x .["pixel_size_y"] = psize_y .["logpath"] = logpath + .["author"] = author_ckey - SET_SERIALIZATION_SEMVER(semvers, "1.0.0") + SET_SERIALIZATION_SEMVER(semvers, "1.0.1") return . /datum/picture/deserialize_list(list/input, list/options) - if((SCHEMA_VERSION in options) && (options[SCHEMA_VERSION] != "1.0.0")) - CRASH("Invalid schema version for datum/picture: [options[SCHEMA_VERSION]] (expected 1.0.0)") + if((SCHEMA_VERSION in options)) + switch(options[SCHEMA_VERSION]) + if("1.0.0") + author_ckey = null + if("1.0.1") + if(input["author"]) + author_ckey = input["author"] + else + CRASH("Invalid schema version for datum/picture: [options[SCHEMA_VERSION]] (expected \[1.0.0, 1.0.1\])") . = ..() if(!.) return . diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm index 1727c04de32..5cf55f99bfe 100644 --- a/code/modules/photography/camera/camera.dm +++ b/code/modules/photography/camera/camera.dm @@ -1,6 +1,3 @@ - -#define CAMERA_PICTURE_SIZE_HARD_LIMIT 21 - /obj/item/camera name = "camera" icon = 'icons/obj/art/camera.dmi' @@ -424,5 +421,3 @@ if(!target) return INVOKE_ASYNC(camera, TYPE_PROC_REF(/obj/item/camera, attempt_picture), target) - -#undef CAMERA_PICTURE_SIZE_HARD_LIMIT diff --git a/code/modules/sprite_editing/workspace.dm b/code/modules/sprite_editing/workspace.dm index 9871c342fd1..d5d752ec6c3 100644 --- a/code/modules/sprite_editing/workspace.dm +++ b/code/modules/sprite_editing/workspace.dm @@ -202,7 +202,12 @@ bottom_layer["[dir]"][y][x] = blend_color(bottom_layer["[dir]"][y][x], top_layer["[dir]"][y][x]) layers.Cut(layer, layer+1) if("addLayer") - layers += list(list("name" = "New Layer", "visible" = TRUE, "data" = create_layer_data())) + var/layer_name = "New Layer" + var/dupe_count = 1 + for(var/list/layer in layers) + if(layer["name"] == layer_name) + layer_name = "New Layer [++dupe_count]" + layers += list(list("name" = layer_name, "visible" = TRUE, "data" = create_layer_data())) if("deleteLayer") var/layer = transaction["layer"] layers.Cut(layer, layer+1) diff --git a/config/logging.txt b/config/logging.txt index 09728bd8fb2..cd9b38b6203 100644 --- a/config/logging.txt +++ b/config/logging.txt @@ -107,3 +107,6 @@ LOG_WORLD_TOPIC ## log manual target zone switching LOG_ZONE_SWITCH + +## log player image creation +LOG_IMAGE diff --git a/tgstation.dme b/tgstation.dme index 093b2c25dd0..e12dffe4356 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -65,6 +65,7 @@ #include "code\__DEFINES\bodyparts.dm" #include "code\__DEFINES\botany.dm" #include "code\__DEFINES\callbacks.dm" +#include "code\__DEFINES\camera.dm" #include "code\__DEFINES\cameranets.dm" #include "code\__DEFINES\cargo.dm" #include "code\__DEFINES\chat.dm" @@ -525,6 +526,7 @@ #include "code\__HELPERS\logging\economy.dm" #include "code\__HELPERS\logging\fishing.dm" #include "code\__HELPERS\logging\game.dm" +#include "code\__HELPERS\logging\image.dm" #include "code\__HELPERS\logging\manifest.dm" #include "code\__HELPERS\logging\mecha.dm" #include "code\__HELPERS\logging\mob.dm" @@ -3248,6 +3250,7 @@ #include "code\modules\admin\verbs\server.dm" #include "code\modules\admin\verbs\spawnobjasmob.dm" #include "code\modules\admin\verbs\special_verbs.dm" +#include "code\modules\admin\verbs\sprite_auditor.dm" #include "code\modules\admin\verbs\lua\_wrappers.dm" #include "code\modules\admin\verbs\lua\helpers.dm" #include "code\modules\admin\verbs\lua\lua_editor.dm" @@ -5726,7 +5729,7 @@ #include "code\modules\modular_computers\computers\machinery\modular_computer.dm" #include "code\modules\modular_computers\file_system\computer_file.dm" #include "code\modules\modular_computers\file_system\data.dm" -#include "code\modules\modular_computers\file_system\picture_file.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" @@ -5748,6 +5751,7 @@ #include "code\modules\modular_computers\file_system\programs\frontier.dm" #include "code\modules\modular_computers\file_system\programs\jobmanagement.dm" #include "code\modules\modular_computers\file_system\programs\mafia_ntos.dm" +#include "code\modules\modular_computers\file_system\programs\nanopaint.dm" #include "code\modules\modular_computers\file_system\programs\newscasterapp.dm" #include "code\modules\modular_computers\file_system\programs\notepad.dm" #include "code\modules\modular_computers\file_system\programs\nt_pay.dm" diff --git a/tgui/packages/tgui/interfaces/Canvas.tsx b/tgui/packages/tgui/interfaces/Canvas.tsx index e1c8777f46f..c51876997bd 100644 --- a/tgui/packages/tgui/interfaces/Canvas.tsx +++ b/tgui/packages/tgui/interfaces/Canvas.tsx @@ -71,25 +71,18 @@ const ZoomListener = ({ pixelsPerUnit, children, }: PropsWithChildren) => ( - <> - {/* I'm too lazy to go through the process of adding onWheel to BoxProps. */} -
ev.currentTarget.focus()} - onWheel={(ev) => { - if (!ev.shiftKey) return; - ev.preventDefault(); - setZoom( - clamp(zoom + (Math.sign(-ev.deltaY) * 1) / pixelsPerUnit, 1, 3), - ); - }} - style={{ - width: '100%', - height: '100%', - }} - > - {children} -
- + ev.currentTarget.focus()} + onWheel={(ev) => { + if (!ev.shiftKey) return; + ev.preventDefault(); + setZoom(clamp(zoom + (Math.sign(-ev.deltaY) * 1) / pixelsPerUnit, 1, 3)); + }} + > + {children} + ); type EditableCanvasProps = Pick< diff --git a/tgui/packages/tgui/interfaces/NtosCamera.jsx b/tgui/packages/tgui/interfaces/NtosCamera.jsx deleted file mode 100644 index ec60409d2ec..00000000000 --- a/tgui/packages/tgui/interfaces/NtosCamera.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Button, Image, NoticeBox, Stack } from 'tgui-core/components'; - -import { useBackend } from '../backend'; -import { NtosWindow } from '../layouts'; - -export const NtosCamera = (props) => { - return ( - - - - - - ); -}; - -export const NtosCameraContent = (props) => { - const { act, data } = useBackend(); - const { photo, paper_left } = data; - - if (!photo) { - return ( - - Phototrasen Images - Tap (right-click) with your tablet to snap a photo! - - ); - } - - return ( - - -