#define PAINTINGS_DATA_FORMAT_VERSION 3 /* { "version":2 "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", "frame_type" : "bones" }, ] } */ /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 /// The appearance of the frame overlay that appears when the painting if framed. var/frame_type = "simple" /// 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"] patron_name = json_data["patron_name"] credit_value = json_data["credit_value"] width = json_data["width"] height = json_data["height"] medium = json_data["medium"] frame_type = json_data["frame_type"] 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 new_data["frame_type"] = frame_type return new_data SUBSYSTEM_DEF(persistent_paintings) name = "Persistent Paintings" init_order = INIT_ORDER_PERSISTENT_PAINTINGS flags = SS_NO_FIRE /// A list of painting frames that this controls var/list/obj/structure/sign/painting/painting_frames = list() /// Hashes of paintings deleted this round var/list/deleted_paintings_md5s = list() /// A list of /datum/paintings saved or ready to be saved this round. var/list/paintings = list() /// A list of paintings' data for paintings that are currently stored in the library. var/list/cached_painting_data = list() /// A list of paintings' data for paintings that are currently stored in the library, with admin metadata var/list/admin_painting_data = list() ///The list of available frame reskins (they are purely cosmetic) and the associated patronage amount required for them. var/list/frame_types_by_patronage_tier = list( "simple" = 0, "iron" = PATRONAGE_OK_FRAME, "bamboo" = PATRONAGE_OK_FRAME, "bones" = PATRONAGE_OK_FRAME, "bronze" = PATRONAGE_NICE_FRAME, "clown" = PATRONAGE_NICE_FRAME, "frog" = PATRONAGE_NICE_FRAME, "silver" = PATRONAGE_GREAT_FRAME, "necropolis" = PATRONAGE_GREAT_FRAME, "gold" = PATRONAGE_EXCELLENT_FRAME, "diamond" = PATRONAGE_AMAZING_FRAME, "rainbow" = PATRONAGE_SUPERB_FRAME, "supermatter" = PATRONAGE_LEGENDARY_FRAME, ) /datum/controller/subsystem/persistent_paintings/Initialize() var/json_file = file("data/paintings.json") if(fexists(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() cache_paintings() return SS_INIT_SUCCESS /datum/controller/subsystem/persistent_paintings/proc/cache_paintings() cached_painting_data = list() admin_painting_data = list() for(var/datum/painting/painting as anything in paintings) cached_painting_data += list(list( "title" = painting.title, "creator" = painting.creator_name, "md5" = painting.md5, "ref" = REF(painting), "width" = painting.width, "height" = painting.height, "ratio" = painting.width/painting.height, )) var/list/pdata = painting.to_json() pdata["ref"] = REF(painting) UNTYPED_LIST_ADD(admin_painting_data, pdata) /** * Generates painting data ready to be consumed by ui. * Args: * * filter: a bitfield argument is used to filter out paintings that don't match certain requisites. * * admin : whether all the json data of the painting is added to the return value or only the more IC details * * search_text : text to search for if the PAINTINGS_FILTER_SEARCH_TITLE or PAINTINGS_FILTER_SEARCH_CREATOR filters are enabled. */ /datum/controller/subsystem/persistent_paintings/proc/painting_ui_data(filter=NONE, admin=FALSE, search_text) var/searching = filter & (PAINTINGS_FILTER_SEARCH_TITLE|PAINTINGS_FILTER_SEARCH_CREATOR) && search_text var/list/paintings = admin ? admin_painting_data : cached_painting_data if(!searching && !(filter & PAINTINGS_FILTER_AI_PORTRAIT)) return paintings var/list/filtered_paintings = list() for(var/painting in paintings) if(filter & PAINTINGS_FILTER_AI_PORTRAIT && ((painting["width"] != 24 && painting["width"] != 23) || (painting["height"] != 24 && painting["height"] != 23))) continue if(searching) var/haystack_text = "" if(filter & PAINTINGS_FILTER_SEARCH_TITLE) haystack_text = painting["title"] else if(filter & PAINTINGS_FILTER_SEARCH_CREATOR) haystack_text = painting["creator"] if(!findtext(haystack_text, search_text)) continue filtered_paintings += list(painting) return filtered_paintings /// 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/version = current_data["version"] if(version < 1) current_data = migrate_to_version_1(current_data) if(version < 2) //Makes sure old paintings get a cosmetic frame type from their patronage tiers. current_data = migrate_to_version_2(current_data) if(version < 3) //Reduces the allowed length of titles from 1000 characters circa to 42. current_data["version"] = 3 var/old_title = current_data["title"] var/new_title = reject_bad_name(old_title, allow_numbers = TRUE, ascii_only = FALSE, strict = TRUE, cap_after_symbols = FALSE) current_data["title"] = new_title || "Illegibly Titled Artwork" return current_data /datum/controller/subsystem/persistent_paintings/proc/migrate_to_version_1(list/current_data) 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 result /datum/controller/subsystem/persistent_paintings/proc/migrate_to_version_2(list/current_data) fcopy("data/paintings.json","data/paintings_migration_backup_1.json") //Better safe than sorry current_data["version"] = 2 for(var/painting_data in current_data["paintings"]) painting_data["tags"] |= "Migrated from version 1" var/credit_value = painting_data["credit_value"] var/list/possible_frame_types = get_available_frames(credit_value, only_current_tier = TRUE) painting_data["frame_type"] = pick(possible_frame_types) || "simple" return current_data /** * returns a list of cosmetic frames which patronage tier values are are within credit_value. * If only_current_tier is TRUE, the list will be populated only by the ones from the highest tier. */ /datum/controller/subsystem/persistent_paintings/proc/get_available_frames(credit_value, only_current_tier = FALSE) var/list/frame_candidates = list() var/current_tier_patronage = 0 for(var/frametype in frame_types_by_patronage_tier) var/required_patronage = frame_types_by_patronage_tier[frametype] if(credit_value >= required_patronage) if(only_current_tier && current_tier_patronage < required_patronage) current_tier_patronage = required_patronage frame_candidates.Cut() frame_candidates += frametype return frame_candidates /// 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") var/list/collated_data = list() if(fexists(json_file)) var/list/old_data = json_decode(file2text(json_file)) for(var/list/painting_data as anything in old_data["paintings"]) collated_data[painting_data["md5"]] = painting_data var/list/painting_data = list() for(var/datum/painting/painting as anything in paintings) collated_data[painting.md5] = painting.to_json() //Current data has priority over old data // Remove deleted paintings from the list collated_data -= deleted_paintings_md5s // Flatten the resulting list for(var/key in collated_data) painting_data += list(collated_data[key]) var/list/all_data = list("version" = PAINTINGS_DATA_FORMAT_VERSION) all_data["paintings"] = painting_data var/payload = json_encode(all_data) fdel(json_file) WRITE_FILE(json_file, payload) cache_paintings() #undef PAINTINGS_DATA_FORMAT_VERSION