diff --git a/code/controllers/subsystem/persistent_paintings.dm b/code/controllers/subsystem/persistent_paintings.dm index ccf14a37269..f0a1fe83f98 100644 --- a/code/controllers/subsystem/persistent_paintings.dm +++ b/code/controllers/subsystem/persistent_paintings.dm @@ -1,3 +1,92 @@ +#define PAINTINGS_DATA_FORMAT_VERSION 1 +/* +{ + "version":1 + "paintings":[ + { + "md5":"2e117d9d372fb6823bd81d3542a419d6", //unique identifier + "creator_ckey" : "example", + "creator_name" : "example", + "creation_date" : "YYYY-MM-DD hh:mm:ss", + "creation_round_id": 222, + "title": "example title", + "tags": ["library","library_private"], + "patron_ckey" : "example", + "patron_name" : "example", + "credit_value" : 999, + "width" : 24, + "height" : 24, + "medium" : "Oil on canvas" + }, + ] +} +*/ + +/datum/painting + /// md5 of the png file, also the filename. + var/md5 + /// Title + var/title + /// Author's ckey + var/creator_ckey + /// Author's name + var/creator_name + /// Timestamp when painting was made (finalized ?) + var/creation_date + /// Round if when the painting was made + var/creation_round_id + /// List of this painting string tags if any + var/list/tags + /// Patron ckey + var/patron_ckey + /// Patron name + var/patron_name + /// Amount paid by last patron for this painting + var/credit_value = 0 + /// painting width + var/width + /// painting height + var/height + /// short painting medium description + var/medium + /// Was the painting loaded from json or created this round + var/loaded_from_json = FALSE + +/datum/painting/proc/load_from_json(list/json_data) + md5 = json_data["md5"] + title = json_data["title"] + creator_ckey = json_data["creator_ckey"] + creator_name = json_data["creator_name"] + creation_date = json_data["creation_date"] + creation_round_id = json_data["creation_round_id"] + tags = json_data["tags"] + patron_ckey = json_data["patron_ckey"] + credit_value = json_data["credit_value"] + width = json_data["width"] + height = json_data["height"] + medium = json_data["medium"] + loaded_from_json = TRUE + +/datum/painting/proc/to_json() + var/list/new_data = list() + new_data["md5"] = md5 + new_data["title"] = title + new_data["creator_ckey"] = creator_ckey + new_data["creator_name"] = creator_name + new_data["creation_date"] = creation_date + new_data["creation_round_id"] = creation_round_id + new_data["tags"] = tags + new_data["patron_ckey"] = patron_ckey + new_data["patron_name"] = patron_name + new_data["credit_value"] = credit_value + new_data["width"] = width + new_data["height"] = height + new_data["medium"] = medium + return new_data + +/// Only returns paintings with 23x23 or 24x24 sizes fitting AI display icon. +#define PAINTINGS_FILTER_AI_PORTRAIT 1 + SUBSYSTEM_DEF(persistent_paintings) name = "Persistent Paintings" init_order = INIT_ORDER_PERSISTENT_PAINTINGS @@ -6,24 +95,111 @@ SUBSYSTEM_DEF(persistent_paintings) /// A list of painting frames that this controls var/list/obj/structure/sign/painting/painting_frames = list() - /// A map of identifiers (such as library) to paintings from paintings.json + /// A list of /datum/paintings saved or ready to be saved this round. var/list/paintings = list() /datum/controller/subsystem/persistent_paintings/Initialize(start_timeofday) var/json_file = file("data/paintings.json") if(fexists(json_file)) - paintings = json_decode(file2text(json_file)) + var/list/raw_data = update_format(json_decode(file2text(json_file))) + for(var/list/painting_data as anything in raw_data["paintings"]) + var/datum/painting/loaded_painting = new + loaded_painting.load_from_json(painting_data) + paintings += loaded_painting for(var/obj/structure/sign/painting/painting_frame as anything in painting_frames) painting_frame.load_persistent() return ..() +/// Generates painting data ready to be consumed by ui +/datum/controller/subsystem/persistent_paintings/proc/painting_ui_data(filter=NONE,admin=FALSE) + . = list() + for(var/datum/painting/painting as anything in paintings) + if(filter & PAINTINGS_FILTER_AI_PORTRAIT && ((painting.width != 24 && painting.width != 23) || (painting.height != 24 && painting.height != 23))) + continue + if(admin) + var/list/pdata = painting.to_json() + pdata["ref"] = REF(painting) + . += list(pdata) + else + . += list(list("title" = painting.title,"md5" = painting.md5,"ref" = REF(painting))) + +/// Returns paintings with given tag. +/datum/controller/subsystem/persistent_paintings/proc/get_paintings_with_tag(tag_name) + . = list() + for(var/datum/painting/painting as anything in paintings) + if(!painting.tags || !(tag_name in painting.tags)) + continue + . += painting + +/// Updates paintings data format to latest if necessary +/datum/controller/subsystem/persistent_paintings/proc/update_format(current_data) + if(current_data["version"] && current_data["version"] == PAINTINGS_DATA_FORMAT_VERSION) + return current_data + + var/current_format = current_data["version"] || 0 + switch(current_format) + if(0) + fcopy("data/paintings.json","data/paintings_migration_backup_0.json") //Better safe than losing all metadata + var/list/result = list() + result["version"] = 1 + var/list/data = list() + // Squash categories into tags + for(var/category in current_data) + for(var/old_data in current_data[category]) + var/duplicate_found = FALSE + for(var/list/entry in data) + if(entry["md5"] == old_data["md5"]) + entry["tags"] |= category + duplicate_found = TRUE + break + if(duplicate_found) + continue + var/old_png_path = "data/paintings/[category]/[old_data["md5"]].png" + var/new_png_path = "data/paintings/images/[old_data["md5"]].png" + fcopy(old_png_path,new_png_path) + fdel(old_png_path) + var/icon/painting_icon = new(new_png_path) + var/width = painting_icon.Width() + var/height = painting_icon.Height() + var/list/new_data = list() + new_data["md5"] = old_data["md5"] + new_data["title"] = old_data["title"] || "Untitled Artwork" + new_data["creator_ckey"] = old_data["ckey"] || "" + new_data["creator_name"] = "Anonymous" + new_data["creation_date"] = time2text(world.realtime) // Could use creation/modified file helpers in rustg + new_data["creation_round_id"] = GLOB.round_id + new_data["tags"] = list(category,"Migrated from version 0") + new_data["patron_ckey"] = "" + new_data["patron_name"] = "" + new_data["credit_value"] = 0 + new_data["width"] = width + new_data["height"] = height + new_data["medium"] = "Spraypaint on canvas" //Let's go with most common tool. + data += list(new_data) + result["paintings"] = data + //We're going to save this immidiately this is non-recoverable operation + var/json_file = file("data/paintings.json") + fdel(json_file) + WRITE_FILE(json_file, json_encode(result)) + return update_format(result) + /// Saves all persistent paintings /datum/controller/subsystem/persistent_paintings/proc/save_paintings() + // Collect new painting data for(var/obj/structure/sign/painting/painting_frame as anything in painting_frames) painting_frame.save_persistent() + save_to_file() + +/// Saves all currently tracked painting data to file +/datum/controller/subsystem/persistent_paintings/proc/save_to_file() var/json_file = file("data/paintings.json") fdel(json_file) - WRITE_FILE(json_file, json_encode(paintings)) + var/list/all_data = list("version" = PAINTINGS_DATA_FORMAT_VERSION) + var/list/painting_data = list() + for(var/datum/painting/painting as anything in paintings) + painting_data += list(painting.to_json()) + all_data["paintings"] = painting_data + WRITE_FILE(json_file, json_encode(all_data)) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index f9d7d3c9e3f..79c231ec5de 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -76,6 +76,7 @@ GLOBAL_PROTECT(admin_verbs_admin) /datum/admins/proc/view_all_circuits, /datum/admins/proc/view_all_sdql_spells, /datum/admins/proc/known_alts_panel, + /datum/admins/proc/paintings_manager, ) GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/ban_panel, /client/proc/stickybanpanel)) GLOBAL_PROTECT(admin_verbs_ban) diff --git a/code/modules/admin/painting_manager.dm b/code/modules/admin/painting_manager.dm new file mode 100644 index 00000000000..eba5cb61a52 --- /dev/null +++ b/code/modules/admin/painting_manager.dm @@ -0,0 +1,96 @@ +/datum/admins/proc/paintings_manager() + set name = "Paintings Manager" + set category = "Admin" + + if(!check_rights(R_ADMIN)) + return + var/datum/paintings_manager/ui = new(usr) + ui.ui_interact(usr) + +/// Painting Admin Management Panel +/datum/paintings_manager + +/datum/paintings_manager/ui_state(mob/user) + return GLOB.admin_state + +/datum/paintings_manager/ui_close(mob/user) + qdel(src) + +/datum/paintings_manager/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PaintingAdminPanel") + ui.open() + +/datum/paintings_manager/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/simple/portraits) + ) + +/datum/paintings_manager/ui_data(mob/user) + . = list() + .["paintings"] = SSpersistent_paintings.painting_ui_data(filter = NONE, admin = TRUE) + +/datum/paintings_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return + if (!check_rights(R_ADMIN)) + return + var/mob/user = usr + var/datum/painting/chosen_painting = locate(params["ref"]) in SSpersistent_paintings.paintings + if(!chosen_painting) + return + switch(action) + if("delete") + //Delete the png file + var/png = "data/paintings/images/[chosen_painting.md5].png" + fdel(png) + //Remove entry from paintings list + SSpersistent_paintings.paintings -= chosen_painting + SSpersistent_paintings.save_to_file() // Save now so we don't have broken variations if this round crashes + //Delete any painting instances in the current round + for(var/obj/structure/sign/painting/painting as anything in SSpersistent_paintings.painting_frames) + if(painting.current_canvas && painting.current_canvas.painting_metadata == chosen_painting) + QDEL_NULL(painting.current_canvas) + painting.update_appearance() + log_admin("[key_name(user)] has deleted a persistent painting made by [chosen_painting.creator_ckey].") + message_admins(span_notice("[key_name_admin(user)] has deleted persistent painting made by [chosen_painting.creator_ckey].")) + return TRUE + if("rename") + //Modify the metadata + var/old_title = chosen_painting.title + var/new_title = stripped_input(user, "New painting title?", "Painting rename", chosen_painting.title) + if(!new_title) + return + chosen_painting.title = new_title + log_admin("[key_name(user)] has renamed a persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5] from [old_title] to [chosen_painting.title].") + return TRUE + if("rename_author") + var/old_name = chosen_painting.creator_name + var/new_name = stripped_input(user, "New painting author name?", "Painting rename", chosen_painting.creator_name) + if(!new_name) + return + chosen_painting.creator_name = new_name + log_admin("[key_name(user)] has renamed a persistent painting author made by [chosen_painting.creator_name] with id [chosen_painting.md5] from [old_name] to [chosen_painting.creator_name].") + return TRUE + if("dumpit") + //Modify the metadata + chosen_painting.patron_name = "" + chosen_painting.patron_ckey = "" + chosen_painting.credit_value = 0 + log_admin("[key_name(user)] has reset patronage data on a persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5].") + return TRUE + if("remove_tag") + if(chosen_painting.tags) + chosen_painting.tags -= params["tag"] + log_admin("[key_name(user)] has removed tag [params["tag"]] from persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5].") + return TRUE + if("add_tag") + var/tag_name = stripped_input(user, "New tag name?", "???") + if(!tag_name) + return + if(!chosen_painting.tags) + chosen_painting.tags = list() + chosen_painting.tags |= tag_name + log_admin("[key_name(user)] has added tag [tag_name] to persistent painting made by [chosen_painting.creator_ckey] with id [chosen_painting.md5].") + return TRUE diff --git a/code/modules/art/paintings.dm b/code/modules/art/paintings.dm index e77e64271c8..123799afaaf 100644 --- a/code/modules/art/paintings.dm +++ b/code/modules/art/paintings.dm @@ -45,16 +45,18 @@ var/width = 11 var/height = 11 var/list/grid - var/canvas_color = "#ffffff" //empty canvas color + /// empty canvas color + var/canvas_color = "#ffffff" + /// Is it clean canvas or was there something painted on it at some point, used to decide when to show wip splotch overlay var/used = FALSE - var/painting_name = "Untitled Artwork" //Painting name, this is set after framing. var/finalized = FALSE //Blocks edits - var/author_ckey var/icon_generated = FALSE var/icon/generated_icon ///boolean that blocks persistence from saving it. enabled from printing copies, because we do not want to save copies. var/no_save = FALSE + var/datum/painting/painting_metadata + // Painting overlay offset when framed var/framed_offset_x = 11 var/framed_offset_y = 10 @@ -66,6 +68,12 @@ . = ..() reset_grid() + painting_metadata = new + painting_metadata.title = "Untitled Artwork" + painting_metadata.creation_round_id = GLOB.round_id + painting_metadata.width = width + painting_metadata.height = height + /obj/item/canvas/proc/reset_grid() grid = new/list(width,height) for(var/x in 1 to width) @@ -86,7 +94,6 @@ ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, "Canvas", name) - ui.set_autoupdate(FALSE) ui.open() /obj/item/canvas/attackby(obj/item/I, mob/living/user, params) @@ -98,8 +105,15 @@ /obj/item/canvas/ui_data(mob/user) . = ..() .["grid"] = grid - .["name"] = painting_name + .["name"] = painting_metadata.title + .["author"] = painting_metadata.creator_name + .["patron"] = painting_metadata.patron_name + .["medium"] = painting_metadata.medium + .["date"] = painting_metadata.creation_date .["finalized"] = finalized + .["editable"] = !finalized //Ideally you should be able to draw moustaches on existing paintings in the gallery but that's not implemented yet + .["show_plaque"] = istype(loc,/obj/structure/sign/painting) + .["paint_tool_color"] = get_paint_tool_color(user.get_active_held_item()) /obj/item/canvas/examine(mob/user) . = ..() @@ -107,32 +121,79 @@ /obj/item/canvas/ui_act(action, params) . = ..() - if(. || finalized) + if(.) return var/mob/user = usr switch(action) if("paint") + if(finalized) + return TRUE var/obj/item/I = user.get_active_held_item() - var/color = get_paint_tool_color(I) - if(!color) + var/tool_color = get_paint_tool_color(I) + if(!tool_color) return FALSE - var/x = text2num(params["x"]) - var/y = text2num(params["y"]) - grid[x][y] = color + var/list/data = params["data"] + //could maybe validate continuity but eh + for(var/point in data) + var/x = text2num(point["x"]) + var/y = text2num(point["y"]) + grid[x][y] = tool_color + var/medium = get_paint_tool_medium(I) + if(medium && painting_metadata.medium && painting_metadata.medium != medium) + painting_metadata.medium = "Mixed medium" + else + painting_metadata.medium = medium used = TRUE update_appearance() . = TRUE if("finalize") . = TRUE - if(!finalized) - finalize(user) + finalize(user) + if("patronage") + . = TRUE + patron(user) /obj/item/canvas/proc/finalize(mob/user) + if(painting_metadata.loaded_from_json || finalized) + return finalized = TRUE - author_ckey = user.ckey + painting_metadata.creator_ckey = user.ckey + painting_metadata.creator_name = user.real_name + painting_metadata.creation_date = time2text(world.realtime) + painting_metadata.creation_round_id = GLOB.round_id generate_proper_overlay() try_rename(user) +/obj/item/canvas/proc/patron(mob/user) + if(!finalized || !painting_metadata.loaded_from_json || !isliving(user)) + return + var/mob/living/living_user = user + var/obj/item/card/id/id_card = living_user.get_idcard(TRUE) + if(!id_card) + to_chat(user,span_notice("You don't even have a id and you want to be an art patron?")) + return + if(!id_card.registered_account || !id_card.registered_account.account_job) + to_chat(user,span_notice("No valid non-departamental account found.")) + return + var/datum/bank_account/account = id_card.registered_account + if(account.account_balance < painting_metadata.credit_value) + to_chat(user,span_notice("You can't afford this.")) + return + var/sniped_amount = painting_metadata.credit_value + var/offer_amount = input(user,"How much do you want to offer ? Minimum : [painting_metadata.credit_value]","Patronage Amount", painting_metadata.credit_value + 1) as num|null + if(account.account_balance < offer_amount) + to_chat(user,span_notice("You can't afford this.")) + return + if(!offer_amount || sniped_amount != painting_metadata.credit_value || offer_amount < painting_metadata.credit_value+1 || !user.canUseTopic(src)) + return + if(!account.adjust_money(-offer_amount)) + to_chat(user,span_warning("Transaction failure. Please try again.")) + return + painting_metadata.patron_ckey = user.ckey + painting_metadata.patron_name = user.real_name + painting_metadata.credit_value = offer_amount + to_chat(user,span_notice("Nanotrasen Trust Foundation thanks you for your contribution. You're now offical patron of this painting.")) + /obj/item/canvas/update_overlays() . = ..() if(icon_generated) @@ -153,9 +214,11 @@ if(icon_generated) return var/png_filename = "data/paintings/temp_painting.png" - var/result = rustg_dmi_create_png(png_filename,"[width]","[height]",get_data_string()) + var/image_data = get_data_string() + var/result = rustg_dmi_create_png(png_filename, "[width]", "[height]", image_data) if(result) CRASH("Error generating painting png : [result]") + painting_metadata.md5 = md5(lowertext(image_data)) generated_icon = new(png_filename) icon_generated = TRUE update_appearance() @@ -168,14 +231,17 @@ return data.Join("") //Todo make this element ? -/obj/item/canvas/proc/get_paint_tool_color(obj/item/I) - if(!I) +/obj/item/canvas/proc/get_paint_tool_color(obj/item/painting_implement) + if(!painting_implement) return - if(istype(I, /obj/item/toy/crayon)) - var/obj/item/toy/crayon/crayon = I + if(istype(painting_implement, /obj/item/paint_palette)) + var/obj/item/paint_palette/palette = painting_implement + return palette.current_color + if(istype(painting_implement, /obj/item/toy/crayon)) + var/obj/item/toy/crayon/crayon = painting_implement return crayon.paint_color - else if(istype(I, /obj/item/pen)) - var/obj/item/pen/P = I + else if(istype(painting_implement, /obj/item/pen)) + var/obj/item/pen/P = painting_implement switch(P.colour) if("black") return "#000000" @@ -184,14 +250,37 @@ if("red") return "#ff0000" return P.colour - else if(istype(I, /obj/item/soap) || istype(I, /obj/item/reagent_containers/glass/rag)) + else if(istype(painting_implement, /obj/item/soap) || istype(painting_implement, /obj/item/reagent_containers/glass/rag)) return canvas_color +/// Generates medium description +/obj/item/canvas/proc/get_paint_tool_medium(obj/item/painting_implement) + if(!painting_implement) + return + if(istype(painting_implement, /obj/item/paint_palette)) + return "Oil on canvas" + else if(istype(painting_implement, /obj/item/toy/crayon/spraycan)) + return "Spraycan on canvas" + else if(istype(painting_implement, /obj/item/toy/crayon)) + return "Crayon on canvas" + else if(istype(painting_implement, /obj/item/pen)) + return "Ink on canvas" + else if(istype(painting_implement, /obj/item/soap) || istype(painting_implement, /obj/item/reagent_containers/glass/rag)) + return //These are just for cleaning, ignore them + else + return "Unknown medium" + /obj/item/canvas/proc/try_rename(mob/user) + if(painting_metadata.loaded_from_json) // No renaming old paintings + return var/new_name = stripped_input(user,"What do you want to name the painting?") - if(new_name != painting_name && new_name && user.canUseTopic(src,BE_CLOSE)) - painting_name = new_name - SStgui.update_uis(src) + if(new_name != painting_metadata.title && new_name && user.canUseTopic(src, BE_CLOSE)) + painting_metadata.title = new_name + var/sign_choice = tgui_alert(user, "Do you want to sign it or remain anonymous?", "Sign painting?", list("Yes", "No")) + if(sign_choice != "Yes") + painting_metadata.creator_name = "Anonymous" + SStgui.update_uis(src) + /obj/item/canvas/nineteen_nineteen name = "canvas (19x19)" @@ -271,7 +360,7 @@ /obj/structure/sign/painting/attackby(obj/item/I, mob/user, params) if(!current_canvas && istype(I, /obj/item/canvas)) frame_canvas(user,I) - else if(current_canvas && current_canvas.painting_name == initial(current_canvas.painting_name) && istype(I,/obj/item/pen)) + else if(current_canvas && current_canvas.painting_metadata.title == initial(current_canvas.painting_metadata.title) && istype(I,/obj/item/pen)) try_rename(user) else return ..() @@ -302,11 +391,11 @@ update_appearance() /obj/structure/sign/painting/proc/try_rename(mob/user) - if(current_canvas.painting_name == initial(current_canvas.painting_name)) + if(current_canvas.painting_metadata.title == initial(current_canvas.painting_metadata.title)) current_canvas.try_rename(user) /obj/structure/sign/painting/update_name(updates) - name = current_canvas ? "painting - [current_canvas.painting_name]" : initial(name) + name = current_canvas ? "painting - [current_canvas.painting_metadata.title]" : initial(name) return ..() /obj/structure/sign/painting/update_desc(updates) @@ -337,24 +426,13 @@ * Deleting paintings leaves their json, so this proc will remove the json and try again if it finds one of those. */ /obj/structure/sign/painting/proc/load_persistent() - if(!persistence_id || !SSpersistent_paintings.paintings[persistence_id]) + if(!persistence_id) return - var/list/painting_category = SSpersistent_paintings.paintings[persistence_id] - var/list/painting - while(!painting) - if(!length(SSpersistent_paintings.paintings[persistence_id])) - return //aborts loading anything this category has no usable paintings - var/list/chosen = pick(painting_category) - if(!fexists("data/paintings/[persistence_id]/[chosen["md5"]].png")) //shitmin deleted this art, lets remove json entry to avoid errors - painting_category -= list(chosen) - continue //and try again - painting = chosen - var/title = painting["title"] - var/author = painting["ckey"] - var/png = "data/paintings/[persistence_id]/[painting["md5"]].png" - if(!title) - title = "Untitled Artwork" //legacy artwork allowed null names which was bad for the json, lets fix that - painting["title"] = title + var/list/valid_paintings = SSpersistent_paintings.get_paintings_with_tag(persistence_id) + if(!length(valid_paintings)) + return //aborts loading anything this category has no usable paintings + var/datum/painting/painting = pick(valid_paintings) + var/png = "data/paintings/images/[painting.md5].png" var/icon/I = new(png) var/obj/item/canvas/new_canvas var/w = I.Width() @@ -364,39 +442,43 @@ if(initial(new_canvas.width) == w && initial(new_canvas.height) == h) new_canvas = new T(src) break + if(!istype(new_canvas)) + CRASH("Found painting size with no matching canvas type") + new_canvas.painting_metadata = painting new_canvas.fill_grid_from_icon(I) new_canvas.generated_icon = I new_canvas.icon_generated = TRUE new_canvas.finalized = TRUE - new_canvas.painting_name = title - new_canvas.author_ckey = author - new_canvas.name = "painting - [title]" + new_canvas.name = "painting - [painting.title]" current_canvas = new_canvas + current_canvas.update_appearance() update_appearance() /obj/structure/sign/painting/proc/save_persistent() - if(!persistence_id || !current_canvas || current_canvas.no_save) + if(!persistence_id || !current_canvas || current_canvas.no_save || current_canvas.painting_metadata.loaded_from_json) return if(SANITIZE_FILENAME(persistence_id) != persistence_id) stack_trace("Invalid persistence_id - [persistence_id]") return - if(!current_canvas.painting_name) - current_canvas.painting_name = "Untitled Artwork" var/data = current_canvas.get_data_string() var/md5 = md5(lowertext(data)) var/list/current = SSpersistent_paintings.paintings[persistence_id] if(!current) current = list() - for(var/list/entry in current) - if(entry["md5"] == md5) + for(var/datum/painting/entry in SSpersistent_paintings.paintings) + if(entry.md5 == md5) // No duplicates return - var/png_directory = "data/paintings/[persistence_id]/" + current_canvas.painting_metadata.md5 = md5 + if(!current_canvas.painting_metadata.tags) + current_canvas.painting_metadata.tags = list(persistence_id) + else + current_canvas.painting_metadata.tags |= persistence_id + var/png_directory = "data/paintings/images/" var/png_path = png_directory + "[md5].png" var/result = rustg_dmi_create_png(png_path,"[current_canvas.width]","[current_canvas.height]",data) if(result) CRASH("Error saving persistent painting: [result]") - current += list(list("title" = current_canvas.painting_name , "md5" = md5, "ckey" = current_canvas.author_ckey)) - SSpersistent_paintings.paintings[persistence_id] = current + SSpersistent_paintings.paintings += current_canvas.painting_metadata /obj/item/canvas/proc/fill_grid_from_icon(icon/I) var/h = I.Height() + 1 @@ -423,31 +505,20 @@ desc_with_canvas = "A painting hung away from lesser minds." persistence_id = "library_private" -/obj/structure/sign/painting/vv_get_dropdown() - . = ..() - VV_DROPDOWN_OPTION(VV_HK_REMOVE_PAINTING, "Remove Persistent Painting") +/// Simple painting utility. +/obj/item/paint_palette + name = "paint palette" + desc = "paintbrush included" + icon = 'icons/obj/artstuff.dmi' + icon_state = "palette" + lefthand_file = 'icons/mob/inhands/equipment/palette_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/palette_righthand.dmi' + w_class = WEIGHT_CLASS_TINY + ///Chosen paint color + var/current_color -/obj/structure/sign/painting/vv_do_topic(list/href_list) +/obj/item/paint_palette/attack_self(mob/user, modifiers) . = ..() - if(href_list[VV_HK_REMOVE_PAINTING]) - if(!check_rights(NONE)) - return - var/mob/user = usr - if(!persistence_id || !current_canvas) - to_chat(user,span_warning("This is not a persistent painting.")) - return - var/md5 = md5(lowertext(current_canvas.get_data_string())) - var/author = current_canvas.author_ckey - var/list/current = SSpersistent_paintings.paintings[persistence_id] - if(current) - for(var/list/entry in current) - if(entry["md5"] == md5) - current -= entry - var/png = "data/paintings/[persistence_id]/[md5].png" - fdel(png) - for(var/obj/structure/sign/painting/painting as anything in SSpersistent_paintings.painting_frames) - if(painting.current_canvas && md5(painting.current_canvas.get_data_string()) == md5) - QDEL_NULL(painting.current_canvas) - painting.update_appearance() - log_admin("[key_name(user)] has deleted a persistent painting made by [author].") - message_admins(span_notice("[key_name_admin(user)] has deleted persistent painting made by [author].")) + var/chosen_color = input(user,"Pick new color","Palette") as color|null + if(chosen_color) + current_color = chosen_color diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 532a6216392..4e316998596 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -442,28 +442,18 @@ ..() /datum/asset/simple/portraits - var/tab = "use subtypes of this please" assets = list() /datum/asset/simple/portraits/New() - if(!length(SSpersistent_paintings.paintings[tab])) + if(!length(SSpersistent_paintings.paintings)) return - for(var/list/portrait as anything in SSpersistent_paintings.paintings[tab]) - var/png = "data/paintings/[tab]/[portrait["md5"]].png" + for(var/datum/painting/portrait as anything in SSpersistent_paintings.paintings) + var/png = "data/paintings/images/[portrait.md5].png" if(fexists(png)) - var/asset_name = "[tab]_[portrait["md5"]]" + var/asset_name = "paintings_[portrait.md5]" assets[asset_name] = png ..() //this is where it registers all these assets we added to the list -/datum/asset/simple/portraits/library - tab = "library" - -/datum/asset/simple/portraits/library_secure - tab = "library_secure" - -/datum/asset/simple/portraits/library_private - tab = "library_private" - /datum/asset/simple/safe assets = list( "safe_dial.png" = 'icons/ui_icons/safe/safe_dial.png' diff --git a/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm b/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm index d792ff82bb0..2b7912921ae 100644 --- a/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm +++ b/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm @@ -30,16 +30,12 @@ /datum/portrait_picker/ui_assets(mob/user) return list( - get_asset_datum(/datum/asset/simple/portraits/library), - get_asset_datum(/datum/asset/simple/portraits/library_secure), - get_asset_datum(/datum/asset/simple/portraits/library_private) + get_asset_datum(/datum/asset/simple/portraits) ) /datum/portrait_picker/ui_data(mob/user) var/list/data = list() - data["library"] = SSpersistent_paintings.paintings["library"] ? SSpersistent_paintings.paintings["library"] : 0 - data["library_secure"] = SSpersistent_paintings.paintings["library_secure"] ? SSpersistent_paintings.paintings["library_secure"] : 0 - data["library_private"] = SSpersistent_paintings.paintings["library_private"] ? SSpersistent_paintings.paintings["library_private"] : 0 //i'm gonna regret this, won't i? + data["paintings"] = SSpersistent_paintings.painting_ui_data(filter = PAINTINGS_FILTER_AI_PORTRAIT) return data /datum/portrait_picker/ui_act(action, params) @@ -48,11 +44,13 @@ return switch(action) if("select") - var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private") - var/folder = tab2key[params["tab"]] - var/list/current_list = SSpersistent_paintings.paintings[folder] - var/list/chosen_portrait = current_list[params["selected"]] - var/png = "data/paintings/[folder]/[chosen_portrait["md5"]].png" + //var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private") + //var/folder = tab2key[params["tab"]] + //var/list/current_list = SSpersistent_paintings.paintings[folder] + var/datum/painting/chosen_portrait = locate(params["selected"]) in SSpersistent_paintings.paintings + if(!chosen_portrait) + return + var/png = "data/paintings/images/[chosen_portrait.md5].png" var/icon/portrait_icon = new(png) var/mob/living/ai = holder.mob var/w = portrait_icon.Width() 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 69d40a363f9..1260b761ac2 100644 --- a/code/modules/modular_computers/file_system/programs/portrait_printer.dm +++ b/code/modules/modular_computers/file_system/programs/portrait_printer.dm @@ -23,16 +23,12 @@ /datum/computer_file/program/portrait_printer/ui_data(mob/user) var/list/data = list() - data["library"] = SSpersistent_paintings.paintings["library"] ? SSpersistent_paintings.paintings["library"] : 0 - data["library_secure"] = SSpersistent_paintings.paintings["library_secure"] ? SSpersistent_paintings.paintings["library_secure"] : 0 - data["library_private"] = SSpersistent_paintings.paintings["library_private"] ? SSpersistent_paintings.paintings["library_private"] : 0 //i'm gonna regret this, won't i? + data["paintings"] = SSpersistent_paintings.painting_ui_data() return data /datum/computer_file/program/portrait_printer/ui_assets(mob/user) return list( - get_asset_datum(/datum/asset/simple/portraits/library), - get_asset_datum(/datum/asset/simple/portraits/library_secure), - get_asset_datum(/datum/asset/simple/portraits/library_private) + get_asset_datum(/datum/asset/simple/portraits) ) /datum/computer_file/program/portrait_printer/ui_act(action, params) @@ -53,13 +49,9 @@ printer.stored_paper -= CANVAS_PAPER_COST //canvas printing! - var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private") - var/folder = tab2key[params["tab"]] - var/list/current_list = SSpersistent_paintings.paintings[folder] - var/list/chosen_portrait = current_list[params["selected"]] - var/author = chosen_portrait["author"] - var/title = chosen_portrait["title"] - var/png = "data/paintings/[folder]/[chosen_portrait["md5"]].png" + var/datum/painting/chosen_portrait = locate(params["selected"]) 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() @@ -69,15 +61,17 @@ 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) + 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.painting_name = title - printed_canvas.author_ckey = author - printed_canvas.name = "painting - [title]" + 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() - to_chat(usr, span_notice("You have printed [title] onto a new canvas.")) + to_chat(usr, span_notice("You have printed [chosen_portrait.title] onto a new canvas.")) playsound(computer.physical, 'sound/items/poster_being_created.ogg', 100, TRUE) diff --git a/code/modules/vending/games.dm b/code/modules/vending/games.dm index 5dff958ec73..22582f95d20 100644 --- a/code/modules/vending/games.dm +++ b/code/modules/vending/games.dm @@ -31,7 +31,8 @@ /obj/item/razor=3, /obj/item/canvas/nineteen_nineteen = 5, /obj/item/canvas/twentythree_nineteen = 5, - /obj/item/canvas/twentythree_twentythree = 5 + /obj/item/canvas/twentythree_twentythree = 5, + /obj/item/paint_palette = 3 ) contraband = list( /obj/item/dice/fudge = 9, diff --git a/icons/mob/inhands/equipment/palette_lefthand.dmi b/icons/mob/inhands/equipment/palette_lefthand.dmi new file mode 100644 index 00000000000..e68495cdd6c Binary files /dev/null and b/icons/mob/inhands/equipment/palette_lefthand.dmi differ diff --git a/icons/mob/inhands/equipment/palette_righthand.dmi b/icons/mob/inhands/equipment/palette_righthand.dmi new file mode 100644 index 00000000000..cce674c3515 Binary files /dev/null and b/icons/mob/inhands/equipment/palette_righthand.dmi differ diff --git a/icons/obj/artstuff.dmi b/icons/obj/artstuff.dmi index 847ebd5ca61..17dd9d38886 100644 Binary files a/icons/obj/artstuff.dmi and b/icons/obj/artstuff.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 3603dd12a4f..e0d06483e8b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1732,6 +1732,7 @@ #include "code\modules\admin\outfit_editor.dm" #include "code\modules\admin\outfit_manager.dm" #include "code\modules\admin\outfits.dm" +#include "code\modules\admin\painting_manager.dm" #include "code\modules\admin\permissionedit.dm" #include "code\modules\admin\player_panel.dm" #include "code\modules\admin\poll_management.dm" diff --git a/tgui/packages/tgui/interfaces/Canvas.js b/tgui/packages/tgui/interfaces/Canvas.js deleted file mode 100644 index 4a73fc60558..00000000000 --- a/tgui/packages/tgui/interfaces/Canvas.js +++ /dev/null @@ -1,112 +0,0 @@ -import { Component, createRef } from 'inferno'; -import { useBackend } from '../backend'; -import { Box, Button } from '../components'; -import { Window } from '../layouts'; - -const PX_PER_UNIT = 24; - -class PaintCanvas extends Component { - constructor(props) { - super(props); - this.canvasRef = createRef(); - this.onCVClick = props.onCanvasClick; - } - - componentDidMount() { - this.drawCanvas(this.props); - } - - componentDidUpdate() { - this.drawCanvas(this.props); - } - - drawCanvas(propSource) { - const ctx = this.canvasRef.current.getContext("2d"); - const grid = propSource.value; - const x_size = grid.length; - if (!x_size) { - return; - } - const y_size = grid[0].length; - const x_scale = Math.round(this.canvasRef.current.width / x_size); - const y_scale = Math.round(this.canvasRef.current.height / y_size); - ctx.save(); - ctx.scale(x_scale, y_scale); - for (let x = 0; x < grid.length; x++) { - const element = grid[x]; - for (let y = 0; y < element.length; y++) { - const color = element[y]; - ctx.fillStyle = color; - ctx.fillRect(x, y, 1, 1); - } - } - ctx.restore(); - } - - clickwrapper(event) { - const x_size = this.props.value.length; - if (!x_size) { - return; - } - const y_size = this.props.value[0].length; - const x_scale = this.canvasRef.current.width / x_size; - const y_scale = this.canvasRef.current.height / y_size; - const x = Math.floor(event.offsetX / x_scale)+1; - const y = Math.floor(event.offsetY / y_scale)+1; - this.onCVClick(x, y); - } - - render() { - const { - res = 1, - value, - dotsize = PX_PER_UNIT, - ...rest - } = this.props; - const [width, height] = getImageSize(value); - return ( - this.clickwrapper(e)}> - Canvas failed to render. - - ); - } -} - -const getImageSize = value => { - const width = value.length; - const height = width !== 0 ? value[0].length : 0; - return [width, height]; -}; - -export const Canvas = (props, context) => { - const { act, data } = useBackend(context); - const dotsize = PX_PER_UNIT; - const [width, height] = getImageSize(data.grid); - return ( - - - - act("paint", { x, y })} /> - - {!data.finalized && ( - act("finalize")} - content="Finalize" /> - )} - {data.name} - - - - - ); -}; diff --git a/tgui/packages/tgui/interfaces/Canvas.tsx b/tgui/packages/tgui/interfaces/Canvas.tsx new file mode 100644 index 00000000000..50a77cc8340 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Canvas.tsx @@ -0,0 +1,253 @@ +import { Color } from 'common/color'; +import { Component, createRef, RefObject } from 'inferno'; +import { useBackend } from '../backend'; +import { Box, Button, Flex } from '../components'; +import { Window } from '../layouts'; + +const PX_PER_UNIT = 24; + +type PaintCanvasProps = Partial<{ + onCanvasModifiedHandler: (data : PointData[]) => void, + value: string[][], + width: number, + height: number, + imageWidth: number, + imageHeight: number, + editable: boolean, + drawing_color: string | null, +}>; + +type PointData = { + x: number, + y: number, + color: Color +} + +const fromDM = (data: string[][]) => { + return data.map(inner => inner.map(v => Color.fromHex(v))); +}; + +const toMassPaintFormat = (data: PointData[]) => { + return data.map(p => ({ x: p.x+1, y: p.y+1 })); // 1-based index dm side +}; + +class PaintCanvas extends Component { + canvasRef: RefObject; + baseImageData: Color[][] + modifiedElements: PointData[]; + onCanvasModified: (data: PointData[]) => void; + drawing: boolean; + drawing_color: string; + + constructor(props) { + super(props); + this.canvasRef = createRef(); + this.modifiedElements = []; + this.drawing = false; + this.onCanvasModified = props.onCanvasModifiedHandler; + + this.handleStartDrawing = this.handleStartDrawing.bind(this); + this.handleDrawing = this.handleDrawing.bind(this); + this.handleEndDrawing = this.handleEndDrawing.bind(this); + } + + componentDidMount() { + this.prepareCanvas(); + this.syncCanvas(); + } + + componentDidUpdate() { + // eslint-disable-next-line max-len + if (this.props.value !== undefined && JSON.stringify(this.baseImageData) !== JSON.stringify(fromDM(this.props.value))) { + this.syncCanvas(); + } + } + + prepareCanvas() { + const canvas = this.canvasRef.current!; + const ctx = canvas.getContext("2d"); + const width = this.props.width || canvas.width || 360; + const height = this.props.height || canvas.height || 360; + const x_resolution = this.props.imageWidth || 36; + const y_resolution = this.props.imageHeight || 36; + const x_scale = Math.round(width / x_resolution); + const y_scale = Math.round(height / y_resolution); + ctx?.setTransform(1, 0, 0, 1, 0, 0); + ctx?.scale(x_scale, y_scale); // This clears the canvas. + } + + syncCanvas() { + if (this.props.value === undefined) { + return; + } + this.baseImageData = fromDM(this.props.value); + this.modifiedElements = []; + + const canvas = this.canvasRef.current!; + const ctx = canvas.getContext("2d")!; + for (let x = 0; x < this.baseImageData.length; x++) { + const element = this.baseImageData[x]; + for (let y = 0; y < element.length; y++) { + const color = element[y]; + ctx.fillStyle = color.toString(); + ctx.fillRect(x, y, 1, 1); + } + } + } + + eventToCoords(event : MouseEvent) { + const canvas = this.canvasRef.current!; + const width = this.props.width || canvas.width || 360; + const height = this.props.height || canvas.height || 360; + const x_resolution = this.props.imageWidth || 36; + const y_resolution = this.props.imageHeight || 36; + const x_scale = Math.round(width / x_resolution); + const y_scale = Math.round(height / y_resolution); + const x = Math.floor(event.offsetX / x_scale); + const y = Math.floor(event.offsetY / y_scale); + return { x, y }; + } + + handleStartDrawing(event : MouseEvent) { + if (!this.props.editable + || this.props.drawing_color === undefined + || this.props.drawing_color === null) { + return; + } + this.modifiedElements = []; + this.drawing = true; + this.drawing_color = this.props.drawing_color; + const coords = this.eventToCoords(event); + this.drawPoint(coords.x, coords.y, this.drawing_color); + } + + drawPoint(x: number, y: number, color: any) { + let p: PointData = { x, y, color: Color.fromHex(color) }; + this.modifiedElements.push(p); + const canvas = this.canvasRef.current!; + const ctx = canvas.getContext("2d")!; + ctx.fillStyle = color; + ctx.fillRect(x, y, 1, 1); + } + + handleDrawing(event: MouseEvent) { + if (!this.drawing) { + return; + } + const coords = this.eventToCoords(event); + this.drawPoint(coords.x, coords.y, this.drawing_color); + } + + handleEndDrawing(event: MouseEvent) { + if (!this.drawing) { + return; + } + this.drawing = false; + const canvas = this.canvasRef.current!; + const ctx = canvas.getContext("2d")!; + if (this.onCanvasModified !== undefined) { + this.onCanvasModified(this.modifiedElements); + } + } + + render() { + const { + value, + width = 300, + height = 300, + imageWidth = 36, + imageHeight = 36, + ...rest + } = this.props; + return ( + + Canvas failed to render. + + ); + } +} + +const getImageSize = value => { + const width = value.length; + const height = width !== 0 ? value[0].length : 0; + return [width, height]; +}; + +type CanvasData = { + grid: string[][], + finalized: boolean, + name: string, + editable: boolean, + paint_tool_color: string | null, + author: string | null, + medium: string | null, + patron: string | null, + date: string | null, + show_plaque: boolean +} + +export const Canvas = (props, context) => { + const { act, data } = useBackend(context); + const [width, height] = getImageSize(data.grid); + const scaled_width = width * PX_PER_UNIT; + const scaled_height = height * PX_PER_UNIT; + const average_plaque_height = 90; + return ( + + + + act("paint", { data: toMassPaintFormat(changed) })} + editable={data.editable} + /> + + {!data.finalized && ( + + act("finalize")} + content="Finalize" /> + + )} + {!!data.finalized && !!data.show_plaque && ( + + {data.name} + + {data.author} + {!!data.date && `- ${new Date(data.date).getFullYear()+540}`} + + {data.medium} + + {!!data.patron && `Sponsored by ${data.patron} `} + }> + + + + + {chosenPainting.title} + + + + )} + {!chosenPainting && ( + + + Title + Author + Preview + Actions + + {paintings.map(painting => ( + + {painting.title} + {painting.creator_ckey} + + + + + + + ))} +
+ )} +
+
+ ); +}; + + diff --git a/tgui/packages/tgui/interfaces/PortraitPicker.js b/tgui/packages/tgui/interfaces/PortraitPicker.js index 59b9b227046..fe0591d1c25 100644 --- a/tgui/packages/tgui/interfaces/PortraitPicker.js +++ b/tgui/packages/tgui/interfaces/PortraitPicker.js @@ -1,37 +1,16 @@ import { resolveAsset } from '../assets'; import { useBackend, useLocalState } from '../backend'; -import { Button, Flex, NoticeBox, Section, Tabs } from '../components'; +import { Button, Flex, NoticeBox, Section } from '../components'; import { Window } from '../layouts'; export const PortraitPicker = (props, context) => { const { act, data } = useBackend(context); - const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0); const [listIndex, setListIndex] = useLocalState(context, 'listIndex', 0); const { - library, - library_secure, - library_private, + paintings, } = data; - const TABS = [ - { - name: 'Common Portraits', - asset_prefix: "library", - list: library, - }, - { - name: 'Secure Portraits', - asset_prefix: "library_secure", - list: library_secure, - }, - { - name: 'Private Portraits', - asset_prefix: "library_private", - list: library_private, - }, - ]; - const tab2list = TABS[tabIndex].list; - const current_portrait_title = tab2list[listIndex]["title"]; - const current_portrait_asset_name = TABS[tabIndex].asset_prefix + "_" + tab2list[listIndex]["md5"]; + const current_portrait_title = paintings[listIndex]["title"]; + const current_portrait_asset_name = "paintings" + "_" + paintings[listIndex]["md5"]; return ( { height={406}> - -
- - {TABS.map((tabObj, i) => ( - { - setListIndex(0); - setTabIndex(i); - }}> - {tabObj.name} - - ))} - -
-
{ icon="check" content="Select Portrait" onClick={() => act("select", { - tab: tabIndex+1, - selected: listIndex+1, + selected: paintings[listIndex]["ref"], })} />