pokerogue/src/utils/data.ts
Sirz Benjie c8a66b2e59
[Bug] Prevent an empty starterpreferences object from being saved (#6410)
* Prevent an empty starterpreferences object from being saved

* Fix ssui nullish coalescing

* Update src/utils/data.ts

Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>

---------

Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>
2025-08-25 23:48:55 -07:00

105 lines
3.7 KiB
TypeScript

import { loggedInUser } from "#app/account";
import { saveKey } from "#app/constants";
import type { StarterAttributes } from "#system/game-data";
import { AES, enc } from "crypto-js";
/**
* Perform a deep copy of an object.
* @param values - The object to be deep copied.
* @returns A new object that is a deep copy of the input.
*/
export function deepCopy(values: object): object {
// Convert the object to a JSON string and parse it back to an object to perform a deep copy
return JSON.parse(JSON.stringify(values));
}
/**
* Deeply merge two JSON objects' common properties together.
* This copies all values from `source` that match properties inside `dest`,
* checking recursively for non-null nested objects.
* If a property in `source` does not exist in `dest` or its `typeof` evaluates differently, it is skipped.
* If it is a non-array object, its properties are recursed into and checked in turn.
* All other values are copied verbatim.
* @param dest - The object to merge values into
* @param source - The object to source merged values from
* @remarks Do not use for regular objects; this is specifically made for JSON copying.
*/
export function deepMergeSpriteData(dest: object, source: object) {
for (const key of Object.keys(source)) {
if (
!(key in dest) ||
typeof source[key] !== typeof dest[key] ||
Array.isArray(source[key]) !== Array.isArray(dest[key])
) {
continue;
}
// Pure objects get recursed into; everything else gets overwritten
if (typeof source[key] !== "object" || source[key] === null || Array.isArray(source[key])) {
dest[key] = source[key];
} else {
deepMergeSpriteData(dest[key], source[key]);
}
}
}
export function encrypt(data: string, bypassLogin: boolean): string {
if (bypassLogin) {
return btoa(encodeURIComponent(data));
}
return AES.encrypt(data, saveKey).toString();
}
export function decrypt(data: string, bypassLogin: boolean): string {
if (bypassLogin) {
return decodeURIComponent(atob(data));
}
return AES.decrypt(data, saveKey).toString(enc.Utf8);
}
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
// if they ever add private static variables, move this into StarterPrefs
const StarterPrefers_DEFAULT: string = "{}";
let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT;
export interface StarterPreferences {
[key: number]: StarterAttributes | undefined;
}
// called on starter selection show once
export function loadStarterPreferences(): StarterPreferences {
return JSON.parse(
(StarterPrefers_private_latest =
localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT),
);
}
/**
* Check if an object has no properties of its own (its shape is `{}`)
* @param obj - Object to check
* @returns - Whether the object is bare
*/
export function isBareObject(obj: object): boolean {
for (const _ in obj) {
return false;
}
return true;
}
export function saveStarterPreferences(prefs: StarterPreferences): void {
// Fastest way to check if an object has any properties (does no allocation)
if (isBareObject(prefs)) {
console.warn("Refusing to save empty starter preferences");
return;
}
// no reason to store `{}` (for starters not customized)
const pStr: string = JSON.stringify(prefs, (_, value) => (isBareObject(value) ? undefined : value));
if (pStr !== StarterPrefers_private_latest) {
// something changed, store the update
localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr);
// update the latest prefs
StarterPrefers_private_latest = pStr;
}
}