From e4a42152c4d6a6d9417f86d4c8af8dd53cc29352 Mon Sep 17 00:00:00 2001 From: RKz Date: Mon, 11 Dec 2023 13:17:17 -0500 Subject: [PATCH 01/10] https://github.com/tgstation/tgstation/pull/57673 --- beestation.dme | 1 + code/__HELPERS/icons.dm | 5 +- code/modules/admin/admin_verbs.dm | 4 +- code/modules/admin/verbs/debug.dm | 33 --- code/modules/admin/verbs/selectequipment.dm | 231 ++++++++++++++++++ .../serialization/preferences_database.dm | 9 + code/modules/mob/living/carbon/human/dummy.dm | 19 +- code/modules/tgui/external.dm | 1 - tgui/packages/common/collections.ts | 2 + .../tgui/interfaces/SelectEquipment.js | 214 ++++++++++++++++ .../savefileimporter/code/savefile_parser.dm | 10 + 11 files changed, 487 insertions(+), 42 deletions(-) create mode 100644 code/modules/admin/verbs/selectequipment.dm create mode 100644 tgui/packages/tgui/interfaces/SelectEquipment.js diff --git a/beestation.dme b/beestation.dme index a5400a0da5b77..d364651ef34b7 100644 --- a/beestation.dme +++ b/beestation.dme @@ -1771,6 +1771,7 @@ #include "code\modules\admin\verbs\randomverbs.dm" #include "code\modules\admin\verbs\reestablish_db_connection.dm" #include "code\modules\admin\verbs\requests.dm" +#include "code\modules\admin\verbs\selectequipment.dm" #include "code\modules\admin\verbs\shuttlepanel.dm" #include "code\modules\admin\verbs\spawnfloorcluwne.dm" #include "code\modules\admin\verbs\spawnobjasmob.dm" diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index d6a4b15c867f7..50dc8230ebd52 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1123,10 +1123,9 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing") + COMPILE_OVERLAYS(body) for(var/D in showDirs) - body.setDir(D) - COMPILE_OVERLAYS(body) - var/icon/partial = getFlatIcon(body) + var/icon/partial = getFlatIcon(body, defdir=D) out_icon.Insert(partial,dir=D) humanoid_icon_cache[icon_id] = out_icon diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 23b6a384af66b..df1afadb20f7b 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -82,7 +82,7 @@ GLOBAL_PROTECT(admin_verbs_ban) GLOBAL_LIST_INIT(admin_verbs_sounds, list(/client/proc/play_local_sound, /client/proc/play_sound, /client/proc/set_round_end_sound, /client/proc/play_soundtrack)) GLOBAL_PROTECT(admin_verbs_sounds) GLOBAL_LIST_INIT(admin_verbs_fun, list( - /client/proc/cmd_admin_dress, + /client/proc/cmd_select_equipment, /client/proc/cmd_admin_gib_self, /client/proc/drop_bomb, /client/proc/set_dynex_scale, @@ -231,7 +231,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list( /client/proc/play_sound, /client/proc/set_round_end_sound, /client/proc/play_soundtrack, - /client/proc/cmd_admin_dress, + /client/proc/cmd_select_equipment, /client/proc/cmd_admin_gib_self, /client/proc/drop_bomb, /client/proc/drop_dynex_bomb, diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index a972781bccb8e..51e922ba097e2 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -487,39 +487,6 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that set name = "Test Areas (ALL)" cmd_admin_areatest(FALSE) -/client/proc/cmd_admin_dress(mob/M in GLOB.mob_list) - set category = "Fun" - set name = "Select equipment" - if(!(ishuman(M) || isobserver(M))) - alert("Invalid mob") - return - - var/dresscode = robust_dress_shop() - - if(!dresscode) - return - - var/delete_pocket - var/mob/living/carbon/human/H - if(isobserver(M)) - H = M.change_mob_type(/mob/living/carbon/human, null, null, TRUE) - else - H = M - if(H.l_store || H.r_store || H.s_store) //saves a lot of time for admins and coders alike - if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") != "Yes") - delete_pocket = TRUE - - SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - for (var/obj/item/I in H.get_equipped_items(delete_pocket)) - qdel(I) - if(dresscode != "Naked") - H.equipOutfit(dresscode) - - H.regenerate_icons() - - log_admin("[key_name(usr)] changed the equipment of [key_name(H)] to [dresscode].") - message_admins("[key_name_admin(usr)] changed the equipment of [ADMIN_LOOKUPFLW(H)] to [dresscode].") - /client/proc/robust_dress_shop() var/list/outfits = list("Naked","Custom","As Job...","As Job(Plasmaman)...", "Debug") var/list/paths = subtypesof(/datum/outfit) - typesof(/datum/outfit/job) - typesof(/datum/outfit/plasmaman) - typesof(/datum/outfit/debug) diff --git a/code/modules/admin/verbs/selectequipment.dm b/code/modules/admin/verbs/selectequipment.dm new file mode 100644 index 0000000000000..004fcc6ba5bb4 --- /dev/null +++ b/code/modules/admin/verbs/selectequipment.dm @@ -0,0 +1,231 @@ +/client/proc/cmd_select_equipment(mob/target in GLOB.mob_list) + set category = "Admin.Events" + set name = "Select equipment" + + + var/datum/select_equipment/ui = new(usr, target) + ui.ui_interact(usr) + +/* + * This is the datum housing the select equipment UI. + * + * You may notice some oddities about the way outfits are passed to the UI and vice versa here. + * That's because it handles both outfit typepaths (for normal outfits) *and* outfit objects (for custom outfits). + * + * Custom outfits need to be objects as they're created in runtime. + * "Then just handle the normal outfits as objects too and simplify the handling" - you may say. + * There are about 300 outfit types at the time of writing this. Initializing all of these to objects would be a huge waste. + * + */ + +/datum/select_equipment + var/client/user + var/mob/target_mob + + var/dummy_key + var/mob/living/carbon/human/dummy/dummy + + //static list to share all the outfit typepaths between all instances of this datum. + var/static/list/cached_outfits + + //a typepath if the selected outfit is a normal outfit; + //an object if the selected outfit is a custom outfit + var/datum/outfit/selected_outfit = /datum/outfit + //serializable string for the UI to keep track of which outfit is selected + var/selected_identifier = "/datum/outfit" + +/datum/select_equipment/New(_user, mob/target) + user = CLIENT_FROM_VAR(_user) + + if(!ishuman(target) && !isobserver(target)) + tgui_alert(usr,"Invalid mob") + return + target_mob = target + +/datum/select_equipment/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "SelectEquipment", "Select Equipment") + ui.open() + ui.set_autoupdate(FALSE) + +/datum/select_equipment/ui_state(mob/user) + return GLOB.admin_state + +/datum/select_equipment/ui_status(mob/user, datum/ui_state/state) + if(QDELETED(target_mob)) + return UI_CLOSE + return ..() + +/datum/select_equipment/ui_close(mob/user) + clear_human_dummy(dummy_key) + qdel(src) + +/datum/select_equipment/proc/init_dummy() + dummy_key = "selectequipmentUI_[target_mob]" + dummy = generate_or_wait_for_human_dummy(dummy_key) + var/mob/living/carbon/carbon_target = target_mob + if(istype(carbon_target)) + carbon_target.dna.transfer_identity(dummy) + dummy.updateappearance() + + unset_busy_human_dummy(dummy_key) + return + +/** + * Packs up data about an outfit as an assoc list to send to the UI as an outfit entry. + * + * Args: + * * category (string) - The tab it will be under + * + * * identifier (typepath or ref) - This will sent this back to ui_act to preview or spawn in an outfit. + * * Must be unique between all entries. + * + * * name (string) - Will be the text on the button + * + * * priority (bool)(optional) - If True, the UI will sort the entry to the top, right below favorites. + * + * * custom_entry (bool)(optional) - Send the identifier with a "ref" keyword instead of "path", + * * for the UI to tell apart custom outfits from normal ones. + * + * Returns (list) An outfit entry + */ + +/datum/select_equipment/proc/outfit_entry(category, identifier, name, priority=FALSE, custom_entry=FALSE) + if(custom_entry) + return list("category" = category, "ref" = identifier, "name" = name, "priority" = priority) + return list("category" = category, "path" = identifier, "name" = name, "priority" = priority) + +/datum/select_equipment/proc/make_outfit_entries(category="General", list/outfit_list) + var/list/entries = list() + for(var/path as anything in outfit_list) + var/datum/outfit/outfit = path + entries += list(outfit_entry(category, path, initial(outfit.name))) + return entries + +//GLOB.custom_outfits lists outfit *objects* so we'll need to do some custom handling for it +/datum/select_equipment/proc/make_custom_outfit_entries(list/outfit_list) + var/list/entries = list() + for(var/datum/outfit/outfit as anything in outfit_list) + entries += list(outfit_entry("Custom", REF(outfit), outfit.name, custom_entry=TRUE)) //it's either this or special handling on the UI side + return entries + +/datum/select_equipment/ui_data(mob/user) + var/list/data = list() + if(!dummy) + init_dummy() + + var/datum/preferences/prefs = target_mob?.client?.prefs + var/icon/dummysprite = get_flat_human_icon(null, prefs=prefs, dummy_key = dummy_key, outfit_override = selected_outfit) + data["icon64"] = icon2base64(dummysprite) + data["name"] = target_mob + + data["favorites"] = list() + if(prefs) + data["favorites"] = prefs.favorite_outfits + + var/list/custom + custom += make_custom_outfit_entries(GLOB.custom_outfits) + data["custom_outfits"] = custom + data["current_outfit"] = selected_identifier + return data + + +/datum/select_equipment/ui_static_data(mob/user) + var/list/data = list() + if(!cached_outfits) + cached_outfits = list() + cached_outfits += list(outfit_entry("General", /datum/outfit, "Naked", priority=TRUE)) + cached_outfits += make_outfit_entries("General", subtypesof(/datum/outfit) - typesof(/datum/outfit/job) - typesof(/datum/outfit/plasmaman)) + cached_outfits += make_outfit_entries("Jobs", typesof(/datum/outfit/job)) + cached_outfits += make_outfit_entries("Plasmamen Outfits", typesof(/datum/outfit/plasmaman)) + + data["outfits"] = cached_outfits + return data + + +/datum/select_equipment/proc/resolve_outfit(text) + + var/path = text2path(text) + if(ispath(path, /datum/outfit)) + return path + + else //don't bail yet - could be a custom outfit + var/datum/outfit/custom_outfit = locate(text) + if(istype(custom_outfit)) + return custom_outfit + + +/datum/select_equipment/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("preview") + var/datum/outfit/new_outfit = resolve_outfit(params["path"]) + + if(ispath(new_outfit)) //got a typepath - that means we're dealing with a normal outfit + selected_identifier = new_outfit //these are keyed by type + //by the way, no, they can't be keyed by name because many of them have duplicate names + + else if(istype(new_outfit)) //got an initialized object - means it's a custom outfit + selected_identifier = REF(new_outfit) //and the outfit will be keyed by its ref (cause its type will always be /datum/outfit) + + else //we got nothing and should bail + return + + selected_outfit = new_outfit + + if("applyoutfit") + var/datum/outfit/new_outfit = resolve_outfit(params["path"]) + if(new_outfit && ispath(new_outfit)) //initialize it + new_outfit = new new_outfit + if(!istype(new_outfit)) + return + user.admin_apply_outfit(target_mob, new_outfit) + + if("customoutfit") + user.outfit_manager() + + if("togglefavorite") + var/datum/outfit/outfit_path = resolve_outfit(params["path"]) + if(!ispath(outfit_path)) //we do *not* want custom outfits (i.e objects) here, they're not even persistent + return + + if(user.prefs.favorite_outfits.Find(outfit_path)) //already there, remove it + user.prefs.favorite_outfits -= outfit_path + else //not there, add it + user.prefs.favorite_outfits += outfit_path + user.prefs.save_preferences() + +/client/proc/admin_apply_outfit(mob/target, dresscode) + if(!ishuman(target) && !isobserver(target)) + tgui_alert(usr,"Invalid mob") + return + + if(!dresscode) + return + + var/delete_pocket + var/mob/living/carbon/human/human_target + if(isobserver(target)) + human_target = target.change_mob_type(/mob/living/carbon/human, delete_old_mob = TRUE) + else + human_target = target + if(human_target.l_store || human_target.r_store || human_target.s_store) //saves a lot of time for admins and coders alike + if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No") + delete_pocket = TRUE + + SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + for(var/obj/item/item in human_target.get_equipped_items(delete_pocket)) + qdel(item) + + if(dresscode != "Naked") + human_target.equipOutfit(dresscode) + + human_target.regenerate_icons() + + log_admin("[key_name(usr)] changed the equipment of [key_name(human_target)] to [dresscode].") + message_admins("[key_name_admin(usr)] changed the equipment of [ADMIN_LOOKUPFLW(human_target)] to [dresscode].") + + return dresscode diff --git a/code/modules/client/preferences/serialization/preferences_database.dm b/code/modules/client/preferences/serialization/preferences_database.dm index 2f5e92ff949bc..b075d8bcb7d13 100644 --- a/code/modules/client/preferences/serialization/preferences_database.dm +++ b/code/modules/client/preferences/serialization/preferences_database.dm @@ -89,6 +89,14 @@ READPREF_JSONDEC(purchased_gear, PREFERENCE_TAG_PURCHASED_GEAR) READPREF_JSONDEC(role_preferences_global, PREFERENCE_TAG_ROLE_PREFERENCES_GLOBAL) + READPREF_JSONDEC(favorite_outfits, PREFERENCE_TAG_FAVORITE_OUTFITS) + var/list/parsed_favs = list() + for(var/typetext in favorite_outfits) + var/datum/outfit/path = text2path(typetext) + if(ispath(path)) //whatever typepath fails this check probably doesn't exist anymore + parsed_favs += path + favorite_outfits = unique_list(parsed_favs) + // Custom hotkeys READPREF_JSONDEC(key_bindings, PREFERENCE_TAG_KEYBINDS) @@ -181,6 +189,7 @@ PREP_WRITEPREF_JSONENC(key_bindings, PREFERENCE_TAG_KEYBINDS) PREP_WRITEPREF_JSONENC(purchased_gear, PREFERENCE_TAG_PURCHASED_GEAR) PREP_WRITEPREF_JSONENC(role_preferences_global, PREFERENCE_TAG_ROLE_PREFERENCES_GLOBAL) + PREP_WRITEPREF_JSONENC(favorite_outfits, PREFERENCE_TAG_FAVORITE_OUTFITS) // QuerySelect can execute many queries at once. That name is dumb but w/e SSdbcore.QuerySelect(write_queries, TRUE, TRUE) diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm index 7b03ef9d920f0..aacd50ffd9bdb 100644 --- a/code/modules/mob/living/carbon/human/dummy.dm +++ b/code/modules/mob/living/carbon/human/dummy.dm @@ -74,17 +74,30 @@ GLOBAL_LIST_EMPTY(dummy_mob_list) D = new GLOB.human_dummy_list[slotkey] = D GLOB.dummy_mob_list += D + else + D.regenerate_icons() //they were cut in wipe_state() D.in_use = TRUE return D -/proc/unset_busy_human_dummy(slotnumber) - if(!slotnumber) +/proc/unset_busy_human_dummy(slotkey) + if(!slotkey) return - var/mob/living/carbon/human/dummy/D = GLOB.human_dummy_list[slotnumber] + var/mob/living/carbon/human/dummy/D = GLOB.human_dummy_list[slotkey] if(istype(D)) D.wipe_state() D.in_use = FALSE +/proc/clear_human_dummy(slotkey) + if(!slotkey) + return + + var/mob/living/carbon/human/dummy/dummy = GLOB.human_dummy_list[slotkey] + + GLOB.human_dummy_list -= slotkey + if(istype(dummy)) + GLOB.dummy_mob_list -= dummy + qdel(dummy) + /mob/living/carbon/human/dummy/add_to_mob_list() return diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 4b919631c0f47..eb613bc05baa9 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -50,7 +50,6 @@ * return list Data to be sent to the UI. */ /datum/proc/ui_data(mob/user) - SHOULD_NOT_SLEEP(TRUE) // Optional, but good code practice. Remove this if you have a valid use case. return list() // Not implemented. /** diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 7f5f1c0572366..a28f05fb8d3cb 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -149,6 +149,8 @@ export const sortStrings = sortBy(); export const range = (start: number, end: number): number[] => new Array(end - start).fill(null).map((_, index) => index + start); + export const sort = sortBy(); + /** * A fast implementation of reduce. */ diff --git a/tgui/packages/tgui/interfaces/SelectEquipment.js b/tgui/packages/tgui/interfaces/SelectEquipment.js new file mode 100644 index 0000000000000..e7abf709bc919 --- /dev/null +++ b/tgui/packages/tgui/interfaces/SelectEquipment.js @@ -0,0 +1,214 @@ +import { filter, map, sortBy, uniq } from 'common/collections'; +import { flow } from 'common/fp'; +import { createSearch } from 'common/string'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Icon, Input, Section, Stack, Tabs } from '../components'; +import { Window } from '../layouts'; + +// here's an important mental define: +// custom outfits give a ref keyword instead of path +const getOutfitKey = outfit => outfit.path || outfit.ref; + +const useOutfitTabs = (context, categories) => { + return useLocalState(context, 'selected-tab', categories[0]); +}; + +export const SelectEquipment = (props, context) => { + const { act, data } = useBackend(context); + const { + name, + icon64, + current_outfit, + favorites, + } = data; + + const isFavorited = entry => favorites?.includes(entry.path); + + const outfits = map(entry => ({ + ...entry, + favorite: isFavorited(entry), + }))([ + ...data.outfits, + ...data.custom_outfits, + ]); + + // even if no custom outfits were sent, we still want to make sure there's + // at least a 'Custom' tab so the button to create a new one pops up + const categories = uniq([ + ...outfits.map(entry => entry.category), + 'Custom', + ]); + const [tab] = useOutfitTabs(context, categories); + + const [searchText, setSearchText] = useLocalState( + context, 'searchText', ''); + const searchFilter = createSearch(searchText, entry => ( + entry.name + entry.path + )); + + const visibleOutfits = flow([ + filter(entry => entry.category === tab), + filter(searchFilter), + sortBy( + entry => !entry.favorite, + entry => !entry.priority, + entry => entry.name + ), + ])(outfits); + + const getOutfitEntry = current_outfit => outfits.find(outfit => ( + getOutfitKey(outfit) === current_outfit + )); + + const currentOutfitEntry = getOutfitEntry(current_outfit); + + return ( + + + + + + + setSearchText(value)} /> + + + + + + + + + + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+
+ ); +}; + +const DisplayTabs = (props, context) => { + const { categories } = props; + const [tab, setTab] = useOutfitTabs(context, categories); + return ( + + {categories.map(category => ( + setTab(category)}> + {category} + + ))} + + ); +}; + +const OutfitDisplay = (props, context) => { + const { act, data } = useBackend(context); + const { current_outfit } = data; + const { entries, currentTab } = props; + return ( +
+ {entries.map(entry => ( + + )} +
+ ); +}; + +const CurrentlySelectedDisplay = (props, context) => { + const { act, data } = useBackend(context); + const { current_outfit } = data; + const { entry } = props; + return ( + + {entry?.path && ( + + act('togglefavorite', { + path: entry.path, + })} /> + + )} + + + Currently selected: + + + {entry?.name} + + + + + + + ); +}; diff --git a/tools/savefileimporter/code/savefile_parser.dm b/tools/savefileimporter/code/savefile_parser.dm index 8d39202bceba4..d9b82e2c511ac 100644 --- a/tools/savefileimporter/code/savefile_parser.dm +++ b/tools/savefileimporter/code/savefile_parser.dm @@ -218,6 +218,16 @@ if(em) log_info("Query error when processing [owning_ckey] | [em]") + //favorite outfits + READ_FILE(S["favorite_outfits"], favorite_outfits) + + var/list/parsed_favs = list() + for(var/typetext in favorite_outfits) + var/datum/outfit/path = text2path(typetext) + if(ispath(path)) //whatever typepath fails this check probably doesn't exist anymore + parsed_favs += path + favorite_outfits = uniqueList(parsed_favs) + // Now do characters parse_characters(owning_ckey, S, character_dirs) From d9f1827e71d25c94c0747b567ad0ca61a19af76d Mon Sep 17 00:00:00 2001 From: RKz Date: Mon, 11 Dec 2023 13:30:10 -0500 Subject: [PATCH 02/10] https://github.com/tgstation/tgstation/pull/58125 --- beestation.dme | 2 + code/__DEFINES/preferences.dm | 2 + code/__DEFINES/vv.dm | 3 + code/controllers/subsystem/persistence.dm | 33 +++ code/datums/outfit.dm | 75 ++++-- code/modules/admin/outfit_editor.dm | 200 ++++++++++++++++ code/modules/admin/outfit_manager.dm | 73 ++++++ code/modules/admin/outfits.dm | 221 +----------------- code/modules/admin/topic.dm | 22 -- tgui/packages/tgui/interfaces/OutfitEditor.js | 146 ++++++++++++ .../packages/tgui/interfaces/OutfitManager.js | 80 +++++++ 11 files changed, 599 insertions(+), 258 deletions(-) create mode 100644 code/modules/admin/outfit_editor.dm create mode 100644 code/modules/admin/outfit_manager.dm create mode 100644 tgui/packages/tgui/interfaces/OutfitEditor.js create mode 100644 tgui/packages/tgui/interfaces/OutfitManager.js diff --git a/beestation.dme b/beestation.dme index d364651ef34b7..20fb62b221513 100644 --- a/beestation.dme +++ b/beestation.dme @@ -1690,6 +1690,8 @@ #include "code\modules\admin\holder2.dm" #include "code\modules\admin\ipintel.dm" #include "code\modules\admin\IsBanned.dm" +#include "code\modules\admin\outfit_editor.dm" +#include "code\modules\admin\outfit_manager.dm" #include "code\modules\admin\outfits.dm" #include "code\modules\admin\permissionedit.dm" #include "code\modules\admin\player_panel.dm" diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index c9da8c7c4b4a1..b89f524a13857 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -140,6 +140,7 @@ GLOBAL_LIST_INIT(helmet_styles, list( #define PREFERENCE_TAG_KEYBINDS "key_bindings" #define PREFERENCE_TAG_PURCHASED_GEAR "purchased_gear" #define PREFERENCE_TAG_ROLE_PREFERENCES_GLOBAL "be_special" +#define PREFERENCE_TAG_FAVORITE_OUTFITS "favorite_outfits" #define PREFERENCE_TAG_PAI_NAME "pai_name" #define PREFERENCE_TAG_PAI_DESCRIPTION "pai_description" #define PREFERENCE_TAG_PAI_COMMENT "pai_comment" @@ -151,6 +152,7 @@ GLOBAL_LIST_INIT(undatumized_preference_tags_player, list( PREFERENCE_TAG_KEYBINDS, PREFERENCE_TAG_PURCHASED_GEAR, PREFERENCE_TAG_ROLE_PREFERENCES_GLOBAL, + PREFERENCE_TAG_FAVORITE_OUTFITS, PREFERENCE_TAG_PAI_NAME, PREFERENCE_TAG_PAI_DESCRIPTION, PREFERENCE_TAG_PAI_COMMENT, diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index 375f7fb4be978..b8996f491e676 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -159,3 +159,6 @@ // paintings #define VV_HK_REMOVE_PAINTING "remove_painting" + +//outfits +#define VV_HK_TO_OUTFIT_EDITOR "outfit_editor" diff --git a/code/controllers/subsystem/persistence.dm b/code/controllers/subsystem/persistence.dm index 4aaaee1441243..0bad661b57a77 100644 --- a/code/controllers/subsystem/persistence.dm +++ b/code/controllers/subsystem/persistence.dm @@ -28,6 +28,7 @@ SUBSYSTEM_DEF(persistence) if(CONFIG_GET(flag/use_antag_rep)) LoadAntagReputation() LoadPaintings() + load_custom_outfits() return ..() /datum/controller/subsystem/persistence/proc/LoadPoly() @@ -162,6 +163,7 @@ SUBSYSTEM_DEF(persistence) if(CONFIG_GET(flag/use_antag_rep)) CollectAntagReputation() SavePaintings() + save_custom_outfits() /datum/controller/subsystem/persistence/proc/GetPhotoAlbums() var/album_path = file("data/photo_albums.json") @@ -330,3 +332,34 @@ SUBSYSTEM_DEF(persistence) var/json_file = file("data/paintings.json") fdel(json_file) WRITE_FILE(json_file, json_encode(paintings)) + +/datum/controller/subsystem/persistence/proc/load_custom_outfits() + var/file = file("data/custom_outfits.json") + if(!fexists(file)) + return + var/outfits_json = file2text(file) + var/list/outfits = json_decode(outfits_json) + if(!islist(outfits)) + return + + for(var/outfit_data in outfits) + if(!islist(outfit_data)) + continue + + var/outfittype = text2path(outfit_data["outfit_type"]) + if(!ispath(outfittype, /datum/outfit)) + continue + var/datum/outfit/outfit = new outfittype + if(!outfit.load_from(outfit_data)) + continue + GLOB.custom_outfits += outfit + +/datum/controller/subsystem/persistence/proc/save_custom_outfits() + var/file = file("data/custom_outfits.json") + fdel(file) + + var/list/data = list() + for(var/datum/outfit/outfit in GLOB.custom_outfits) + data += list(outfit.get_json_data()) + + WRITE_FILE(file, json_encode(data)) diff --git a/code/datums/outfit.dm b/code/datums/outfit.dm index 6020dd534297b..cfb3f92f538db 100755 --- a/code/datums/outfit.dm +++ b/code/datums/outfit.dm @@ -240,42 +240,42 @@ if(!istype(H) && !ismonkey(H)) return if(H.back) - H.back.add_fingerprint(H,1) //The 1 sets a flag to ignore gloves + H.back.add_fingerprint(H, ignoregloves = TRUE) for(var/obj/item/I in H.back.contents) - I.add_fingerprint(H,1) + I.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_id) - H.wear_id.add_fingerprint(H,1) + H.wear_id.add_fingerprint(H, ignoregloves = TRUE) if(H.w_uniform) - H.w_uniform.add_fingerprint(H,1) + H.w_uniform.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_suit) - H.wear_suit.add_fingerprint(H,1) + H.wear_suit.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_mask) - H.wear_mask.add_fingerprint(H,1) + H.wear_mask.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_neck) - H.wear_neck.add_fingerprint(H,1) + H.wear_neck.add_fingerprint(H, ignoregloves = TRUE) if(H.head) - H.head.add_fingerprint(H,1) + H.head.add_fingerprint(H, ignoregloves = TRUE) if(H.shoes) - H.shoes.add_fingerprint(H,1) + H.shoes.add_fingerprint(H, ignoregloves = TRUE) if(H.gloves) - H.gloves.add_fingerprint(H,1) + H.gloves.add_fingerprint(H, ignoregloves = TRUE) if(H.ears) - H.ears.add_fingerprint(H,1) + H.ears.add_fingerprint(H, ignoregloves = TRUE) if(H.glasses) - H.glasses.add_fingerprint(H,1) + H.glasses.add_fingerprint(H, ignoregloves = TRUE) if(H.belt) - H.belt.add_fingerprint(H,1) + H.belt.add_fingerprint(H, ignoregloves = TRUE) for(var/obj/item/I in H.belt.contents) - I.add_fingerprint(H,1) + I.add_fingerprint(H, ignoregloves = TRUE) if(H.s_store) - H.s_store.add_fingerprint(H,1) + H.s_store.add_fingerprint(H, ignoregloves = TRUE) if(H.l_store) - H.l_store.add_fingerprint(H,1) + H.l_store.add_fingerprint(H, ignoregloves = TRUE) if(H.r_store) - H.r_store.add_fingerprint(H,1) + H.r_store.add_fingerprint(H, ignoregloves = TRUE) for(var/obj/item/I in H.held_items) - I.add_fingerprint(H,1) - return 1 + I.add_fingerprint(H, ignoregloves = TRUE) + return TRUE /// Return a list of all the types that are required to disguise as this outfit type /datum/outfit/proc/get_chameleon_disguise_info() @@ -312,6 +312,33 @@ .["implants"] = implants .["accessory"] = accessory +/// Copy most vars from another outfit to this one +/datum/outfit/proc/copy_from(datum/outfit/target) + name = target.name + uniform = target.uniform + suit = target.suit + toggle_helmet = target.toggle_helmet + back = target.back + belt = target.belt + gloves = target.gloves + shoes = target.shoes + head = target.head + mask = target.mask + neck = target.neck + ears = target.ears + glasses = target.glasses + id = target.id + l_pocket = target.l_pocket + r_pocket = target.r_pocket + suit_store = target.suit_store + r_hand = target.r_hand + l_hand = target.l_hand + internals_slot = target.internals_slot + backpack_contents = target.backpack_contents + box = target.box + implants = target.implants + accessory = target.accessory + /datum/outfit/proc/save_to_file(mob/admin) var/stored_data = get_json_data() var/json = json_encode(stored_data) @@ -358,3 +385,13 @@ implants += imptype accessory = text2path(outfit_data["accessory"]) return TRUE + +/datum/outfit/vv_get_dropdown() + . = ..() + VV_DROPDOWN_OPTION("", "---") + VV_DROPDOWN_OPTION(VV_HK_TO_OUTFIT_EDITOR, "Outfit Editor") + +/datum/outfit/vv_do_topic(list/href_list) + . = ..() + if(href_list[VV_HK_TO_OUTFIT_EDITOR]) + usr.client.open_outfit_editor(src) diff --git a/code/modules/admin/outfit_editor.dm b/code/modules/admin/outfit_editor.dm new file mode 100644 index 0000000000000..2540ba965dafd --- /dev/null +++ b/code/modules/admin/outfit_editor.dm @@ -0,0 +1,200 @@ +/client/proc/open_outfit_editor(datum/outfit/target) + var/datum/outfit_editor/ui = new(usr, target) + ui.ui_interact(usr) + +#define OUTFIT_EDITOR_NAME "Outfit-O-Tron 9000" +/datum/outfit_editor + var/client/owner + + var/dummy_key + + var/datum/outfit/drip + +/datum/outfit_editor/New(user, datum/outfit/target) + owner = CLIENT_FROM_VAR(user) + + if(ispath(target)) + drip = new /datum/outfit + drip.copy_from(new target) + else if(istype(target)) + drip = target + else + drip = new /datum/outfit + drip.name = "New Outfit" + +/datum/outfit_editor/ui_state(mob/user) + return GLOB.admin_state + +/datum/outfit_editor/ui_status(mob/user, datum/ui_state/state) + if(QDELETED(drip)) + return UI_CLOSE + return ..() + +/datum/outfit_editor/ui_close(mob/user) + clear_human_dummy(dummy_key) + qdel(src) + +/datum/outfit_editor/proc/init_dummy() + dummy_key = "outfit_editor_[owner]" + var/mob/living/carbon/human/dummy/dummy = generate_or_wait_for_human_dummy(dummy_key) + var/mob/living/carbon/carbon_target = owner.mob + if(istype(carbon_target)) + carbon_target.dna.transfer_identity(dummy) + dummy.updateappearance() + + unset_busy_human_dummy(dummy_key) + +/datum/outfit_editor/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "OutfitEditor", OUTFIT_EDITOR_NAME) + ui.open() + ui.set_autoupdate(FALSE) + +/datum/outfit_editor/proc/entry(data) + if(ispath(data, /obj/item)) + var/obj/item/item = data + return list( + "path" = item, + "name" = initial(item.name), + "desc" = initial(item.desc), + // at this point initializing the item is probably faster tbh + "sprite" = icon2base64(icon(initial(item.icon), initial(item.icon_state))), + ) + + return data + +/datum/outfit_editor/proc/serialize_outfit() + var/list/outfit_slots = drip.get_json_data() + . = list() + for(var/key in outfit_slots) + var/val = outfit_slots[key] + . += list("[key]" = entry(val)) + +/datum/outfit_editor/ui_data(mob/user) + var/list/data = list() + + data["outfit"] = serialize_outfit() + data["saveable"] = !GLOB.custom_outfits.Find(drip) + + var/datum/preferences/prefs = owner.prefs + var/icon/dummysprite = get_flat_human_icon(null, + prefs = prefs, + dummy_key = dummy_key, + showDirs = list(SOUTH), + outfit_override = drip) + data["dummy64"] = icon2base64(dummysprite) + + return data + + +/datum/outfit_editor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return + . = TRUE + + var/slot = params["slot"] + switch(action) + if("click") + choose_item(slot) + if("ctrlClick") + choose_any_item(slot) + if("clear") + if(drip.vars.Find(slot)) + drip.vars[slot] = null + + if("rename") + var/newname = stripped_input(owner, "What do you want to name this outfit?", OUTFIT_EDITOR_NAME) + if(newname) + drip.name = newname + if("save") + GLOB.custom_outfits |= drip + SStgui.update_user_uis(owner.mob) + if("delete") + GLOB.custom_outfits -= drip + SStgui.update_user_uis(owner.mob) + if("vv") + owner.debug_variables(drip) + + +/datum/outfit_editor/proc/set_item(slot, obj/item/choice) + if(!choice) + return + if(!ispath(choice)) + alert(owner, "Invalid item", OUTFIT_EDITOR_NAME, "oh no") + return + if(initial(choice.icon_state) == null) //hacky check copied from experimentor code + var/msg = "Warning: This item's icon_state is null, indicating it is very probably not actually a usable item." + if(alert(owner, msg, OUTFIT_EDITOR_NAME, "Use it anyway", "Cancel") != "Use it anyway") + return + + if(drip.vars.Find(slot)) + drip.vars[slot] = choice + +/datum/outfit_editor/proc/choose_any_item(slot) + var/obj/item/choice = pick_closest_path(FALSE) + + if(!choice) + return + + set_item(slot, choice) + +//this proc will try to give a good selection of items that the user can choose from +//it does *not* give a selection of all items that can fit in a slot because lag; +//most notably the hand and pocket slots because they accept pretty much anything +//also stuff that fits in the belt and back slots are scattered pretty much all over the place +/datum/outfit_editor/proc/choose_item(slot) + var/list/options = list() + + switch(slot) + if("head") + options = typesof(/obj/item/clothing/head) + if("glasses") + options = typesof(/obj/item/clothing/glasses) + if("ears") + options = typesof(/obj/item/radio/headset) + + if("neck") + options = typesof(/obj/item/clothing/neck) + if("mask") + options = typesof(/obj/item/clothing/mask) + + if("uniform") + options = typesof(/obj/item/clothing/under) + if("suit") + options = typesof(/obj/item/clothing/suit) + if("gloves") + options = typesof(/obj/item/clothing/gloves) + + if("suit_store") + var/obj/item/clothing/suit/suit = drip.suit + if(suit) + suit = new suit //initial() doesn't like lists + options = suit.allowed + if(!options.len) //nothing will happen, but don't let the user think it's broken + to_chat(owner, "No options available for the current suit.") + + if("belt") + options = typesof(/obj/item/storage/belt) + if("id") + options = typesof(/obj/item/card/id) + + if("l_hand") + choose_any_item(slot) + if("back") + options = typesof(/obj/item/storage/backpack) + if("r_hand") + choose_any_item(slot) + + if("l_pocket") + choose_any_item(slot) + if("shoes") + options = typesof(/obj/item/clothing/shoes) + if("r_pocket") + choose_any_item(slot) + + if(length(options)) + set_item(slot, tgui_input_list(owner, "Choose an item", OUTFIT_EDITOR_NAME, options)) + + +#undef OUTFIT_EDITOR_NAME diff --git a/code/modules/admin/outfit_manager.dm b/code/modules/admin/outfit_manager.dm new file mode 100644 index 0000000000000..9d20b6454723e --- /dev/null +++ b/code/modules/admin/outfit_manager.dm @@ -0,0 +1,73 @@ +/client/proc/outfit_manager() + set category = "Debug" + set name = "Outfit Manager" + + if(!check_rights(R_DEBUG)) + return + var/datum/outfit_manager/ui = new(usr) + ui.ui_interact(usr) + + +/datum/outfit_manager + var/client/owner + +/datum/outfit_manager/New(user) + owner = CLIENT_FROM_VAR(user) + +/datum/outfit_manager/ui_state(mob/user) + return GLOB.admin_state + +/datum/outfit_manager/ui_close(mob/user) + qdel(src) + +/datum/outfit_manager/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "OutfitManager") + ui.open() + +/datum/outfit_manager/proc/entry(datum/outfit/outfit) + var/vv = FALSE + var/datum/outfit/varedit/varoutfit = outfit + if(istype(varoutfit)) + vv = length(varoutfit.vv_values) + return list( + "name" = "[outfit.name] [vv ? "(VV)" : ""]", + "ref" = REF(outfit), + ) + +/datum/outfit_manager/ui_data(mob/user) + var/list/data = list() + + var/list/outfits = list() + for(var/datum/outfit/custom_outfit in GLOB.custom_outfits) + outfits += list(entry(custom_outfit)) + data["outfits"] = outfits + + return data + +/datum/outfit_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return + . = TRUE + + switch(action) + if("new") + owner.open_outfit_editor(new /datum/outfit) + if("load") + owner.holder.load_outfit(owner.mob) + if("copy") + var/datum/outfit/outfit = tgui_input_list(owner, "Pick an outfit to copy from", "Outfit Manager", subtypesof(/datum/outfit)) + if(ispath(outfit)) + owner.open_outfit_editor(new outfit) + + var/datum/outfit/target_outfit = locate(params["outfit"]) + if(!istype(target_outfit)) + return + switch(action) //wow we're switching through action again this is horrible optimization smh + if("edit") + owner.open_outfit_editor(target_outfit) + if("save") + owner.holder.save_outfit(owner.mob, target_outfit) + if("delete") + owner.holder.delete_outfit(owner.mob, target_outfit) diff --git a/code/modules/admin/outfits.dm b/code/modules/admin/outfits.dm index 94fa17f4ecd57..cd2b102311906 100644 --- a/code/modules/admin/outfits.dm +++ b/code/modules/admin/outfits.dm @@ -1,35 +1,14 @@ GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits -/client/proc/outfit_manager() - set category = "Debug" - set name = "Outfit Manager" - - if(!check_rights(R_DEBUG)) - return - holder.outfit_manager(usr) - -/datum/admins/proc/outfit_manager(mob/admin) - var/list/dat = list("
    ") - for(var/datum/outfit/O in GLOB.custom_outfits) - var/vv = FALSE - var/datum/outfit/varedit/VO = O - if(istype(VO)) - vv = length(VO.vv_values) - dat += "
  • [O.name][vv ? "(VV)" : ""]
  • Save Delete" - dat += "
