Files
Timothy TeakettleandGitHub 920bcbed82 loadout sanitization fixes (#7432)
## About The Pull Request
loadout name/desc is stored not encoded and is encoded when it's applied
to the item
we still do a length check on it

collars don't reset their name when you apply a tag now, by storing what
it considers its initial name

## Why It's Good For The Game
fixes bugs

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Please note that
maintainers freely reserve the right to remove and add tags should they
deem it appropriate. You can attempt to finagle the system all you want,
but it's best to shoot for clear communication right off the bat. -->

🆑
fix: loadout items with a changed name/desc should no longer double
sanitize
fix: collars don't reset their name when you apply a tag now
🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->
2025-12-17 12:02:08 -05:00

468 lines
17 KiB
Plaintext

/datum/category_group/player_setup_category/loadout_preferences
name = "Loadout"
sort_order = 5
category_item_type = /datum/category_item/player_setup_item/loadout
/datum/category_group/player_setup_category/loadout_preferences/override_tab_to(mob/user)
var/datum/category_item/player_setup_item/loadout/gear/entry = locate() in items
entry.ui_interact(user)
return TRUE
/datum/category_item/player_setup_item/loadout
abstract_type = /datum/category_item/player_setup_item/loadout
/datum/category_item/player_setup_item/loadout/slot
name = "Loadout Slot"
save_key = CHARACTER_DATA_LOADOUT_SLOT
/datum/category_item/player_setup_item/loadout/slot/default_value(randomizing)
return 1
/datum/category_item/player_setup_item/loadout/slot/filter_data(datum/preferences/prefs, data, list/errors)
return sanitize_integer(data, 1, LOADOUT_MAX_SLOTS, 1)
/datum/category_item/player_setup_item/loadout/gear
name = "Loadout"
save_key = CHARACTER_DATA_LOADOUT
/datum/category_item/player_setup_item/loadout/gear/filter_data(datum/preferences/prefs, data, list/errors)
var/list/slots = sanitize_islist(data)
if(length(slots) > LOADOUT_MAX_SLOTS)
slots.len = LOADOUT_MAX_SLOTS
var/list/datum/loadout_entry/valid_entries = valid_loadout_entries(prefs)
for(var/i in 1 to LOADOUT_MAX_SLOTS)
var/numkey = num2text(i)
if(isnull(slots[numkey]))
continue
var/list/slot = (slots[numkey] = sanitize_islist(slots[numkey]))
var/list/dedupe = list()
if(length(slot) > LOADOUT_MAX_ITEMS)
slot.len = LOADOUT_MAX_ITEMS
var/list/slot_entries = slot[LOADOUT_SLOTDATA_ENTRIES]
for(var/id in slot_entries)
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
slot_entries -= id
errors?.Add("Could not find loadout entry id '[id]'")
continue
if(dedupe[id])
slot_entries -= id
errors?.Add("Fatal: Removed duplicate loadout entry for id '[id]'")
continue
else
dedupe[id] = TRUE
if(!(entry in valid_entries))
slot_entries -= id
errors?.Add("Not allowed to take loadout entry id '[id]")
continue
// commented out - /datum/loadout_entry checks this on spawn.
/*
var/list/entry_data = slot_entries[id]
for(var/datakey in entry_data)
switch(datakey)
if(LOADOUT_ENTRYDATA_RECOLOR)
if(!(entry.loadout_customize_flags & LOADOUT_CUSTOMIZE_COLOR))
entry_data -= LOADOUT_ENTRYDATA_RECOLOR
errors?.Add("Loadout entry id '[id]' does not allow recoloring.")
if(LOADOUT_ENTRYDATA_RENAME)
if(!(entry.loadout_customize_flags & LOADOUT_CUSTOMIZE_NAME))
entry_data -= LOADOUT_ENTRYDATA_RENAME
errors?.Add("Loadout entry id '[id]' does not allow renaming.")
if(LOADOUT_ENTRYDATA_REDESC)
if(!(entry.loadout_customize_flags & LOADOUT_CUSTOMIZE_DESC))
entry_data -= LOADOUT_ENTRYDATA_REDESC
errors?.Add("Loadout entry id '[id]' does not allow setting description.")
else
// else we just don't care because gear tweaks need to sanitize their own stuff.
*/
return slots
/datum/category_item/player_setup_item/loadout/gear/spawn_checks(datum/preferences/prefs, data, flags, list/errors, list/warnings)
var/list/slots = sanitize_islist(data)
var/slot_index = prefs.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/list/slot = SAFEINDEXACCESS(slots, slot_index)
if(!islist(slot))
return TRUE
var/max_cost = max_loadout_cost()
var/current_cost = 0
. = TRUE
for(var/id in slot)
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
errors?.Add("Could not find loadout item [id].")
. = FALSE
if(current_cost + entry.cost > max_cost)
if(current_cost <= max_cost)
// only when going over.
errors?.Add("Insufficient loadout points for all items selected.")
. = FALSE
current_cost += entry.cost
/datum/category_item/player_setup_item/loadout/gear/ui_static_data(mob/user, datum/tgui/ui)
. = ..()
.["gearContext"] = tgui_loadout_context()
var/list/allowed_ids = list()
for(var/datum/loadout_entry/entry as anything in valid_loadout_entries(pref))
allowed_ids += entry.legacy_get_id()
.["gearAllowed"] = allowed_ids
.["gearData"] = tgui_loadout_data()
.["characterName"] = pref.real_name
/datum/category_item/player_setup_item/loadout/gear/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "CharacterLoadoutStandalone")
ui.open()
/datum/category_item/player_setup_item/loadout/gear/ui_status(mob/user, datum/ui_state/state)
return UI_INTERACTIVE
/datum/category_item/player_setup_item/loadout/gear/proc/tgui_loadout_selected(list/loadout_slot)
. = list()
.["name"] = loadout_slot[LOADOUT_SLOTDATA_NAME]
var/cost_used = 0
var/cost_max = max_loadout_cost()
var/list/our_entries = loadout_slot[LOADOUT_SLOTDATA_ENTRIES]
var/list/entries = list()
var/list/cost_categories = list()
var/list/cost_subcategories = list()
.["entries"] = entries
for(var/id in our_entries)
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
our_entries -= id
continue
cost_used += entry.cost
var/list/transformed = our_entries[id]
transformed = transformed.Copy()
var/list/tweak_texts = list()
for(var/datum/loadout_tweak/tweak as anything in entry.tweaks)
tweak_texts[tweak.id] = tweak.get_contents(our_entries[id][LOADOUT_ENTRYDATA_TWEAKS]?[tweak.id] || tweak.get_default())
transformed["tweakTexts"] = tweak_texts
entries[entry.legacy_get_id()] = transformed
cost_categories[entry.category] += entry.cost
LAZYINITLIST(cost_subcategories[entry.category])
cost_subcategories[entry.category][entry.subcategory] += entry.cost
.["costUsed"] = cost_used
.["costMax"] = cost_max
.["costCategories"] = cost_categories
.["costSubcategories"] = cost_subcategories
/datum/category_item/player_setup_item/loadout/gear/proc/tgui_loadout_data()
. = list()
var/list/slots = list()
var/slot_index = pref.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/list/all_slots = pref.get_character_data(CHARACTER_DATA_LOADOUT)
for(var/i in 1 to LOADOUT_MAX_SLOTS)
var/list/the_slot = all_slots["[i]"]
slots[++slots.len] = list(
"name" = the_slot?[LOADOUT_SLOTDATA_NAME] || "Slot [i]"
)
var/list/the_slot = all_slots["[slot_index]"] || list()
.["slots"] = slots
.["slot"] = tgui_loadout_selected(the_slot)
.["slotIndex"] = slot_index
/datum/category_item/player_setup_item/loadout/gear/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state, datum/event_args/actor/actor)
. = ..()
if(.)
return
// todo: we really need a better way, tgui prefs when lol
if(pref.client_ckey != usr.ckey && !check_rights(usr))
return
switch(action)
if("toggle")
var/id = params["id"]
if(!id)
return TRUE
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
return TRUE
var/list/loadout = pref.get_character_data(CHARACTER_DATA_LOADOUT)
loadout = loadout.Copy()
var/slot_index = pref.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/list/slot = loadout["[slot_index]"]
slot = slot?.Copy() || list()
loadout["[slot_index]"] = slot
var/list/entries = slot[LOADOUT_SLOTDATA_ENTRIES]
if(isnull(entries))
slot[LOADOUT_SLOTDATA_ENTRIES] = (entries = list())
else
entries = entries.Copy()
slot[LOADOUT_SLOTDATA_ENTRIES] = entries
if(entries[id])
entries -= id
else
entries[id] = list()
pref.set_character_data(CHARACTER_DATA_LOADOUT, loadout)
push_loadout_data()
pref.update_character_previews()
return TRUE
if("rename")
var/id = params["id"]
if(isnull(id))
return TRUE
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
return TRUE
var/list/loadout = pref.get_character_data(CHARACTER_DATA_LOADOUT)
loadout = loadout.Copy()
var/slot_index = pref.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/list/slot = loadout["[slot_index]"]
slot = slot?.Copy() || list()
loadout["[slot_index]"] = slot
var/list/entries = slot[LOADOUT_SLOTDATA_ENTRIES]
var/list/entry_data = entries[id]
if(isnull(entry_data))
return TRUE
entry_data = entry_data.Copy()
entries[id] = entry_data
if(!(entry.loadout_customize_flags & LOADOUT_CUSTOMIZE_NAME))
return TRUE
var/name = params["name"]
if(length(name) > MAX_NAME_LEN)
return TRUE
if(isnull(name))
return TRUE
if(name)
entry_data[LOADOUT_ENTRYDATA_RENAME] = name
else
entry_data -= LOADOUT_ENTRYDATA_RENAME
pref.set_character_data(CHARACTER_DATA_LOADOUT, loadout)
push_loadout_data()
return TRUE
if("redesc")
var/id = params["id"]
if(isnull(id))
return TRUE
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
return TRUE
var/list/loadout = pref.get_character_data(CHARACTER_DATA_LOADOUT)
loadout = loadout.Copy()
var/slot_index = pref.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/list/slot = loadout["[slot_index]"]
slot = slot?.Copy() || list()
loadout["[slot_index]"] = slot
var/list/entries = slot[LOADOUT_SLOTDATA_ENTRIES]
var/list/entry_data = entries[id]
if(isnull(entry_data))
return TRUE
entry_data = entry_data.Copy()
entries[id] = entry_data
if(!(entry.loadout_customize_flags & LOADOUT_CUSTOMIZE_DESC))
return TRUE
var/desc = params["desc"]
if(length(desc) > MAX_MESSAGE_LEN)
return TRUE
if(isnull(desc))
return TRUE
if(desc)
entry_data[LOADOUT_ENTRYDATA_REDESC] = desc
else
entry_data -= LOADOUT_ENTRYDATA_REDESC
pref.set_character_data(CHARACTER_DATA_LOADOUT, loadout)
push_loadout_data()
return TRUE
if("recolor")
var/id = params["id"]
if(isnull(id))
return TRUE
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
return TRUE
var/list/loadout = pref.get_character_data(CHARACTER_DATA_LOADOUT)
loadout = loadout.Copy()
var/slot_index = pref.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/list/slot = loadout["[slot_index]"]
slot = slot?.Copy() || list()
loadout["[slot_index]"] = slot
var/list/entries = slot[LOADOUT_SLOTDATA_ENTRIES]
var/list/entry_data = entries[id]
if(isnull(entry_data))
return TRUE
entry_data = entry_data.Copy()
entries[id] = entry_data
if(!(entry.loadout_customize_flags & LOADOUT_CUSTOMIZE_COLOR))
return TRUE
var/color = sanitize_probably_a_byond_color(params["color"], null)
if(!isnull(color) && (color != "#ffffff"))
entry_data[LOADOUT_ENTRYDATA_RECOLOR] = color
else
entry_data -= LOADOUT_ENTRYDATA_RECOLOR
pref.set_character_data(CHARACTER_DATA_LOADOUT, loadout)
push_loadout_data()
pref.update_character_previews()
return TRUE
if("tweak")
var/id = params["id"]
if(!id)
return TRUE
var/datum/loadout_entry/entry = global.gear_datums[id]
if(isnull(entry))
return TRUE
var/list/loadout = pref.get_character_data(CHARACTER_DATA_LOADOUT)
loadout = loadout.Copy()
var/slot_index = pref.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/list/slot = loadout["[slot_index]"]
slot = slot?.Copy() || list()
loadout["[slot_index]"] = slot
var/list/entries = slot[LOADOUT_SLOTDATA_ENTRIES]
var/list/entry_data = entries[id]
if(isnull(entry_data))
return TRUE
entry_data = entry_data.Copy()
entries[id] = entry_data
var/list/tweak_data = entry_data[LOADOUT_ENTRYDATA_TWEAKS] || list()
tweak_data = deep_copy_list(tweak_data)
entry_data[LOADOUT_ENTRYDATA_TWEAKS] = tweak_data
var/tweak_id = params["tweakId"]
for(var/datum/loadout_tweak/tweak as anything in entry.tweaks)
if(tweak.id != tweak_id)
continue
tweak_data[tweak.id] = tweak.get_metadata(usr, tweak_data[tweak.id])
pref.set_character_data(CHARACTER_DATA_LOADOUT, loadout)
push_loadout_data()
pref.update_character_previews()
return TRUE
if("clear")
var/list/loadout = pref.get_character_data(CHARACTER_DATA_LOADOUT)
loadout = loadout.Copy()
var/slot_index = pref.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
var/wanted_index = text2num(params["index"])
if(slot_index != wanted_index)
return TRUE
var/list/slot = loadout["[slot_index]"]
slot = slot.Copy()
loadout["[slot_index]"] = slot
slot[LOADOUT_SLOTDATA_ENTRIES] = list()
pref.set_character_data(CHARACTER_DATA_LOADOUT, loadout)
push_loadout_data()
pref.update_character_previews()
return TRUE
if("slot")
var/index = text2num(params["index"])
if(!index)
return TRUE
if(index > LOADOUT_MAX_SLOTS)
return TRUE
pref.set_character_data(CHARACTER_DATA_LOADOUT_SLOT, index)
push_loadout_data()
pref.update_character_previews()
return TRUE
if("slotName")
var/index = text2num(params["index"])
if(!ISINRANGE(index, 1, LOADOUT_MAX_SLOTS))
return FALSE
var/name = params["name"] || (input(usr, "Choose a name for Slot [index]", "Slot Rename", "Slot [index]") as text|null)
if(isnull(name))
return TRUE
var/list/loadout = pref.get_character_data(CHARACTER_DATA_LOADOUT)
loadout = loadout.Copy()
var/slot_index = index
var/list/slot = loadout["[slot_index]"] || list()
slot = slot?.Copy()
loadout["[slot_index]"] = slot
if(name)
slot?[LOADOUT_SLOTDATA_NAME] = name
else
slot?.Remove(LOADOUT_SLOTDATA_NAME)
pref.set_character_data(CHARACTER_DATA_LOADOUT, loadout)
push_loadout_data()
return TRUE
/datum/category_item/player_setup_item/loadout/gear/proc/current_loadout_slot(datum/preferences/prefs)
var/list/all_slots = prefs.get_character_data(CHARACTER_DATA_LOADOUT)
var/index_slot = prefs.get_character_data(CHARACTER_DATA_LOADOUT_SLOT)
return all_slots["[index_slot]"] || (all_slots["[index_slot]"] = list())
/datum/category_item/player_setup_item/loadout/gear/proc/push_loadout_data()
push_ui_data(
data = list(
"gearData" = tgui_loadout_data(),
)
)
/datum/category_item/player_setup_item/loadout/gear/proc/push_character_name()
push_ui_data(
data = list(
"characterName" = pref.real_name,
)
)
/datum/category_item/player_setup_item/loadout/gear/load_character(savefile/S)
// LITERALLY JUST A SHIM TO TRIGGER UI UPDATES.
addtimer(CALLBACK(src, PROC_REF(push_loadout_data)), 0)
addtimer(CALLBACK(src, PROC_REF(push_character_name)), 0)
// These checks should all be on /datum/loadout_entry
// However, for performance reasons, we're going to avoid doing that
// Because this is way faster than
// manually calling procs and fetching species name every time.
/datum/category_item/player_setup_item/loadout/gear/proc/check_loadout_entry(datum/preferences/prefs, datum/loadout_entry/entry)
if(entry.legacy_species_lock && (entry.legacy_species_lock != prefs.real_species_name()))
return FALSE
if(entry.ckeywhitelist && (prefs.client_ckey != entry.ckeywhitelist))
return FALSE
return TRUE
/datum/category_item/player_setup_item/loadout/gear/proc/valid_loadout_entries(datum/preferences/prefs)
RETURN_TYPE(/list)
. = list()
var/datum/species/real_species = prefs.real_species_datum()
var/real_species_name = real_species.name
for(var/entry_name in gear_datums)
var/datum/loadout_entry/entry = gear_datums[entry_name]
if(entry.legacy_species_lock && (entry.legacy_species_lock != real_species_name))
continue
if(entry.ckeywhitelist && !(prefs.client_ckey in entry.ckeywhitelist))
continue
. += entry
/**
* generate list of gear entry datums associated to data
*
* @params
* * flags - PREF_COPY_TO flags
* * role - the role being used for equip
*/
/datum/preferences/proc/generate_loadout_entry_list(flags, datum/prototype/role/role, override_slot = null)
RETURN_TYPE(/list)
. = list()
var/list/loadout_slots = get_character_data(CHARACTER_DATA_LOADOUT)
var/loadout_slot = isnull(override_slot) ? get_character_data(CHARACTER_DATA_LOADOUT_SLOT) : override_slot
var/list/loadout_data = loadout_slots?["[loadout_slot]"]?[LOADOUT_SLOTDATA_ENTRIES]
if(isnull(loadout_data))
return
var/max_cost = max_loadout_cost()
var/used_cost = 0
var/real_species_name = real_species_name()
for(var/id in loadout_data)
var/datum/loadout_entry/entry = gear_datums[id]
if(!(flags & PREF_COPY_TO_LOADOUT_IGNORE_ROLE))
if(length(entry.allowed_roles))
if(!istype(role, /datum/prototype/role/job))
continue
var/datum/prototype/role/job/J = role
if(!(J.title in entry.allowed_roles))
continue
if(!(flags & PREF_COPY_TO_LOADOUT_IGNORE_WHITELIST))
if(length(entry.ckeywhitelist) && !(client_ckey in entry.ckeywhitelist))
continue
if(!(flags & PREF_COPY_TO_LOADOUT_IGNORE_CHECKS))
if(entry.legacy_species_lock && (entry.legacy_species_lock != real_species_name))
continue
if(max_cost < (used_cost + entry.cost))
continue
used_cost += entry.cost
.[entry] = loadout_data[id]
/proc/max_loadout_cost()
. = LOADOUT_MAX_COST
for(var/name in SSevents.holidays)
var/datum/holiday/H = SSevents.holidays[name]
if(H.loadout_spam)
return LOADOUT_MAX_COST_HOLIDAY_SPAM