[MIRROR] Fixes savefile corruption bug and allows character swapping (#11406)

Co-authored-by: Cameron Lennox <killer65311@gmail.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-08-13 11:19:03 -07:00
committed by GitHub
parent c58ead6223
commit 4d3de029e3
22 changed files with 104 additions and 53 deletions

View File

@@ -192,6 +192,12 @@
stun_time -= min(flicker_break_chance / 5, 1)
return stun_time
///Sees if the savefile we have selected in CHARACTER SETUP is the same as our ACTIVE CHARACTER savefile.
/datum/component/shadekin/proc/correct_savefile_selected()
if(owner.client.prefs.default_slot == owner.mind.loaded_from_slot)
return TRUE
return FALSE
/datum/component/shadekin/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
@@ -208,6 +214,7 @@
"no_retreat" = no_retreat,
"nutrition_energy_conversion" = nutrition_energy_conversion,
"extended_kin" = extended_kin,
"savefile_selected" = correct_savefile_selected()
)
return data
@@ -227,14 +234,14 @@
if(!isnum(new_time))
return FALSE
flicker_time = new_time
ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_time, new_time, WRITE_PREF_MANUAL)
ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_time, new_time, WRITE_PREF_MANUAL, save_to_played_slot = TRUE)
return TRUE
if("adjust_color")
var/set_new_color = tgui_color_picker(ui.user, "Select a color you wish the lights to flicker as (Default is #E0EFF0)", "Color Selector", flicker_color)
if(!set_new_color)
return FALSE
flicker_color = set_new_color
ui.user.write_preference_directly(/datum/preference/color/living/flicker_color, set_new_color, WRITE_PREF_MANUAL)
ui.user.write_preference_directly(/datum/preference/color/living/flicker_color, set_new_color, WRITE_PREF_MANUAL, save_to_played_slot = TRUE)
return TRUE
if("adjust_break")
var/new_break_chance = text2num(params["val"])
@@ -242,7 +249,7 @@
if(!isnum(new_break_chance))
return FALSE
flicker_break_chance = new_break_chance
ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_break_chance, new_break_chance, WRITE_PREF_MANUAL)
ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_break_chance, new_break_chance, WRITE_PREF_MANUAL, save_to_played_slot = TRUE)
return TRUE
if("adjust_distance")
var/new_distance = text2num(params["val"])
@@ -250,16 +257,16 @@
if(!isnum(new_distance))
return FALSE
flicker_distance = new_distance
ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_distance, new_distance, WRITE_PREF_MANUAL)
ui.user.write_preference_directly(/datum/preference/numeric/living/flicker_distance, new_distance, WRITE_PREF_MANUAL, save_to_played_slot = TRUE)
return TRUE
if("toggle_retreat")
var/new_retreat = !no_retreat
no_retreat = !no_retreat
ui.user.write_preference_directly(/datum/preference/toggle/living/dark_retreat_toggle, new_retreat, WRITE_PREF_MANUAL)
ui.user.write_preference_directly(/datum/preference/toggle/living/dark_retreat_toggle, new_retreat, WRITE_PREF_MANUAL, save_to_played_slot = TRUE)
if("toggle_nutrition")
var/new_retreat = !nutrition_energy_conversion
nutrition_energy_conversion = !nutrition_energy_conversion
ui.user.write_preference_directly(/datum/preference/toggle/living/shadekin_nutrition_conversion, new_retreat, WRITE_PREF_MANUAL)
ui.user.write_preference_directly(/datum/preference/toggle/living/shadekin_nutrition_conversion, new_retreat, WRITE_PREF_MANUAL, save_to_played_slot = TRUE)
/mob/living/proc/shadekin_control_panel()
set name = "Shadekin Control Panel"

View File

