diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 3d08f8d70992..87462aa73691 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -11,6 +11,9 @@
#define isweakref(D) (istype(D, /datum/weakref))
+GLOBAL_VAR_INIT(magic_appearance_detecting_image, new /image) // appearances are awful to detect safely, but this seems to be the best way ~ninjanomnom
+#define isappearance(thing) (!istype(thing, /image) && !ispath(thing) && istype(GLOB.magic_appearance_detecting_image, thing))
+
#define isgenerator(A) (istype(A, /generator))
//Turfs
diff --git a/code/modules/admin/view_variables/debug_variables.dm b/code/modules/admin/view_variables/debug_variables.dm
index f92d5de53650..0cf2f84168e6 100644
--- a/code/modules/admin/view_variables/debug_variables.dm
+++ b/code/modules/admin/view_variables/debug_variables.dm
@@ -41,6 +41,10 @@
item = "[name_part] = /icon ([value])"
#endif
+ else if(isappearance(value))
+ var/image/actually_an_appearance = value
+ item = "[name_part] = /appearance ([actually_an_appearance.icon])"
+
else if (isfile(value))
item = "[name_part] = '[value]'"
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 161931bb7b09..44d6e823132b 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -376,7 +376,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
body = new
// Without this, it doesn't show up in the menu
- body.appearance_flags &= ~KEEP_TOGETHER
+ // body.appearance_flags &= ~KEEP_TOGETHER // NON-MODULE CHANGE
/datum/preferences/proc/create_character_profiles()
var/list/profiles = list()
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index eb0b923554e5..b534566a5380 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -5,7 +5,7 @@
// You do not need to raise this if you are adding new values that have sane defaults.
// Only raise this value when changing the meaning/format/name/layout of an existing value
// where you would want the updater procs below to run
-#define SAVEFILE_VERSION_MAX 43
+#define SAVEFILE_VERSION_MAX 43.1
/*
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
diff --git a/maplestation_modules/code/__DEFINES/_module_defines.dm b/maplestation_modules/code/__DEFINES/_module_defines.dm
index c8dd1faf9724..7154e4b2982d 100644
--- a/maplestation_modules/code/__DEFINES/_module_defines.dm
+++ b/maplestation_modules/code/__DEFINES/_module_defines.dm
@@ -37,3 +37,6 @@
#define SOUND_NORMAL (1<<0)
#define SOUND_QUESTION (1<<1)
#define SOUND_EXCLAMATION (1<<2)
+
+/// Max loadout presets available
+#define MAX_LOADOUTS 5
diff --git a/maplestation_modules/code/modules/client/preferences/height.dm b/maplestation_modules/code/modules/client/preferences/height.dm
index 9331a75cca01..5a67a7cde9d3 100644
--- a/maplestation_modules/code/modules/client/preferences/height.dm
+++ b/maplestation_modules/code/modules/client/preferences/height.dm
@@ -37,7 +37,7 @@
return
// Snowflake, but otherwise the dummy in the prefs menu will be resized and you can't see anything
- if(istype(target, /mob/living/carbon/human/dummy))
+ if(isdummy(target))
return
// Just in case
if(!ishuman(target))
@@ -143,6 +143,19 @@
#undef SIZE_PREF_PRIORITY
#undef HEIGHT_PREF_PRIORITY
+// To speed up the preference menu, we apply 1 filter to the entire mob
+/mob/living/carbon/human/dummy/regenerate_icons()
+ . = ..()
+ apply_height_filters(src, TRUE)
+
+/mob/living/carbon/human/dummy/apply_height_filters(mutable_appearance/appearance, only_apply_in_prefs = FALSE)
+ if(only_apply_in_prefs)
+ return ..()
+
+// Not necessary with above
+/mob/living/carbon/human/dummy/apply_height_offsets(mutable_appearance/appearance, upper_torso)
+ return
+
/mob/living/carbon/human/get_mob_height()
// If you have roundstart dwarfism (IE: resized), it'll just return normal mob height, so no filters are applied
if(HAS_TRAIT_FROM_ONLY(src, TRAIT_DWARF, ROUNDSTART_TRAIT))
diff --git a/maplestation_modules/code/modules/client/preferences/loadout_preference.dm b/maplestation_modules/code/modules/client/preferences/loadout_preference.dm
index 65597c07bf55..7b6f5d2aa6d6 100644
--- a/maplestation_modules/code/modules/client/preferences/loadout_preference.dm
+++ b/maplestation_modules/code/modules/client/preferences/loadout_preference.dm
@@ -1,3 +1,16 @@
+/datum/preference/numeric/active_loadout
+ savefile_key = "active_loadout"
+ savefile_identifier = PREFERENCE_CHARACTER
+ can_randomize = FALSE
+ minimum = 1
+ maximum = MAX_LOADOUTS
+
+/datum/preference/numeric/active_loadout/create_default_value()
+ return minimum
+
+/datum/preference/numeric/active_loadout/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
/datum/preference/loadout
savefile_key = "loadout_list"
savefile_identifier = PREFERENCE_CHARACTER
@@ -15,21 +28,56 @@
if(!istype(target))
return // Not a crash, 'cause this proc could be passed non-humans (AIs, etc) and that's fine
- for(var/datum/loadout_item/item as anything in loadout_list_to_datums(value))
+ var/slot = prefs.read_preference(/datum/preference/numeric/active_loadout)
+ for(var/datum/loadout_item/item as anything in loadout_list_to_datums(value[slot]))
item.post_equip_item(prefs, target)
-/datum/preference/loadout/serialize(input, datum/preferences/preferences)
- // Sanitize on save even though it's highly unlikely this will need it
- return sanitize_loadout_list(input)
-
/datum/preference/loadout/deserialize(input, datum/preferences/preferences)
// Sanitize on load to ensure no invalid paths from older saves get in
- // Pass in the prefernce owner so they can get feedback messages on stuff that failed to load (if they exist)
- return sanitize_loadout_list(input, preferences.parent?.mob)
+ var/slot = preferences.read_preference(/datum/preference/numeric/active_loadout)
-// Default value is NULL - the loadout list is a lazylist
+ for(var/i in 1 to length(input))
+ if(islist(input[i]))
+ // Pass in the prefernce owner so they can get feedback messages on stuff that failed to load (if they exist)
+ input[i] = sanitize_loadout_list(input[i], preferences.parent?.mob, slot)
+
+ return input
+
+// Default value is null - the loadout list is a lazylist
/datum/preference/loadout/create_default_value(datum/preferences/preferences)
return null
/datum/preference/loadout/is_valid(value)
return isnull(value) || islist(value)
+
+/**
+ * Removes all invalid paths from loadout lists.
+ * This is a general sanitization for preference loading.
+ *
+ * returns a list, or null if empty
+ */
+/datum/preference/loadout/proc/sanitize_loadout_list(list/passed_list, mob/optional_loadout_owner, loadout_slot)
+ var/list/sanitized_list
+ for(var/path in passed_list)
+ // Loading from json has each path in the list as a string that we need to convert back to typepath
+ var/obj/item/real_path = istext(path) ? text2path(path) : path
+ if(!ispath(real_path))
+ to_chat(optional_loadout_owner, span_boldnotice("The following invalid item path was found in loadout slot [loadout_slot]: [real_path || "null"]. \
+ It has been removed, renamed, or is otherwise missing - You may want to check your loadout settings."))
+ continue
+
+ else if(!istype(GLOB.all_loadout_datums[real_path], /datum/loadout_item))
+ to_chat(optional_loadout_owner, span_boldnotice("The following invalid loadout item was found in loadout slot [loadout_slot]: [real_path || "null"]. \
+ It has been removed, renamed, or is otherwise missing - You may want to check your loadout settings."))
+ continue
+
+ // Set into sanitize list using converted path key
+ var/list/data = passed_list[path]
+ LAZYSET(sanitized_list, real_path, LAZYLISTDUPLICATE(data))
+
+ return sanitized_list
+
+/datum/preferences/update_character(current_version, list/save_data)
+ . = ..()
+ if(current_version < 43.1)
+ save_loadout(src, save_data?["loadout_list"])
diff --git a/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm b/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm
index 7a57b5d6b155..dfd97cf3a287 100644
--- a/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm
+++ b/maplestation_modules/code/modules/loadouts/loadout_items/_loadout_datum.dm
@@ -93,7 +93,7 @@ GLOBAL_LIST_EMPTY(all_loadout_datums)
to_chat(user, span_warning("You already have a greyscaling window open!"))
return
- var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
+ var/list/loadout = get_active_loadout(manager.preferences)
var/list/allowed_configs = list()
if(initial(item_path.greyscale_config))
allowed_configs += "[initial(item_path.greyscale_config)]"
@@ -122,7 +122,7 @@ GLOBAL_LIST_EMPTY(all_loadout_datums)
if(!istype(open_menu))
CRASH("set_slot_greyscale called without a greyscale menu!")
- var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
+ var/list/loadout = get_active_loadout(manager.preferences)
if(!loadout?[item_path])
manager.select_item(src)
@@ -131,11 +131,11 @@ GLOBAL_LIST_EMPTY(all_loadout_datums)
return
loadout[item_path][INFO_GREYSCALE] = colors.Join("")
- manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
+ update_loadout(manager.preferences, loadout)
manager.character_preview_view.update_body()
/datum/loadout_item/proc/set_name(datum/preference_middleware/loadout/manager, mob/user)
- var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
+ var/list/loadout = get_active_loadout(manager.preferences)
var/input_name = tgui_input_text(
user = user,
message = "What name do you want to give [name]? Leave blank to clear.",
@@ -154,10 +154,10 @@ GLOBAL_LIST_EMPTY(all_loadout_datums)
else
loadout[item_path] -= INFO_NAMED
- manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
+ update_loadout(manager.preferences, loadout)
/datum/loadout_item/proc/set_skin(datum/preference_middleware/loadout/manager, mob/user)
- var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
+ var/list/loadout = get_active_loadout(manager.preferences)
var/static/list/list/cached_reskins = list()
if(!islist(cached_reskins[item_path]))
var/obj/item/item_template = new item_path()
@@ -185,7 +185,7 @@ GLOBAL_LIST_EMPTY(all_loadout_datums)
else
loadout[item_path][INFO_RESKIN] = input_skin
- manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
+ update_loadout(manager.preferences, loadout)
/**
* Place our [var/item_path] into [outfit].
@@ -232,7 +232,7 @@ GLOBAL_LIST_EMPTY(all_loadout_datums)
else
// Not valid
item_details -= INFO_RESKIN
- preference_source.write_preference(GLOB.preference_entries[/datum/preference/loadout], preference_list)
+ save_loadout(preference_source, preference_list)
return equipped_item
diff --git a/maplestation_modules/code/modules/loadouts/loadout_items/loadout_datum_accessory.dm b/maplestation_modules/code/modules/loadouts/loadout_items/loadout_datum_accessory.dm
index 7d233e4248ca..9e96a7fe89c7 100644
--- a/maplestation_modules/code/modules/loadouts/loadout_items/loadout_datum_accessory.dm
+++ b/maplestation_modules/code/modules/loadouts/loadout_items/loadout_datum_accessory.dm
@@ -35,7 +35,7 @@
return ..()
/datum/loadout_item/accessory/proc/set_accessory_layer(datum/preference_middleware/loadout/manager, mob/user)
- var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
+ var/list/loadout = get_active_loadout(manager.preferences)
if(!loadout?[item_path])
manager.select_item(src)
@@ -44,7 +44,7 @@
loadout[item_path][INFO_LAYER] = !loadout[item_path][INFO_LAYER]
to_chat(user, span_boldnotice("[name] will now appear [loadout[item_path][INFO_LAYER] ? "above" : "below"] suits."))
- manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
+ update_loadout(manager.preferences, loadout)
/datum/loadout_item/accessory/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
if(outfit.accessory)
diff --git a/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_manager.dm b/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_manager.dm
index ecc9bf6635cd..af5b18a7cb8d 100644
--- a/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_manager.dm
+++ b/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_manager.dm
@@ -73,6 +73,7 @@ GLOBAL_LIST_INIT(loadout_categories, init_loadout_categories())
"toggle_job_clothes" = PROC_REF(action_toggle_job_outfit),
"rotate_dummy" = PROC_REF(action_rotate_model_dir),
"pass_to_loadout_item" = PROC_REF(action_pass_to_loadout_item),
+ "select_slot" = PROC_REF(select_slot),
)
/// The preview dummy.
@@ -112,7 +113,7 @@ GLOBAL_LIST_INIT(loadout_categories, init_loadout_categories())
return TRUE
/datum/preference_middleware/loadout/proc/action_clear_all(list/params, mob/user)
- preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], null)
+ update_loadout(preferences, null)
character_preview_view.update_body()
return TRUE
@@ -143,7 +144,7 @@ GLOBAL_LIST_INIT(loadout_categories, init_loadout_categories())
/// Select [path] item to [category_slot] slot.
/datum/preference_middleware/loadout/proc/select_item(datum/loadout_item/selected_item)
- var/list/loadout = preferences.read_preference(/datum/preference/loadout)
+ var/list/loadout = get_active_loadout(preferences)
var/list/datum/loadout_item/loadout_datums = loadout_list_to_datums(loadout)
for(var/datum/loadout_item/item as anything in loadout_datums)
if(item.category != selected_item.category)
@@ -153,13 +154,13 @@ GLOBAL_LIST_INIT(loadout_categories, init_loadout_categories())
return
LAZYSET(loadout, selected_item.item_path, list())
- preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
+ update_loadout(preferences, loadout)
/// Deselect [deselected_item].
/datum/preference_middleware/loadout/proc/deselect_item(datum/loadout_item/deselected_item)
- var/list/loadout = preferences.read_preference(/datum/preference/loadout)
+ var/list/loadout = get_active_loadout(preferences)
LAZYREMOVE(loadout, deselected_item.item_path)
- preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
+ update_loadout(preferences, loadout)
/datum/preference_middleware/loadout/proc/register_greyscale_menu(datum/greyscale_modify_menu/open_menu)
src.menu = open_menu
@@ -169,6 +170,12 @@ GLOBAL_LIST_INIT(loadout_categories, init_loadout_categories())
SIGNAL_HANDLER
menu = null
+/datum/preference_middleware/loadout/proc/select_slot(list/params, mob/user)
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/numeric/active_loadout], text2num(params["new_slot"]))
+ character_preview_view.update_body()
+ preferences.character_preview_view?.update_body()
+ preferences.update_static_data(user)
+
/datum/preference_middleware/loadout/get_ui_data(mob/user)
var/list/data = list()
@@ -176,12 +183,13 @@ GLOBAL_LIST_INIT(loadout_categories, init_loadout_categories())
character_preview_view = create_character_preview_view(user)
var/list/all_selected_paths = list()
- for(var/path in preferences.read_preference(/datum/preference/loadout))
+ for(var/path in get_active_loadout(preferences))
all_selected_paths += path
data["selected_loadout"] = all_selected_paths
data["mob_name"] = preferences.read_preference(/datum/preference/name/real_name)
data["job_clothes"] = character_preview_view.view_job_clothes
+ data["current_slot"] = preferences.read_preference(/datum/preference/numeric/active_loadout)
return data
@@ -202,6 +210,7 @@ GLOBAL_LIST_INIT(loadout_categories, init_loadout_categories())
data["loadout_tabs"] = loadout_tabs
data["tutorial_text"] = get_tutorial_text()
+ data["max_loadout_slots"] = MAX_LOADOUTS
return data
/// Returns a formatted string for use in the UI.
diff --git a/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_outfit_helpers.dm b/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_outfit_helpers.dm
index eeee16902338..17c1b2e1d654 100644
--- a/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_outfit_helpers.dm
+++ b/maplestation_modules/code/modules/loadouts/loadout_ui/loadout_outfit_helpers.dm
@@ -18,6 +18,9 @@
* preference_source - the preferences of the thing we're equipping
*/
/mob/living/carbon/human/proc/equip_outfit_and_loadout(datum/outfit/outfit, datum/preferences/preference_source, visuals_only = FALSE)
+ if(isnull(preference_source))
+ return equipOutfit(outfit, visuals_only)
+
var/datum/outfit/equipped_outfit
if(ispath(outfit))
@@ -27,13 +30,14 @@
else
CRASH("Outfit passed to equip_outfit_and_loadout was neither a path nor an instantiated type!")
- var/list/preference_list = preference_source?.read_preference(/datum/preference/loadout)
+ var/list/preference_list = get_active_loadout(preference_source)
var/list/loadout_datums = loadout_list_to_datums(preference_list)
// Place any loadout items into the outfit before going forward
for(var/datum/loadout_item/item as anything in loadout_datums)
item.insert_path_into_outfit(equipped_outfit, src, visuals_only)
// Equip the outfit loadout items included
- equipOutfit(equipped_outfit, visuals_only)
+ if(!equipped_outfit.equip(src, visuals_only))
+ return FALSE
// Handle any snowflake on_equips
for(var/datum/loadout_item/item as anything in loadout_datums)
item.on_equip_item(preference_source, src, visuals_only, preference_list)
@@ -66,41 +70,42 @@
return datums
/**
- * Removes all invalid paths from loadout lists.
- * This is a general sanitization for preference saving / loading.
- *
- * passed_list - the loadout list we're sanitizing.
+ * Gets the active loadout of the passed preference source.
*
- * returns a list, or null if empty
+ * Returns a loadout lazylist
*/
-/proc/sanitize_loadout_list(list/passed_list, mob/optional_loadout_owner)
- var/list/sanitized_list
- for(var/path in passed_list)
- // Saving to json has each path in the list as a typepath that will be converted to string
- // Loading from json has each path in the list as a string that we need to convert back to typepath
- var/obj/item/real_path = istext(path) ? text2path(path) : path
- if(!ispath(real_path))
- #ifdef TESTING
- // These stack traces are only useful in testing to find out why items aren't being saved when they should be
- // In a production setting it should be OKAY for the sanitize proc to pick out invalid paths
- stack_trace("invalid path found in loadout list! (Path: [path])")
- #endif
- to_chat(optional_loadout_owner, span_boldnotice("The following invalid item path was found in your loadout: [real_path || "null"]. \
- It has been removed, renamed, or is otherwise missing - You may want to check your loadout settings."))
- continue
+/proc/get_active_loadout(datum/preferences/preferences)
+ RETURN_TYPE(/list)
+ var/slot = preferences.read_preference(/datum/preference/numeric/active_loadout)
+ var/list/all_loadouts = preferences.read_preference(/datum/preference/loadout)
+ if(slot > length(all_loadouts))
+ return null
+ return all_loadouts[slot]
- else if(!istype(GLOB.all_loadout_datums[real_path], /datum/loadout_item))
- #ifdef TESTING
- // Same as above, stack trace only useful in testing to find out why items aren't being saved when they should be
- stack_trace("invalid loadout item found in loadout list! Path: [path]")
- #endif
- to_chat(optional_loadout_owner, span_boldnotice("The following invalid loadout item was found in your loadout: [real_path || "null"]. \
- It has been removed, renamed, or is otherwise missing - You may want to check your loadout settings."))
- continue
+/**
+ * Calls update_preference on the passed preference datum with the passed loadout list
+ */
+/proc/update_loadout(datum/preferences/preferences, list/loadout_list)
+ preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], get_updated_loadout_list(preferences, loadout_list))
- // Grab data using real path key
- var/list/data = passed_list[path]
- // Set into sanitize list using converted path key
- LAZYSET(sanitized_list, real_path, LAZYCOPY(data))
+/**
+ * Calls write_preference on the passed preference datum with the passed loadout list
+ */
+/proc/save_loadout(datum/preferences/preferences, list/loadout_list)
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/loadout], get_updated_loadout_list(preferences, loadout_list))
+
+/**
+ * Returns a list of all loadouts belonging to the passed preference source,
+ * and appends the passed loadout list to the proper index of the list.
+ */
+/proc/get_updated_loadout_list(datum/preferences/preferences, list/loadout_list)
+ RETURN_TYPE(/list)
+ var/slot = preferences.read_preference(/datum/preference/numeric/active_loadout)
+ var/list/new_list = list()
+ for(var/list/loadout in preferences.read_preference(/datum/preference/loadout))
+ UNTYPED_LIST_ADD(new_list, loadout)
+ while(length(new_list) < slot)
+ new_list += null
- return sanitized_list
+ new_list[slot] = loadout_list
+ return new_list
diff --git a/tgui/packages/tgui/interfaces/_LoadoutManager.tsx b/tgui/packages/tgui/interfaces/_LoadoutManager.tsx
index d95775c825c5..93c3ce83e4d6 100644
--- a/tgui/packages/tgui/interfaces/_LoadoutManager.tsx
+++ b/tgui/packages/tgui/interfaces/_LoadoutManager.tsx
@@ -30,6 +30,8 @@ type Data = {
loadout_preview_view: string;
loadout_tabs: LoadoutCategory[];
tutorial_text: string;
+ current_slot: number;
+ max_loadout_slots: number;
};
export const LoadoutPage = (props, context) => {
@@ -297,8 +299,24 @@ const LoadoutTabs = (props, context) => {
const LoadoutPreviewSection = (props, context) => {
const { act, data } = useBackend(context);
- const { mob_name, job_clothes, loadout_preview_view } = data;
+ const {
+ mob_name,
+ job_clothes,
+ loadout_preview_view,
+ current_slot,
+ max_loadout_slots,
+ } = data;
+
const [tutorialStatus] = useLocalState(context, 'tutorialStatus', false);
+
+ const loadoutSlots = (maxSlots: number) => {
+ const slots: number[] = [];
+ for (let i = 1; i < maxSlots + 1; i++) {
+ slots.push(i);
+ }
+ return slots;
+ };
+
return (
{
)}
+
+
+ {loadoutSlots(max_loadout_slots).map((slot) => (
+
+
+ ))}
+
+