Add the ability to duplicate characters (#4743)

## About The Pull Request

Lets you copy characters in the character customizer. This'll be useful
if you ever want to create variants of your character or Need to move
characters to different slots.

This needs large scale testing, I couldn't break anything on local but
given this involves tinkering with saves better safe than sorry.

## Why It's Good For The Game

I saw a complaint on discord about having to delete characters to
reorganize the list, this resolves that.
This also lets you create a "base" character to use as a template or to
generate variants of an existing character without making them from
scratch or editing them.
## Proof Of Testing
<details>
<summary>Screenshots/Videos</summary>
Soon....
</details>

## Changelog
🆑
qol: You can now copy your characters into other slots.
/🆑

---------

Co-authored-by: xPokee <catmc8565@gmail.com>
This commit is contained in:
SpaceCatSS13
2025-12-03 18:25:44 +00:00
committed by GitHub
parent 64ad881b1e
commit 5d8afbabb3
3 changed files with 53 additions and 4 deletions

View File

@@ -250,6 +250,21 @@ GLOBAL_LIST_EMPTY(preferences_datums)
if ("remove_current_slot")
remove_current_slot()
return TRUE
if ("duplicate_current_slot") //BUBBER ADDITION START - Character duplication
save_character()
if(sanitize_languages())
save_character()
var/list/character_list = create_character_profiles()
var/list/slot_choices = list()
for(var/i = 1, i <= character_list.len, i++)
slot_choices += "Slot [i]: [character_list[i]]"
var/target_slot = tgui_input_list(ui.user, "Pick a slot to copy to.", "Duplicate Character", slot_choices, null)
if(!isnull(target_slot))
duplicate_current_slot(slot_choices.Find(target_slot))
tainted_character_profiles = TRUE
else
tgui_alert(ui.user, "Cancelled Duplication", "Duplicate Character")
return TRUE //BUBBER ADDITION END - Character duplication
if ("rotate")
/* SKYRAT EDIT - Bi-directional prefs menu rotation - ORIGINAL:
character_preview_view.dir = turn(character_preview_view.dir, -90)

View File

@@ -389,14 +389,20 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
return needs_update != -3 // BUBBER EDIT
/datum/preferences/proc/save_character(update) // Skyrat edit - Choose when to update (This is stupid)
/datum/preferences/proc/save_character(update, override_slot) // Skyrat edit - Choose when to update (This is stupid) //Bubber Edit - duplication support
SHOULD_NOT_SLEEP(TRUE)
if(!path)
return FALSE
var/tree_key = "character[default_slot]"
if(!(tree_key in savefile.get_entry()))
savefile.set_entry(tree_key, list())
var/save_data = savefile.get_entry(tree_key)
var/save_data //BUBBER EDIT START - Original var/save_data = savefile.get_entry(tree_key)
if(!isnull(override_slot))
var/override_tree_key = "character[override_slot]"
savefile.set_entry(override_tree_key, list())
save_data = savefile.get_entry(override_tree_key)
else
save_data = savefile.get_entry(tree_key) //BUBBER EDIT END
for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
if (preference.savefile_identifier != PREFERENCE_CHARACTER)
@@ -408,7 +414,10 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
recently_updated_keys -= preference.type
if (preference.type in value_cache)
write_preference(preference, preference.serialize(value_cache[preference.type]))
if(!isnull(override_slot)) //BUBBER EDIT ADDITION START - Original: write_preference(preference, preference.serialize(value_cache[preference.type]))
write_preference_special(preference, preference.serialize(value_cache[preference.type]), override_slot)
else
write_preference(preference, preference.serialize(value_cache[preference.type])) //BUBBER EDIT ADDITION END
save_data["version"] = SAVEFILE_VERSION_MAX //load_character will sanitize any bad data, so assume up-to-date.
@@ -467,6 +476,20 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
tainted_character_profiles = TRUE
switch_to_slot(closest_slot)
/datum/preferences/proc/duplicate_current_slot(target_slot)
PRIVATE_PROC(TRUE)
if(isnull(target_slot))
return
save_character(TRUE, target_slot)
/datum/preferences/proc/write_preference_special(datum/preference/preference, preference_value, override_slot)
var/save_data = savefile.get_entry("character[override_slot]")
var/new_value = preference.deserialize(preference_value, src)
var/success = preference.write(save_data, new_value)
if (success)
value_cache[preference.type] = new_value
return success
/datum/preferences/proc/sanitize_be_special(list/input_be_special)
var/list/output = list()

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useBackend } from 'tgui/backend';
import { NoticeBox, Stack } from 'tgui-core/components';
import { NoticeBox, Stack, Button } from 'tgui-core/components'; // BUBBER EDIT CHANGE - ORIGINAL : import { NoticeBox, Stack } from 'tgui-core/components';
import { exhaustiveCheck } from 'tgui-core/exhaustive';
import { SideDropdown } from '../../../bubber_components/SideDropdown'; // BUBBER EDIT ADDITION
@@ -138,6 +138,17 @@ export function CharacterPreferenceWindow(props) {
profiles={data.character_profiles}
/>
</Stack.Item>
{/* BUBBER EDIT ADDITION BEGIN */}
<Stack.Item>
<Button
onClick={() => {act('duplicate_current_slot');}}
fontSize="13px"
icon="copy"
tooltip="Duplicate Current Character (Experimental)" //Delete this comment about being experimental before merge
tooltipPosition="top"
/>
{/* BUBBER EDIT ADDITION END */}
</Stack.Item>
{!data.content_unlocked && (
<Stack.Item grow align="center" mb={-1}>
<NoticeBox color="grey">