" - dat += "Create
" - dat += "Load from file" - admin << browse(dat.Join(),"window=outfitmanager") - -/datum/admins/proc/save_outfit(mob/admin,datum/outfit/O) +/datum/admins/proc/save_outfit(mob/admin, datum/outfit/O) O.save_to_file(admin) - outfit_manager(admin) + SStgui.update_user_uis(admin) -/datum/admins/proc/delete_outfit(mob/admin,datum/outfit/O) +/datum/admins/proc/delete_outfit(mob/admin, datum/outfit/O) GLOB.custom_outfits -= O qdel(O) to_chat(admin,"Outfit deleted.") - outfit_manager(admin) + SStgui.update_user_uis(admin) /datum/admins/proc/load_outfit(mob/admin) var/outfit_file = input("Pick outfit json file:", "File") as null|file @@ -49,195 +28,3 @@ GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits to_chat(admin,"Malformed/Outdated file.") return GLOB.custom_outfits += O - outfit_manager(admin) - -/datum/admins/proc/create_outfit(mob/admin) - var/list/uniforms = typesof(/obj/item/clothing/under) - var/list/suits = typesof(/obj/item/clothing/suit) - var/list/gloves = typesof(/obj/item/clothing/gloves) - var/list/shoes = typesof(/obj/item/clothing/shoes) - var/list/headwear = typesof(/obj/item/clothing/head) - var/list/glasses = typesof(/obj/item/clothing/glasses) - var/list/masks = typesof(/obj/item/clothing/mask) - var/list/ids = typesof(/obj/item/card/id) - - var/uniform_select = "" - - var/suit_select = "" - - var/gloves_select = "" - - var/shoes_select = "" - - var/head_select = "" - - var/glasses_select = "" - - var/mask_select = "" - - var/id_select = "" - - var/dat = {" - Create Outfit -
- - [HrefTokenFormField()] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name: - -
Uniform: - [uniform_select] -
Suit: - [suit_select] -
Back: - -
Belt: - -
Gloves: - [gloves_select] -
Shoes: - [shoes_select] -
Head: - [head_select] -
Mask: - [mask_select] -
Ears: - -
Glasses: - [glasses_select] -
ID: - [id_select] -
Left Pocket: - -
Right Pocket: - -
Suit Store: - -
Right Hand: - -
Left Hand: - -
-
- -
- "} - admin << browse(dat, "window=dressup;size=550x600") - - -/datum/admins/proc/create_outfit_finalize(mob/admin, list/href_list) - var/datum/outfit/O = new - - O.name = href_list["outfit_name"] - O.uniform = text2path(href_list["outfit_uniform"]) - O.shoes = text2path(href_list["outfit_shoes"]) - O.gloves = text2path(href_list["outfit_gloves"]) - O.suit = text2path(href_list["outfit_suit"]) - O.head = text2path(href_list["outfit_head"]) - O.back = text2path(href_list["outfit_back"]) - O.mask = text2path(href_list["outfit_mask"]) - O.glasses = text2path(href_list["outfit_glasses"]) - O.r_hand = text2path(href_list["outfit_r_hand"]) - O.l_hand = text2path(href_list["outfit_l_hand"]) - O.suit_store = text2path(href_list["outfit_s_store"]) - O.l_pocket = text2path(href_list["outfit_l_pocket"]) - O.r_pocket = text2path(href_list["outfit_r_pocket"]) - O.id = text2path(href_list["outfit_id"]) - O.belt = text2path(href_list["outfit_belt"]) - O.ears = text2path(href_list["outfit_ears"]) - - GLOB.custom_outfits.Add(O) - message_admins("[key_name(usr)] created \"[O.name]\" outfit!") diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 9d69115188bd5..45b6d9c4ec50a 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1538,28 +1538,6 @@ else to_chat(usr, "You may only use this when the game is running.") - else if(href_list["create_outfit_finalize"]) - if(!check_rights(R_ADMIN)) - return - create_outfit_finalize(usr,href_list) - else if(href_list["load_outfit"]) - if(!check_rights(R_ADMIN)) - return - load_outfit(usr) - else if(href_list["create_outfit_menu"]) - if(!check_rights(R_ADMIN)) - return - create_outfit(usr) - else if(href_list["delete_outfit"]) - if(!check_rights(R_ADMIN)) - return - var/datum/outfit/O = locate(href_list["chosen_outfit"]) in GLOB.custom_outfits - delete_outfit(usr,O) - else if(href_list["save_outfit"]) - if(!check_rights(R_ADMIN)) - return - var/datum/outfit/O = locate(href_list["chosen_outfit"]) in GLOB.custom_outfits - save_outfit(usr,O) else if(href_list["set_selfdestruct_code"]) if(!check_rights(R_ADMIN)) return diff --git a/tgui/packages/tgui/interfaces/OutfitEditor.js b/tgui/packages/tgui/interfaces/OutfitEditor.js new file mode 100644 index 0000000000000..5fdedbbcf66b1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/OutfitEditor.js @@ -0,0 +1,146 @@ +import { useBackend } from '../backend'; +import { Box, Button, Icon, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +export const OutfitEditor = (props, context) => { + const { act, data } = useBackend(context); + const { outfit, saveable, dummy64 } = data; + return ( + + + +
+ +
+
+
+ ); +}; + +const OutfitSlot = (props, context) => { + const { act, data } = useBackend(context); + const { name, icon, iconRot, slot } = props; + const { outfit } = data; + const currItem = outfit[slot]; + return ( + + + + {currItem?.sprite && ( + <> + + act("clear", { slot })} /> + + )} + + + {currItem?.name || "Empty"} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/OutfitManager.js b/tgui/packages/tgui/interfaces/OutfitManager.js new file mode 100644 index 0000000000000..6fa18ab175789 --- /dev/null +++ b/tgui/packages/tgui/interfaces/OutfitManager.js @@ -0,0 +1,80 @@ +import { useBackend } from '../backend'; +import { Button, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +export const OutfitManager = (props, context) => { + const { act, data } = useBackend(context); + const { outfits } = data; + return ( + + +
+
+
+
+ ); +}; From 49d7088f933ffc96be5f2c0b4b99c020d120a732 Mon Sep 17 00:00:00 2001 From: RKz Date: Mon, 11 Dec 2023 13:32:28 -0500 Subject: [PATCH 03/10] https://github.com/tgstation/tgstation/pull/58366 --- code/modules/admin/outfit_editor.dm | 10 +++------ code/modules/admin/verbs/selectequipment.dm | 15 +++++-------- code/modules/mob/living/carbon/human/dummy.dm | 22 +++++++++++++++++++ .../mob/living/carbon/human/human_defines.dm | 1 + 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/code/modules/admin/outfit_editor.dm b/code/modules/admin/outfit_editor.dm index 2540ba965dafd..0b2244ed57641 100644 --- a/code/modules/admin/outfit_editor.dm +++ b/code/modules/admin/outfit_editor.dm @@ -36,11 +36,7 @@ /datum/outfit_editor/proc/init_dummy() dummy_key = "outfit_editor_[owner]" - var/mob/living/carbon/human/dummy/dummy = generate_or_wait_for_human_dummy(dummy_key) - var/mob/living/carbon/carbon_target = owner.mob - if(istype(carbon_target)) - carbon_target.dna.transfer_identity(dummy) - dummy.updateappearance() + generate_dummy_lookalike(dummy_key, owner.mob) unset_busy_human_dummy(dummy_key) @@ -77,9 +73,9 @@ data["outfit"] = serialize_outfit() data["saveable"] = !GLOB.custom_outfits.Find(drip) - var/datum/preferences/prefs = owner.prefs + if(!dummy_key) + init_dummy() var/icon/dummysprite = get_flat_human_icon(null, - prefs = prefs, dummy_key = dummy_key, showDirs = list(SOUTH), outfit_override = drip) diff --git a/code/modules/admin/verbs/selectequipment.dm b/code/modules/admin/verbs/selectequipment.dm index 004fcc6ba5bb4..93c144f8e4787 100644 --- a/code/modules/admin/verbs/selectequipment.dm +++ b/code/modules/admin/verbs/selectequipment.dm @@ -63,12 +63,7 @@ /datum/select_equipment/proc/init_dummy() dummy_key = "selectequipmentUI_[target_mob]" - dummy = generate_or_wait_for_human_dummy(dummy_key) - var/mob/living/carbon/carbon_target = target_mob - if(istype(carbon_target)) - carbon_target.dna.transfer_identity(dummy) - dummy.updateappearance() - + generate_dummy_lookalike(dummy_key, target_mob) unset_busy_human_dummy(dummy_key) return @@ -112,14 +107,16 @@ /datum/select_equipment/ui_data(mob/user) var/list/data = list() - if(!dummy) + if(!dummy_key) init_dummy() - var/datum/preferences/prefs = target_mob?.client?.prefs - var/icon/dummysprite = get_flat_human_icon(null, prefs=prefs, dummy_key = dummy_key, outfit_override = selected_outfit) + var/icon/dummysprite = get_flat_human_icon(null, + dummy_key = dummy_key, + outfit_override = selected_outfit) data["icon64"] = icon2base64(dummysprite) data["name"] = target_mob + var/datum/preferences/prefs = user?.client?.prefs data["favorites"] = list() if(prefs) data["favorites"] = prefs.favorite_outfits diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm index aacd50ffd9bdb..8f59260d1ab05 100644 --- a/code/modules/mob/living/carbon/human/dummy.dm +++ b/code/modules/mob/living/carbon/human/dummy.dm @@ -79,6 +79,28 @@ GLOBAL_LIST_EMPTY(dummy_mob_list) D.in_use = TRUE return D +/proc/generate_dummy_lookalike(slotkey, mob/target) + if(!istype(target)) + return generate_or_wait_for_human_dummy(slotkey) + + var/mob/living/carbon/human/dummy/copycat = generate_or_wait_for_human_dummy(slotkey) + + if(iscarbon(target)) + var/mob/living/carbon/carbon_target = target + carbon_target.dna.transfer_identity(copycat, transfer_SE = TRUE) + + if(ishuman(target)) + var/mob/living/carbon/human/human_target = target + human_target.copy_clothing_prefs(copycat) + + copycat.updateappearance(icon_update=TRUE, mutcolor_update=TRUE, mutations_overlay_update=TRUE) + else + //even if target isn't a carbon, if they have a client we can make the + //dummy look like what their human would look like based on their prefs + target?.client?.prefs?.copy_to(copycat, icon_updates=TRUE, roundstart_checks=FALSE, character_setup=TRUE) + + return copycat + /proc/unset_busy_human_dummy(slotkey) if(!slotkey) return diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index de64632b7d7ee..89661820c307c 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -27,6 +27,7 @@ var/lip_style = null //no lipstick by default- arguably misleading, as it could be used for general makeup var/lip_color = "white" var/age = 30 //Player's age + //consider updating /mob/living/carbon/human/copy_clothing_prefs() if adding more of these var/underwear = "Nude" //Which underwear the player wants var/underwear_color = "000" var/undershirt = "Nude" //Which undershirt the player wants From a087bf65465f098c6a37f0f5caa40db4d18ee10c Mon Sep 17 00:00:00 2001 From: RKz Date: Mon, 11 Dec 2023 13:38:54 -0500 Subject: [PATCH 04/10] updates --- code/modules/admin/outfit_editor.dm | 16 +++-- code/modules/admin/outfit_manager.dm | 5 +- code/modules/admin/verbs/selectequipment.dm | 2 +- tgui/packages/tgui/interfaces/OutfitEditor.js | 72 ++++++++++++------- .../packages/tgui/interfaces/OutfitManager.js | 29 ++++---- 5 files changed, 80 insertions(+), 44 deletions(-) diff --git a/code/modules/admin/outfit_editor.dm b/code/modules/admin/outfit_editor.dm index 0b2244ed57641..dbb288a477a60 100644 --- a/code/modules/admin/outfit_editor.dm +++ b/code/modules/admin/outfit_editor.dm @@ -100,7 +100,7 @@ drip.vars[slot] = null if("rename") - var/newname = stripped_input(owner, "What do you want to name this outfit?", OUTFIT_EDITOR_NAME) + var/newname = tgui_input_text(owner, "What do you want to name this outfit?", OUTFIT_EDITOR_NAME) if(newname) drip.name = newname if("save") @@ -117,11 +117,11 @@ if(!choice) return if(!ispath(choice)) - alert(owner, "Invalid item", OUTFIT_EDITOR_NAME, "oh no") + tgui_alert(owner, "Invalid item", OUTFIT_EDITOR_NAME, list("oh no")) return if(initial(choice.icon_state) == null) //hacky check copied from experimentor code var/msg = "Warning: This item's icon_state is null, indicating it is very probably not actually a usable item." - if(alert(owner, msg, OUTFIT_EDITOR_NAME, "Use it anyway", "Cancel") != "Use it anyway") + if(tgui_alert(owner, msg, OUTFIT_EDITOR_NAME, list("Use it anyway", "Cancel")) != "Use it anyway") return if(drip.vars.Find(slot)) @@ -167,7 +167,7 @@ if(suit) suit = new suit //initial() doesn't like lists options = suit.allowed - if(!options.len) //nothing will happen, but don't let the user think it's broken + if(!length(options)) //nothing will happen, but don't let the user think it's broken to_chat(owner, "No options available for the current suit.") if("belt") @@ -189,8 +189,12 @@ if("r_pocket") choose_any_item(slot) - if(length(options)) - set_item(slot, tgui_input_list(owner, "Choose an item", OUTFIT_EDITOR_NAME, options)) + if(!length(options)) + return + var/option = tgui_input_list(owner, "Choose an item", OUTFIT_EDITOR_NAME, options) + if(isnull(option)) + return + set_item(slot, option) #undef OUTFIT_EDITOR_NAME diff --git a/code/modules/admin/outfit_manager.dm b/code/modules/admin/outfit_manager.dm index 9d20b6454723e..804acea31f4f9 100644 --- a/code/modules/admin/outfit_manager.dm +++ b/code/modules/admin/outfit_manager.dm @@ -58,7 +58,10 @@ owner.holder.load_outfit(owner.mob) if("copy") var/datum/outfit/outfit = tgui_input_list(owner, "Pick an outfit to copy from", "Outfit Manager", subtypesof(/datum/outfit)) - if(ispath(outfit)) + if(isnull(outfit)) + return + if(!ispath(outfit)) + return owner.open_outfit_editor(new outfit) var/datum/outfit/target_outfit = locate(params["outfit"]) diff --git a/code/modules/admin/verbs/selectequipment.dm b/code/modules/admin/verbs/selectequipment.dm index 93c144f8e4787..c35ff007dfd8a 100644 --- a/code/modules/admin/verbs/selectequipment.dm +++ b/code/modules/admin/verbs/selectequipment.dm @@ -210,7 +210,7 @@ else human_target = target if(human_target.l_store || human_target.r_store || human_target.s_store) //saves a lot of time for admins and coders alike - if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No") + if(tgui_alert(usr,"Drop Items in Pockets? No will delete them.", "Robust quick dress shop", list("Yes", "No")) == "No") delete_pocket = TRUE SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/tgui/packages/tgui/interfaces/OutfitEditor.js b/tgui/packages/tgui/interfaces/OutfitEditor.js index 5fdedbbcf66b1..bb3658fc318f8 100644 --- a/tgui/packages/tgui/interfaces/OutfitEditor.js +++ b/tgui/packages/tgui/interfaces/OutfitEditor.js @@ -6,9 +6,7 @@ export const OutfitEditor = (props, context) => { const { act, data } = useBackend(context); const { outfit, saveable, dummy64 } = data; return ( - + { src={`data:image/jpeg;base64,${dummy64}`} style={{ '-ms-interpolation-mode': 'nearest-neighbor', - }} /> + }} + />
- { color="transparent" icon="pencil-alt" title="Rename this outfit" - onClick={() => act("rename", {})} /> + onClick={() => act('rename', {})} + /> {outfit.name} @@ -44,20 +45,25 @@ export const OutfitEditor = (props, context) => { color="transparent" icon="info" tooltip="Ctrl-click a button to select *any* item instead of what will probably fit in that slot." - tooltipPosition="bottom-left" /> + tooltipPosition="bottom-start" + />
@@ -105,10 +125,12 @@ const OutfitSlot = (props, context) => { const currItem = outfit[slot]; return ( - @@ -121,13 +143,15 @@ const OutfitSlot = (props, context) => { title={currItem?.desc} style={{ '-ms-interpolation-mode': 'nearest-neighbor', - }} /> + }} + /> act("clear", { slot })} /> + onClick={() => act('clear', { slot })} + /> )}
@@ -139,7 +163,7 @@ const OutfitSlot = (props, context) => { 'text-overflow': 'ellipsis', }} title={currItem?.path}> - {currItem?.name || "Empty"} + {currItem?.name || 'Empty'} ); diff --git a/tgui/packages/tgui/interfaces/OutfitManager.js b/tgui/packages/tgui/interfaces/OutfitManager.js index 6fa18ab175789..5552fc2f2c437 100644 --- a/tgui/packages/tgui/interfaces/OutfitManager.js +++ b/tgui/packages/tgui/interfaces/OutfitManager.js @@ -6,10 +6,7 @@ export const OutfitManager = (props, context) => { const { act, data } = useBackend(context); const { outfits } = data; return ( - +
{ icon="file-upload" tooltip="Load an outfit from a file" tooltipPosition="left" - onClick={() => act("load")} /> + onClick={() => act('load')} + />
diff --git a/tgui/packages/tgui/interfaces/OutfitManager.js b/tgui/packages/tgui/interfaces/OutfitManager.js index 5552fc2f2c437..edcfb711c920d 100644 --- a/tgui/packages/tgui/interfaces/OutfitManager.js +++ b/tgui/packages/tgui/interfaces/OutfitManager.js @@ -26,12 +26,7 @@ export const OutfitManager = (props, context) => { tooltipPosition="left" onClick={() => act('copy')} /> - )} @@ -184,15 +155,16 @@ const CurrentlySelectedDisplay = (props, context) => { name={entry.favorite ? 'star' : 'star-o'} color="gold" style={{ cursor: 'pointer' }} - onClick={() => act('togglefavorite', { - path: entry.path, - })} /> + onClick={() => + act('togglefavorite', { + path: entry.path, + }) + } + /> )} - - Currently selected: - + Currently selected: { mr={0.8} lineHeight={2} color="green" - onClick={() => act('applyoutfit', { - path: current_outfit, - })}> + onClick={() => + act('applyoutfit', { + path: current_outfit, + }) + }> Confirm From b4903b61406ce7d6985c7ced03138c13e9245b4e Mon Sep 17 00:00:00 2001 From: RKz Date: Thu, 3 Oct 2024 13:56:24 -0400 Subject: [PATCH 10/10] useless constant --- tgui/packages/common/collections.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 8c0f19b09eb3d..7f5f1c0572366 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -149,8 +149,6 @@ export const sortStrings = sortBy(); export const range = (start: number, end: number): number[] => new Array(end - start).fill(null).map((_, index) => index + start); -export const sort = sortBy(); - /** * A fast implementation of reduce. */