@@ -2,7 +2,7 @@
The way datum/mind stuff works has been changed a lot.
Minds now represent IC characters rather than following a client around constantly.
Guidelines for using minds properly:
- Never mind.transfer_to(ghost). The var/current and var/original of a mind must always be of type mob/living!
- Never mind.transfer_to(ghost). The var/current and var/original_character of a mind must always be of type mob/living!
ghost.mind is however used as a reference to the ghost's corpse
- When creating a new mob for an existing IC character (e.g. cloning a dead guy or borging a brain of a human)
the existing mind of the old mob should be transfered to the new mob like so:
@@ -23,7 +23,7 @@
var/key
var/name //replaces mob/var/original_name
var/mob/living/current
var/mob/living/original //TODO: remove.not used in any meaningful way ~Carn. First I'll need to tweak the way silicon-mobs handle minds.
var/datum/weakref/original_character //replaces /mob/living/original
var/active = 0
var/memory
@@ -508,7 +508,7 @@
mind.key = key
else
mind = new /datum/mind(key)
mind.original = src
mind.original_character = WEAKREF(src)
if(SSticker)
SSticker.minds += mind
else

View File

@@ -10,7 +10,7 @@
player.current = new mob_path(get_turf(player.current))
player.transfer_to(player.current)
if(holder) qdel(holder)
player.original = player.current
player.original_character = WEAKREF(player.current)
if(!preserve_appearance && (flags & ANTAG_SET_APPEARANCE))
spawn(3)
var/mob/living/carbon/human/H = player.current

View File

@@ -93,8 +93,9 @@ var/datum/antagonist/technomancer/technomancers
var/text = print_player_lite(player)
var/obj/item/technomancer_core/core
if(player.original)
core = locate() in player.original
var/mob/living/original = player.original_character?.resolve()
if(original)
core = locate() in original
if(core)
text += "<br>Bought [english_list(core.spells)], and used \a [core]."
else

View File

@@ -89,7 +89,7 @@ var/datum/antagonist/rogue_ai/malf
player.current = new mob_path(get_turf(player.current), null, null, 1)
player.transfer_to(player.current)
if(holder) qdel(holder)
player.original = player.current
player.original_character = WEAKREF(player.current)
return player.current
/datum/antagonist/rogue_ai/set_antag_name(var/mob/living/silicon/player)

View File

@@ -530,7 +530,9 @@ GLOBAL_LIST_EMPTY(additional_antag_types)
continue //Happy connected client
for(var/mob/observer/dead/D in GLOB.dead_mob_list)
if(D.mind && (D.mind.original == L || D.mind.current == L))
if(D.mind)
var/mob/living/original = D.mind.original_character?.resolve()
if((original && original == L) || D.mind.current == L)
if(L.stat == DEAD)
if(L.suiciding) //Suicider
msg += "[span_bold(L.name)] ([ckey(D.mind.key)]), the [L.job] ([span_red(span_bold("Suicide"))])<br>"

View File

@@ -325,7 +325,8 @@ GLOBAL_LIST_EMPTY(all_objectives)
/datum/objective/survive/check_completion()
if(!owner.current || owner.current.stat == DEAD || isbrain(owner.current))
return 0 //Brains no longer win survive objectives. --NEO
if(issilicon(owner.current) && owner.current != owner.original)
var/mob/living/original = owner.original_character?.resolve()
if(issilicon(owner.current) && (original && (owner.current != original)))
return 0
return 1

View File

@@ -144,8 +144,8 @@
continue
sendPDAs["[P.name]"] = "\ref[P]"
data["possibleRecipients"] = sendPDAs
data["isMalfAI"] = ((isAI(user) || isrobot(user)) && (user.mind.special_role && user.mind.original == user))
var/mob/living/original = user.mind.original_character?.resolve()
data["isMalfAI"] = ((isAI(user) || isrobot(user)) && (user.mind.special_role && (original && original == user)))
return data
@@ -210,7 +210,8 @@
temp = noserver
//Hack the Console to get the password
if("hack")
if((isAI(ui.user) || isrobot(ui.user)) && (ui.user.mind.special_role && ui.user.mind.original == ui.user))
var/mob/living/original = ui.user.mind.original_character?.resolve()
if((isAI(ui.user) || isrobot(ui.user)) && (ui.user.mind.special_role && (original && original == ui.user)))
hacking = 1
update_icon()
//Time it takes to bruteforce is dependant on the password length.

View File

@@ -92,7 +92,8 @@
return TRUE
if(!isAI(user))
return FALSE
return (user.mind.special_role && user.mind.original == user)
var/mob/living/original = user.mind.original_character?.resolve()
return (user.mind.special_role && (original && original == user))
/**
* Check if the user is allowed to hack a specific borg

View File

@@ -319,6 +319,7 @@ var/const/preview_icons = 'icons/mob/human_races/preview.dmi'
data["b_type"] = pref.b_type
data["digitigrade"] = pref.digitigrade
data["tail_layering"] = pref.read_preference(/datum/preference/choiced/human/tail_layering)
data["synth_color_toggle"] = pref.synth_color
data["synth_color"] = pref.read_preference(/datum/preference/color/human/synth_color)
@@ -996,6 +997,13 @@ var/const/preview_icons = 'icons/mob/human_races/preview.dmi'
pref.digitigrade = !pref.digitigrade
return TOPIC_REFRESH_UPDATE_PREVIEW
if("set_tail_layering")
var/new_tail_layering = tgui_input_list(user, "Select a tail layer.", "Set Tail Layer", GLOB.tail_layer_options,
pref.read_preference(/datum/preference/choiced/human/tail_layering))
if(new_tail_layering)
pref.update_preference_by_type(/datum/preference/choiced/human/tail_layering, new_tail_layering)
return TOPIC_REFRESH_UPDATE_PREVIEW
if("synth_color_toggle")
pref.synth_color = !pref.synth_color
return TOPIC_REFRESH_UPDATE_PREVIEW

View File

@@ -318,10 +318,17 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
/// Write a /datum/preference type and return its value directly to the json.
/// Please use SScharacter_setup.queue_preferences_save(prefs) when you edit multiple at once and set direct_write to WRITE_PREF_MANUAL
/mob/proc/write_preference_directly(preference_type, preference_value, write_mode = WRITE_PREF_INSTANT)
/// Additionally, if you want something to be changed IN ROUND and change a pref for THAT CHARACTER'S SAVESLOT, ensure save_to_played_slot = TRUE!
/mob/proc/write_preference_directly(preference_type, preference_value, write_mode = WRITE_PREF_INSTANT, save_to_played_slot)
var/remembered_default
if(save_to_played_slot && (mind.loaded_from_slot != client?.prefs?.default_slot))
remembered_default = client?.prefs?.default_slot
client?.prefs?.load_character(mind.loaded_from_slot)
var/success = client?.prefs?.write_preference_by_type(preference_type, preference_value, write_mode)
if(success)
client?.prefs?.value_cache[preference_type] = preference_value
if(remembered_default)
client?.prefs?.return_to_character_slot(src, remembered_default)
return success
/// Set a /datum/preference entry.

View File

@@ -136,9 +136,6 @@
switch(action)
// Basic actions
if("load")
if(!isnewplayer(ui.user))
to_chat(ui.user, span_userdanger("You can't change your character slot while being in round."))
return FALSE
if(!IsGuestKey(ui.user.key))
open_load_dialog(ui.user)
. = TRUE

View File

@@ -76,6 +76,14 @@ var/global/client_record_update_lock = FALSE
playsound(COM, 'sound/machines/deniedbeep.ogg', 50, 0)
return "Update syncronization failed (OOC: Record's owner is offline)"
var/datum/preferences/P = C.prefs
if(P.default_slot != M.mind.loaded_from_slot)
if(COM && !QDELETED(COM))
COM.visible_message(span_notice("\The [COM] buzzes!"))
playsound(COM, 'sound/machines/deniedbeep.ogg', 50, 0)
to_chat(M, span_warning("[user] attempted to update your [record_string] record, but your current character slot does not match your played slot. Please ensure your currently played character is selected in your Character Setup."))
return "Update syncronization failed (OOC: Player's current character slot does not match their played slot. They have been informed.)"
var/choice = tgui_alert(M, "Your [record_string] record has been updated from the a records console by [user]. Please review the changes made to your [record_string] record. Accepting these changes will SAVE your CURRENT character slot! If your new [record_string] record has errors, it is recomended to have it corrected IC instead of editing it yourself.", "Record Updated", list("Review Changes","DENY"))
if(!choice || choice == "DENY")
message_admins("[active.fields["name"]] refused [record_string] record update from [user] without review.")
@@ -84,7 +92,6 @@ var/global/client_record_update_lock = FALSE
playsound(COM, 'sound/machines/deniedbeep.ogg', 50, 0)
return "Update syncronization failed (OOC: Player refused without review)"
var/datum/preferences/P = C.prefs
var/new_data = strip_html_simple(tgui_input_text(M,"Please review [user]'s changes to your [record_string] record before confirming. Confirming will SAVE your CURRENT character slot! If your new [record_string] record major errors, it is recomended to have it corrected IC instead of editing it yourself.","Character Preference", html_decode(active.fields["notes"]), MAX_RECORD_LENGTH, TRUE, prevent_enter = TRUE), MAX_RECORD_LENGTH)
if(!new_data)
message_admins("[active.fields["name"]] refused [record_string] record update from [user] with review.")

View File

@@ -76,8 +76,6 @@
if(!tail_option)
return
tail_layering = tail_option
write_preference_directly(/datum/preference/choiced/human/tail_layering, input)
update_tail_showing()
/mob/living/carbon/human/verb/hide_wings_vr()

View File

@@ -6,6 +6,9 @@
/mob/living/silicon/robot/show_laws(var/everyone = 0)
laws_sanity_check()
var/who
var/mob/living/original
if(mind)
original = mind.original_character?.resolve()
if (everyone)
who = world
@@ -21,7 +24,7 @@
photosync()
to_chat(src, span_infoplain(span_bold("Laws synced with AI, be sure to note any changes.")))
// TODO: Update to new antagonist system.
if(mind && mind.special_role == "traitor" && mind.original == src)
if(mind && mind.special_role == "traitor" && (original && original == src))
to_chat(src, span_infoplain(span_bold("Remember, your AI does NOT share or know about your law 0.")))
else
to_chat(src, span_infoplain(span_bold("No AI selected to sync laws with, disabling lawsync protocol.")))
@@ -32,7 +35,7 @@
if(shell) //AI shell
to_chat(who, span_infoplain(span_bold("Remember, you are an AI remotely controlling your shell, other AIs can be ignored.")))
// TODO: Update to new antagonist system.
else if(mind && (mind.special_role == "traitor" && mind.original == src) && connected_ai)
else if(mind && (mind.special_role == "traitor" && (original && original == src)) && connected_ai)
to_chat(who, span_infoplain(span_bold("Remember, [connected_ai.name] is technically your master, but your objective comes first.")))
else if(connected_ai)
to_chat(who, span_infoplain(span_bold("Remember, [connected_ai.name] is your master, other AIs can be ignored.")))

View File

@@ -44,8 +44,9 @@
if(mind)
if(mind.current == src)
mind.current = null
if(mind.original == src)
mind.original = null
var/mob/living/original = mind.original_character?.resolve()
if(original && original == src)
mind.original_character = null
. = ..()
update_client_z(null)

View File

@@ -464,8 +464,9 @@ var/list/intents = list(I_HELP,I_DISARM,I_GRAB,I_HURT)
var/realname = C.mob.real_name
if(C.mob.mind)
mindname = C.mob.mind.name
if(C.mob.mind.original && C.mob.mind.original.real_name)
realname = C.mob.mind.original.real_name
var/mob/living/original = C.mob.mind.original_character?.resolve()
if(original && original.real_name)
realname = original.real_name
if(mindname && mindname != realname)
name = "[realname] died as [mindname]"
else
@@ -529,10 +530,11 @@ var/list/intents = list(I_HELP,I_DISARM,I_GRAB,I_HURT)
C = O
else if(istype(O, /datum/mind))
var/datum/mind/M = O
var/mob/living/original = M.original_character?.resolve()
if(M.current && M.current.client)
C = M.current.client
else if(M.original && M.original.client)
C = M.original.client
else if(original && original.client)
C = original.client
if(C)
var/name

View File

@@ -423,7 +423,7 @@
if(mind)
mind.active = 0 //we wish to transfer the key manually
mind.original = new_character
mind.original_character = WEAKREF(new_character)
mind.loaded_from_ckey = client.ckey
mind.loaded_from_slot = client.prefs.default_slot
mind.transfer_to(new_character) //won't transfer key since the mind is not active

View File

@@ -100,7 +100,7 @@
if(mind)
mind.transfer_to(O)
O.mind.original = O
O.mind.original_character = WEAKREF(O)
else
O.key = key
@@ -163,7 +163,7 @@
if(mind) //TODO
mind.transfer_to(O)
if(O.mind.assigned_role == JOB_CYBORG)
O.mind.original = O
O.mind.original_character = WEAKREF(O)
else if(mind && mind.special_role)
O.mind.store_memory("In case you look at this after being borged, the objectives are only here until I find a way to make them not show up for you, as I can't simply delete them without screwing up round-end reporting. --NeoFite")
else

View File

@@ -43,6 +43,7 @@ export const SubtabBody = (props: {
s_tone,
b_type,
digitigrade,
tail_layering,
synth_color,
synth_color_toggle,
synth_markings,
@@ -130,6 +131,11 @@ export const SubtabBody = (props: {
{digitigrade ? 'Yes' : 'No'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Tail Layering">
<Button inline onClick={() => act('set_tail_layering')}>
{tail_layering}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Blood Type">
<Button inline onClick={() => act('blood_type')}>
{b_type}

View File

@@ -181,6 +181,7 @@ export type BodyData = {
body_markings: Record<string, BodyMarking>;
tail_style: string;
tail_layering: string;
tail_color1: string;
tail_color2: string;
tail_color3: string;

View File

@@ -11,6 +11,7 @@ import {
Stack,
Tooltip,
} from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react';
type Data = {
stun_time: number;
@@ -18,8 +19,9 @@ type Data = {
flicker_color: string | null;
flicker_break_chance: number;
flicker_distance: number;
no_retreat: number;
extended_kin: number;
no_retreat: BooleanLike;
extended_kin: BooleanLike;
savefile_selected: BooleanLike;
nutrition_energy_conversion: number;
};
@@ -33,6 +35,7 @@ export const ShadekinConfig = (props) => {
flicker_break_chance,
flicker_distance,
no_retreat,
savefile_selected,
extended_kin,
nutrition_energy_conversion,
} = data;
@@ -40,7 +43,7 @@ export const ShadekinConfig = (props) => {
const isSubtle =
flicker_time < 5 || flicker_break_chance < 5 || flicker_distance < 5;
const windowHeight = (isSubtle ? 220 : 190) + (extended_kin ? 95 : 0);
const windowHeight = (isSubtle ? 220 : 190) + (extended_kin ? 95 : 0) + (savefile_selected ? 0 : 90);
return (
<Window width={300} height={windowHeight} theme="abductor">
@@ -51,6 +54,11 @@ export const ShadekinConfig = (props) => {
<NoticeBox>Subtle Phasing, causes {stun_time} s stun.</NoticeBox>
</Stack.Item>
)}
{!savefile_selected && (
<Stack.Item>
<NoticeBox>WARNING: Your current selected savefile (in Character Setup) is not the same as your currently loaded savefile. Please select it to prevent savefile corruption.</NoticeBox>
</Stack.Item>
)}
<Stack.Item>
<Section fill title="Light Settings">
<LabeledList>