From af8f934d1922ff1564b5fc8510def5bc545f3aba Mon Sep 17 00:00:00 2001 From: "Leo L. Schwab" Date: Fri, 16 Jan 2026 17:46:41 -0800 Subject: [PATCH 1/4] Save code redesign Some brief experiments with CyberChef showed that changing from LZString to gzip CompressionStreams would not only be faster, but also result in smaller save codes. New save codes are `data:` URLs, base64-encoded. The MIME types are: - `application/vnd.straightlaced.kinkydungeon.save.game+gzip;version=2` - `application/vnd.straightlaced.kinkydungeon.save.outfit+gzip;version=2` - `application/vnd.straightlaced.kinkydungeon.save.wardrobe+gzip;version=2` (The 'wardrobe` variant is not used yet.) Save codes without a `data:` leader are assumed to be legacy LZString codes, and may still be loaded from browser storage or pasted in to the load dialog. The intention, however, is for all new saves to be in the new format. This initial checkin gets the core code in place, but forces saves in legacy format. This will be removed when the new codes are proved reliable, and I'm convinced we're not accidentally destroying data. (async/Promises are a real pain...) --- Game/src/base/KinkyDungeon.ts | 446 +++++++++++++++++++++++----------- out/saveworker.js | 72 +++++- 2 files changed, 369 insertions(+), 149 deletions(-) diff --git a/Game/src/base/KinkyDungeon.ts b/Game/src/base/KinkyDungeon.ts index ba787a20c..753c472d1 100644 --- a/Game/src/base/KinkyDungeon.ts +++ b/Game/src/base/KinkyDungeon.ts @@ -966,7 +966,7 @@ function KDLoadToggles() { } KDConsentArray = (localStorage.getItem("KDConsentArray") ? JSON.parse(localStorage.getItem("KDConsentArray")) : {}) || {}; KDSeenConsents = (localStorage.getItem("KDSeenConsents") ? JSON.parse(localStorage.getItem("KDSeenConsents")) : []) || []; - + KDDefaultPalette = localStorage.getItem("KDDefaultPalette") || ""; let loaded = localStorage.getItem("KDToggles") ? JSON.parse(localStorage.getItem("KDToggles")) : {}; @@ -1584,9 +1584,9 @@ function KinkyDungeonRun() { if (StandalonePatched && KDCurrentModels) { let refresh = false; if (CommonTime() > lastGlobalRefresh + GlobalRefreshInterval) { - + lastGlobalRefresh = CommonTime(); - + if (KinkyDungeonDrawState != "Game" && KinkyDungeonState != "Game") { refresh = true; KDGlobalFilterCacheRefresh = true; @@ -1859,28 +1859,35 @@ function KinkyDungeonRun() { KinkyDungeonState = "Name"; KDSaveSlot = (localStorage.getItem('KDLastSaveSlot') !== null) ? parseInt(localStorage.getItem('KDLastSaveSlot')) : 4; let emptySlot = undefined; + let code_z: string = null; for (var i = 1; i <= (saveSlotsPerPage*maxSaveSlotPages); i++) { let num = (i); - KinkyDungeonDBLoad(num).then((code) => { + KinkyDungeonDBLoad(num) + .then((code) => { loadedsaveslots[num - 1] = code; - let decoded = LZString.decompressFromBase64(code); - if (!decoded) return; - let parse = JSON.parse(decoded); - if (decoded && parse?.KDGameData?.PlayerName) - loadedsaveNames[num - 1] = - JSON.parse(decoded)?.KDGameData?.PlayerName; - - if (decoded && parse?.KDGameData?.HighestLevelCurrent) + code_z = code; + //let decoded = LZString.decompressFromBase64(code); + return KinkyDungeonDecompressSave (code); + }).then ((decoded) => { + if (!decoded) { + return; + } + const parse = JSON.parse(decoded); + + if (parse?.KDGameData?.PlayerName) + loadedsaveNames[num - 1] = parse?.KDGameData?.PlayerName; + + if (parse?.KDGameData?.HighestLevelCurrent) loadedsaveFloors[num - 1] = parse.KDGameData.HighestLevelCurrent; - - if (decoded && parse?.KDGameData?.Class) + + if (parse?.KDGameData?.Class) loadedsaveClasses[num - 1] = parse.KDGameData.Class; - if (decoded && (parse?.npp || parse?.stats?.npp)) + if (parse?.npp || parse?.stats?.npp) loadedsaveNG[num - 1] = (parse?.npp || parse?.stats?.npp); - if (!emptySlot && !code) { + if (!emptySlot && !code_z) { emptySlot = num; KDSaveSlot = emptySlot; } @@ -1900,13 +1907,19 @@ function KinkyDungeonRun() { }); for (var i = 1; i <= (saveSlotsPerPage*maxSaveSlotPages); i++) { let num = (i); - KinkyDungeonDBLoad(num).then((code) => { + KinkyDungeonDBLoad(num) + .then((code) => { loadedsaveslots[num - 1] = code; - let decoded = LZString.decompressFromBase64(code); - if (decoded && JSON.parse(decoded)?.KDGameData?.PlayerName) - loadedsaveNames[num - 1] = - JSON.parse(decoded)?.KDGameData?.PlayerName; + //let decoded = LZString.decompressFromBase64(code); + return KinkyDungeonDecompressSave (code); + }).then ((decoded) => { + if (decoded) { + const playerName = JSON.parse(decoded)?.KDGameData?.PlayerName; + if (playerName) { + loadedsaveNames[num - 1] = playerName; + } + } }); } @@ -1937,7 +1950,7 @@ function KinkyDungeonRun() { KDOptionFilter = ""; return true; }, true, 1000-350/2, 600, 350, 64, TextGet("GameToggles"), KDBaseWhite, ""); - + let ii = 680; if (KDExitButton) { @@ -2109,35 +2122,37 @@ function KinkyDungeonRun() { JSON.parse(LZString.decompressFromBase64(itt)).appearance || itt : ""; if (orig != ElementValue("saveInputField")) KDOriginalValue = orig; - let decompressed = DecompressB64(ElementValue("saveInputField")); - if (decompressed) { - let origAppearance = KinkyDungeonPlayer.Appearance; - try { - let decodeSave = JSON.parse(decompressed); - if (decodeSave?.saveStat?.appearance) { - if (decodeSave.saveStat.poses) { - KDCurrentModels.get(KinkyDungeonPlayer).Poses = decodeSave.saveStat.poses; + //let decompressed = DecompressB64(ElementValue("saveInputField")); + KinkyDungeonDecompressSave (ElementValue("saveInputField"), SaveType.Game) + .then ((decompressed) => { + if (decompressed) { + let origAppearance = KinkyDungeonPlayer.Appearance; + try { + let decodeSave = JSON.parse(decompressed); + if (decodeSave?.saveStat?.appearance) { + if (decodeSave.saveStat.poses) { + KDCurrentModels.get(KinkyDungeonPlayer).Poses = decodeSave.saveStat.poses; + } + let appearanceFromSave = JSON.stringify(decodeSave.saveStat.appearance); + CharacterAppearanceRestore(KinkyDungeonPlayer, appearanceFromSave, false, false); + KinkyDungeonPlayer.Palette = decodeSave.saveStat.Palette; + KinkyDungeonPlayer.metadata = decodeSave.saveStat.metadata; + CharacterRefresh(KinkyDungeonPlayer); + UpdateModels(KinkyDungeonPlayer); + //KDInitProtectedGroups(KinkyDungeonPlayer); + //KinkyDungeonDressPlayer(KinkyDungeonPlayer, false); + + if (KinkyDungeonPlayer.Appearance.length == 0) + throw new DOMException(); } - let appearanceFromSave = JSON.stringify(decodeSave.saveStat.appearance); - CharacterAppearanceRestore(KinkyDungeonPlayer, appearanceFromSave, false, false); - KinkyDungeonPlayer.Palette = decodeSave.saveStat.Palette; - KinkyDungeonPlayer.metadata = decodeSave.saveStat.metadata; - CharacterRefresh(KinkyDungeonPlayer); - UpdateModels(KinkyDungeonPlayer); - //KDInitProtectedGroups(KinkyDungeonPlayer); - //KinkyDungeonDressPlayer(KinkyDungeonPlayer, false); - - if (KinkyDungeonPlayer.Appearance.length == 0) - throw new DOMException(); + } catch (e) { + console.log("Invalid outfit loaded from save"); + KinkyDungeonPlayer.Appearance = origAppearance; + /** breaks the link */ + KDRefreshSelectedModel(KinkyDungeonPlayer); } - - } catch (e) { - console.log("Invalid outfit loaded from save"); - KinkyDungeonPlayer.Appearance = origAppearance; - /** breaks the link */ - KDRefreshSelectedModel(KinkyDungeonPlayer); } - } + }); } DrawButtonKDEx( @@ -2221,7 +2236,7 @@ function KinkyDungeonRun() { KDDrawConsent(500); } - + else if (KinkyDungeonState == "Challenge") { //DrawTextKD(TextGet("KinkyDungeonChallenge"), 1250, 80, KDBaseWhite, KDTextGray1, 48); KDDrawGameSetupTabs(); @@ -2725,9 +2740,9 @@ function KinkyDungeonRun() { + (loadedsaveNG[slot-1] ? TextGet("KDNGSaveLabel") + loadedsaveNG[slot-1] : "") ) || TextGet("KDEmpty")); let textColor = (danger && (slot == KDSaveSlot)) ? dangerColor : (slot == KDSaveSlot ? selectedColor : defaultColor); - DrawTextFitKD(slotText, - ((danger && (slot == KDSaveSlot)) ? (Math.random() > 0.5 ? -1 : 1) : 0) + xOffsets[col % 2] + 10, - ((danger && (slot == KDSaveSlot)) ? (Math.random() > 0.5 ? -1 : 1) : 0) + yOffset + 25, + DrawTextFitKD(slotText, + ((danger && (slot == KDSaveSlot)) ? (Math.random() > 0.5 ? -1 : 1) : 0) + xOffsets[col % 2] + 10, + ((danger && (slot == KDSaveSlot)) ? (Math.random() > 0.5 ? -1 : 1) : 0) + yOffset + 25, 385, textColor, undefined, undefined, "left") DrawButtonKDEx("slot_" + slot + "prev", () => { // on click change save slot KDSaveSlot = slot; @@ -2735,11 +2750,11 @@ function KinkyDungeonRun() { }, true, ((danger && (slot == KDSaveSlot)) ? (Math.random() > 0.5 ? -1 : 1) : 0) + xOffsets[col % 2], ((danger && (slot == KDSaveSlot)) ? (Math.random() > 0.5 ? -1 : 1) : 0) + yOffset, - 400, 50, "", textColor, "", + 400, 50, "", textColor, "", undefined, undefined, true, KDButtonColor, undefined, true, { centerText: false, - + }); } } @@ -3291,7 +3306,7 @@ function KinkyDungeonRun() { KDForceAllCull = false; - + KDLastActiveElement = document.activeElement; } @@ -4547,6 +4562,7 @@ function KinkyDungeonDBOpen(): Promise { }); } + /** * Save a game to the database. */ @@ -4713,35 +4729,35 @@ function KDDrawLoadMenu() { }, true, CombarXX + 210, YYstart - 5, 150, 45, TextGet("KDCloudSaves"), KDBaseWhite, undefined, "") } - + DrawCheckboxKDEx( "LoadoverrideOF", () => { KDToggles.OverrideOutfit = !KDToggles.OverrideOutfit; KDSaveToggles(); - + // Dress the KDPreviewModel ModelPreviewLoaded = false; KinkyDungeonDressModelPreview(); return true; - }, true, CombarXX + 20, YY + 165 + 375 + 19, 52, 52, + }, true, CombarXX + 20, YY + 165 + 375 + 19, 52, 52, TextGet("KDToggleOverrideOutfitAbbr"), KDToggles.OverrideOutfit, false, KDTextWhite, undefined, { }); - + DrawCheckboxKDEx( "LoadoverrideCo", () => { KDToggles.OverrideConsent = !KDToggles.OverrideConsent; KDSaveToggles(); - + // Dress the KDPreviewModel ModelPreviewLoaded = false; KinkyDungeonDressModelPreview(); return true; - }, true, CombarXX + 20, YY + 165 + 375 + 25 + 58, 52, 52, + }, true, CombarXX + 20, YY + 165 + 375 + 25 + 58, 52, 52, TextGet("KDToggleOverrideConsentAbbr"), KDToggles.OverrideConsent, false, KDTextWhite, undefined, { @@ -4771,14 +4787,13 @@ function KDDrawLoadMenu() { for (let i = startSaveSlot; i < endSaveSlot; i++) { let num = (i); // Slot button - DrawButtonKDEx(TextGet("KDSaveSlotButton") + num, () => { + DrawButtonKDEx(TextGet("KDSaveSlotButton") + num, async () => { console.log("Pressed button for save slot " + num); loadedSaveforPreview = null; LoadMenuCurrentSlot = num; LoadMenuCurrentSave = loadedsaveslots[num - 1]; - loadedSaveforPreview = KinkyDungeonLoadPreview(LoadMenuCurrentSave); + loadedSaveforPreview = await KinkyDungeonLoadPreview(LoadMenuCurrentSave); - let origSaveSlot = KDSaveSlot; KDSaveSlot = num; // @ts-ignore if (!loadedSaveforPreview.invalid) { @@ -4860,16 +4875,15 @@ function KDDrawLoadMenu() { else { for (let i = 1; i < 3; i++) { let num = (i); - DrawButtonKDEx(TextGet("KDSaveSlotButton") + num, () => { + DrawButtonKDEx(TextGet("KDSaveSlotButton") + num, async () => { // Slot button console.log("Pressed button for save slot " + num); loadedSaveforPreview = null; LoadMenuCurrentSlot = num; LoadMenuCurrentSave = loadedcloudsaveslots[num - 1]; - loadedSaveforPreview = KinkyDungeonLoadPreview(LoadMenuCurrentSave); - let origSaveSlot = KDSaveSlot; - KDSaveSlot = num; - // @ts-ignore + loadedSaveforPreview = await KinkyDungeonLoadPreview(LoadMenuCurrentSave); + KDSaveSlot = num; + // @ts-ignore if (!loadedSaveforPreview.invalid) { // Dress the KDPreviewModel ModelPreviewLoaded = false; @@ -4957,25 +4971,28 @@ function KDDrawLoadMenu() { ElementPosition("saveInputField", CombarXX + 215, YY + 145 - 33, 400, 300 - 85); let newValue = ElementValue("saveInputField"); // Load from Code button - DrawButtonKDEx("LoadFromCodeButton", () => { + DrawButtonKDEx("LoadFromCodeButton", async () => { KinkyDungeonKeybindingsTemp = Object.assign({}, KinkyDungeonKeybindingsTemp); LoadMenuCurrentSlot = undefined; - if (newValue) { - loadedSaveforPreview = KinkyDungeonLoadPreview(newValue); - if (loadedSaveforPreview) LoadMenuCurrentSlot = -1; - } else if (KDSlot0) { - loadedSaveforPreview = KinkyDungeonLoadPreview(KDSlot0); - if (loadedSaveforPreview) LoadMenuCurrentSlot = 0; - LoadMenuCurrentSave = KDSlot0; - } - else { + const decodeSrc = newValue || KDSlot0; + const slotidx = !!newValue ? -1 : 0; + + if (decodeSrc) { + loadedSaveforPreview = await KinkyDungeonLoadPreview (decodeSrc); + if (loadedSaveforPreview) { + LoadMenuCurrentSlot = slotidx; + } + if (slotidx == 0) { + LoadMenuCurrentSave = decodeSrc; + } + } else { loadedSaveforPreview = null; LoadMenuCurrentSlot = -1; } + if (loadedSaveforPreview) { - LoadMenuCurrentSave = newValue ? newValue : KDSlot0; + LoadMenuCurrentSave = newValue || KDSlot0; - let origSaveSlot = KDSaveSlot; KDSaveSlot = 0; // @ts-ignore if (!loadedSaveforPreview.invalid) { @@ -5350,21 +5367,23 @@ function KDDrawLoadMenu() { KDMapData.Grid = ""; KinkyDungeonInitialize(1, true); MiniGameKinkyDungeonCheckpoint = "grv"; - if (KinkyDungeonLoadGame(LoadMenuCurrentSave, KDToggles.OverrideConsent)) { - KDGenMapCallback = () => { - if (KDMapData.Grid == "") - KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], - KDMapData.RoomType || "", KDMapData.MapMod || "", MiniGameKinkyDungeonLevel, false, true); - KinkyDungeonState = "Game"; - if (KinkyDungeonKeybindings) { - KDCommitKeybindings(); - } - KDModsAfterGameStart(); - return "Game"; - }; - KinkyDungeonState = "GenMap"; - - } + KinkyDungeonLoadGame (LoadMenuCurrentSave, KDToggles.OverrideConsent) + .then ((success) => { + if (success) { + KDGenMapCallback = () => { + if (KDMapData.Grid == "") + KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], + KDMapData.RoomType || "", KDMapData.MapMod || "", MiniGameKinkyDungeonLevel, false, true); + KinkyDungeonState = "Game"; + if (KinkyDungeonKeybindings) { + KDCommitKeybindings(); + } + KDModsAfterGameStart(); + return "Game"; + }; + KinkyDungeonState = "GenMap"; + } + }); LoadMenuCurrentSave = undefined; LoadMenuCurrentSlot = undefined; ElementRemove("saveInputField"); @@ -5586,11 +5605,11 @@ function KinkyDungeonDressModelPreview() { KDRefreshSelectedModel(KDPreviewModel); if (KDCurrentModels.get(KDPreviewModel)) KDCurrentModels.get(KDPreviewModel).Poses = loadedSaveforPreview.saveStat.poses; - + UpdateModels(KDPreviewModel); } } - + //CharacterAppearanceRestore(KDPreviewModel, DecompressB64(localStorage.getItem(`kinkydungeonappearance${KDCurrentOutfit}`))) //setTimeout(() => { DrawCharacter(KDPreviewModel, PIXIWidth, PIXIHeight, 0.1); @@ -5610,7 +5629,7 @@ function KinkyDungeonDressModelPreview() { /** * Generate Preview data function */ -function KinkyDungeonLoadPreview(String: string): KinkyDungeonSave { +async function KinkyDungeonLoadPreview(String: string): Promise { if (!String) { return { // @ts-ignore @@ -5619,7 +5638,8 @@ function KinkyDungeonLoadPreview(String: string): KinkyDungeonSave { }; } try { - let str: string = DecompressB64(String.trim()); + let str: string = await KinkyDungeonDecompressSave (String.trim(), SaveType.Game); + let returndata: KinkyDungeonSave = null; // We do a little JS witchery here @@ -5846,8 +5866,8 @@ function KinkyDungeonStartNewGame(Load: boolean = false) { MiniGameKinkyDungeonCheckpoint = "grv"; KDMapData.Grid = ""; if (Load) { - KinkyDungeonLoadGame(undefined, true); - KDSendEvent('loadGame'); + KinkyDungeonLoadGame(undefined, true) + .then ((_) => KDSendEvent('loadGame')); } else { KDSendEvent('newGame'); KDGameData.RoomType = "JourneyFloor";//KinkyDungeonStatsChoice.get("easyMode") ? "ShopStart" : "JourneyFloor"; @@ -5985,7 +6005,7 @@ function KDUpdatePlugSettings(evalHardMode: boolean, allow_backport_consent?: bo KDUpdateHardMode(); } - + KDUpdateConsentSettings(allow_backport_consent); } @@ -6104,18 +6124,21 @@ function KinkyDungeonHandleClick(event: MouseEvent) { KinkyDungeonConfigAppearance = false; KinkyDungeonInitialize(1, true); MiniGameKinkyDungeonCheckpoint = "grv"; - if (KinkyDungeonLoadGame(ElementValue("saveInputField"), KDToggles.OverrideConsent)) { - KDSendEvent('loadGame'); - //KDInitializeJourney(KDJourney); - if (KDMapData.Grid == "") KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], KDMapData.RoomType || "", KDMapData.MapMod || "", MiniGameKinkyDungeonLevel, false, true); - ElementRemove("saveInputField"); - KinkyDungeonState = "Game"; + KinkyDungeonLoadGame (ElementValue("saveInputField"), KDToggles.OverrideConsent) + .then ((success) => { + if (success) { + KDSendEvent('loadGame'); + //KDInitializeJourney(KDJourney); + if (KDMapData.Grid == "") KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], KDMapData.RoomType || "", KDMapData.MapMod || "", MiniGameKinkyDungeonLevel, false, true); + ElementRemove("saveInputField"); + KinkyDungeonState = "Game"; - if (KinkyDungeonKeybindings) { - KDCommitKeybindings(); + if (KinkyDungeonKeybindings) { + KDCommitKeybindings(); + } + KDModsAfterGameStart(); } - KDModsAfterGameStart(); - } + }); return true; } } else if (KinkyDungeonState == "LoadOutfit"){ @@ -6840,16 +6863,34 @@ function KinkyDungeonSaveGame(ToString: boolean = false): KinkyDungeonSave { } +enum SaveType { + Game = 'game', + Outfit = 'outfit', + Wardrobe = 'wardrobe', // Complete outfit collection; TBD +} + +interface SaveWorkerMsg { + op: 'cmp' | 'err' | 'cmp-legacy'; + type: SaveType; + data: string; +} + let KDSaveTimeout = 600000; // 10 minutes -async function KinkyDungeonCompressSave(save: string): Promise { +async function KinkyDungeonCompressSave(save: string, type = SaveType.Game): Promise { if (window.Worker) { + const workerMsg: SaveWorkerMsg = { + op: 'cmp', + type: type, + data: save + }; const myWorker = new Worker("out/saveworker.js"); - let pp = new Promise(function (resolve, reject) { + let pp = new Promise(function (resolve, reject) { myWorker.onmessage = function(e) { console.log('Compressed data received from worker'); resolve(e.data); } - myWorker.postMessage(save); + //myWorker.postMessage(save); + myWorker.postMessage(workerMsg); console.log('Save message posted to worker'); setTimeout(reject, KDSaveTimeout); // 10 min timeout }); @@ -6857,25 +6898,140 @@ async function KinkyDungeonCompressSave(save: string): Promise { .then((v) => { console.log('Yay'); myWorker.terminate(); - return v;}) - .catch((v) => { + return v.data;}) + .catch((_v) => { console.log('Nay'); myWorker.terminate(); return LZString.compressToBase64(save);}); } else { + /** + * IMPORTANT: Keep this block in sync with saveworker.js. + */ console.log('Your browser doesn\'t support web workers.'); - return LZString.compressToBase64(save); + const mime_type = `application/vnd.straightlaced.kinkydungeon.save.${type.toString()}+gzip;version=2`; + try { + // Blob. CompressionStream discards MIME-type; we'll add it later. + const save_b_js = new Blob ([save]); + const save_pipe = save_b_js.stream().pipeThrough (new CompressionStream ('gzip')); + + // Compressed blob. + const resp = new Response (save_pipe, { headers: [["Content-Type", mime_type ]]}); + const save_b_z = await resp.blob(); + + // Snarfed from MDN Web docs. + async function toBase64DataURL (blob: Blob): Promise { + return await new Promise ((resolve, reject) => { + const reader = Object.assign (new FileReader(), { + onload: () => resolve (reader.result as string), + onerror: () => reject (reader.error) + }); + reader.readAsDataURL (blob); + }); + } + + const save_z64 = await toBase64DataURL (save_b_z); + + return save_z64; + } catch (err) { + console.log (`Caught ${err}; falling back to LZString...`); + return LZString.compressToBase64 (save); + } } } +/** + * Decompress a save code into JSON. + * + * @param save - Compressed save code, either old-style LZString, or new `data:` URL. + * @param expected_type - Expected type of save. + * @return - JSON string, suitable for parsing; or null if the expected_type doesn't match the code's type. + */ +async function KinkyDungeonDecompressSave (save: string, expected_type = SaveType.Game): Promise +{ + if (!save || !save.trim) { + return null; + } + + /* + * Trim leading/trailing whitespace, and join newline-separated + * lines into single string. + */ + save = "".concat (...save.trim().split ('\n')); + + if (!save.startsWith ('data:')) { + /* Legacy LSZtring save code. */ + console.log ("Decompressing old-style LZString save code..."); + return LZString.decompressFromBase64 (save); + } + + /* + * New style `data:` URL. + * Verify type and version number. (Don't need to match against the whole thing.) + */ + const save_head = save.slice (0, 256); + + /* + * Check save type. + * TODO: Test for `+gzip` and skip decompression if it's not there. + */ + let re = /vnd.straightlaced.kinkydungeon.save.(\w+)(?:\+gzip)?;/ + let res = save_head.match (re); + if (!res || res.length < 2) { + console.log ("Unrecognized save type."); + return null; + } + const save_type = res[1]; + if (save_type != expected_type) { + console.log (`Expected save code type ${expected_type}, got ${save_type}.`); + return null; + } + + /* + * Check version. + */ + re = /;version=(\d+);/ + res = save_head.match (re); + if (!res || res.length < 2) { + console.log ("Missing version in save code."); + return null; + } + const save_version = res[1]; + if (Number (save_version) != 2) { + console.log (`Unsupported save version ${save_version}`); + return null; + } + + /* + * Decode and decompress the data: URL. (This is fast enough that it + * doesn't need to be in a worker.) + */ + try { + const res = await fetch (save); + const save_b_z = new Blob ([await res.blob()]); + const save_pipe = save_b_z.stream().pipeThrough (new DecompressionStream ('gzip')); + + // Decompressed blob. + const resp = new Response (save_pipe); + const save_js = await resp.text(); + + return save_js; + } catch (err) { + console.log (`Save code decompression failed: ${err}`); + } + return null; +} + +// LZString code // N4IgNgpgbhYgXARgDQgMYAsJoNYAcB7ASwDsAXBABlQCcI8FQBxDAgZwvgFoBWakAAo0ibAiQg0EvfgBkIAQzJZJ8fgFkIZeXFWoASgTwQqqAOpEwO/gFFIAWwjk2JkAGExAKwCudFwElLLzYiMSoAX1Q0djJneGAIkAIaACNYgG0AXUisDnSskAATOjZYkAARCAAzeS8wClQAcwIwApdCUhiEAGZUSBgwWNBbCAcnBBQ3Tx9jJFQAsCCQknGEtiNLPNRSGHIkgE8ENNAokjYvO3lkyEYQEnkHBEECMiW1eTuQBIBHL3eXsgOSAixzEZwuVxmoDuD3gTxeYgAylo7KR5J9UD8/kQAStkCDTudLtc4rd7jM4UsAGLCBpEVrfX7kbGAxDAkAAdwUhGWJOh5IA0iQiJVjGE2cUyDR5B0bnzHmUvGgyAAVeRGOQNZwJF4NDBkcQlca9Ai4R7o0ASqUy3lk+WKlVqiCUiCaNTnOwHbVEXX6iCG2bgE04M1hDJhIA= -function KinkyDungeonLoadGame(String: string = "", kdloadconsent = false) { +async function KinkyDungeonLoadGame(String: string = null, kdloadconsent = false): Promise { localStorage.setItem('KDLastSaveSlot', "" + KDSaveSlot); KinkyDungeonSendEvent("beforeLoadGame", {}); - let str = String ? DecompressB64(String.trim()) : - (localStorage.getItem('KinkyDungeonSave') ? DecompressB64(localStorage.getItem('KinkyDungeonSave')) - : (loadedsaveslots[KDSaveSlot-1] ? DecompressB64(loadedsaveslots[KDSaveSlot-1]) : "")); - if (str) { + const src_str = String ?? localStorage.getItem('KinkyDungeonSave') ?? loadedsaveslots[KDSaveSlot-1]; + + if (src_str) { + //const str = DecompressB64 (src_str); + const str = await KinkyDungeonDecompressSave (src_str); + let saveData: KinkyDungeonSave = JSON.parse(str); if ( saveData && saveData.spells != undefined @@ -7408,7 +7564,7 @@ function KDDrawGameSetupTabs(_xOffset: number = 500, xpad: number = 10, num: num }); ii++; } - + DrawButtonKDEx("backButton", (_b) => { @@ -7420,7 +7576,7 @@ function KDDrawGameSetupTabs(_xOffset: number = 500, xpad: number = 10, num: num return true; } KinkyDungeonState = KinkyDungeonState != "Diff" ? "Diff" : "Name"; - + KDConsentFilter = ""; return true; }, true, 1075, 900, 350, 64, TextGet( @@ -7587,8 +7743,8 @@ async function KDLoadBackupDialog() { } else { KDSendMusicToast(TextGet("KDBackupLoadFail")); } - - + + }); reader.readAsText(file); } catch (err) { @@ -7687,7 +7843,7 @@ async function KDSaveBackupDialog(filename: string, text: string) { reject(null); } }); - + } function KDChangeZoom(change: number) { @@ -7732,7 +7888,7 @@ async function RunGenMapCallback() { function KDReloadChallenge() { KDSeenConsents = (localStorage.getItem("KDSeenConsents") ? JSON.parse(localStorage.getItem("KDSeenConsents")) : []) || []; - + KDConsentArray = (localStorage.getItem("KDConsentArray") ? JSON.parse(localStorage.getItem("KDConsentArray")) : {}) || {}; KinkyDungeonSexyMode = localStorage.getItem("KinkyDungeonSexyMode") != undefined ? localStorage.getItem("KinkyDungeonSexyMode") == "True" : true; KinkyDungeonClassMode = localStorage.getItem("KinkyDungeonClassMode") != undefined ? localStorage.getItem("KinkyDungeonClassMode") : "Mage"; @@ -7931,9 +8087,9 @@ function KDTogglesDraw() { } }) return true; - }, !KDBusySavingBackup, - PIXIWidth - 235, 900, 200, 64, - TextGet("KDFullBackup"), KDBusySavingBackup ? KDBaseLightGrey : KDBaseWhite, undefined, undefined, undefined, + }, !KDBusySavingBackup, + PIXIWidth - 235, 900, 200, 64, + TextGet("KDFullBackup"), KDBusySavingBackup ? KDBaseLightGrey : KDBaseWhite, undefined, undefined, undefined, undefined, undefined, undefined, undefined, { hoverData: { text: TextGet("KDFullBackupDesc") @@ -7942,13 +8098,13 @@ function KDTogglesDraw() { hotkey: KDHotkeyToText(KinkyDungeonKeyMenu[3]), hotkeyPress: KinkyDungeonKeyMenu[3], }); - + DrawButtonKDEx("kdtoggle_load", (b) => { KDLoadBackupDialog(); return true; - }, !KDBusyLoadingFile, - PIXIWidth - 450, 900, 200, 64, - TextGet("KDLoadBackup"), KDBusyLoadingFile ? KDBaseLightGrey : KDBaseWhite, undefined, undefined, undefined, + }, !KDBusyLoadingFile, + PIXIWidth - 450, 900, 200, 64, + TextGet("KDLoadBackup"), KDBusyLoadingFile ? KDBaseLightGrey : KDBaseWhite, undefined, undefined, undefined, undefined, undefined, undefined, undefined, { hoverData: { text: TextGet("KDLoadBackupDesc") @@ -7972,7 +8128,7 @@ function KDTogglesDraw() { for (let toggle of toggles.filter((tog) => {return KDToggleCategories[tog] == KDToggleTab || (!KDToggleCategories[tog] && KDToggleTab == "Main");})) { // Draw temp start screen let txt = KDOptionFilter ? TextGet("KDToggle" + toggle).toLocaleLowerCase() : ""; - + if (KDOptionFilter != "" && !(txt == KDOptionFilter.toLocaleLowerCase() || txt.includes(KDOptionFilter.toLocaleLowerCase())))continue; DrawCheckboxKDEx("toggle" + toggle, () => { KDToggles[toggle] = !KDToggles[toggle]; @@ -8100,7 +8256,7 @@ function KDTogglesDraw() { if (KinkyDungeonGameFlag) { KinkyDungeonState = "Game"; } else KinkyDungeonState = "Menu"; - + KDOptionFilter = ""; KDConsentFilter = ""; //ServerAccountUpdate.QueueData({ KinkyDungeonKeybindings: KinkyDungeonKeybindings }); @@ -8131,11 +8287,11 @@ function KDTextReplace(text: string, replacestrings: string[], FromSuff?: string + from.substring(1); to = to.substring(0, 1) + to.substring(1); - + str = str.replace( from, to); - + } return str; @@ -8203,7 +8359,7 @@ function KDDrawWardrobeButton() { function KDLoadConsentFromSave(saveData: KinkyDungeonSave, override) { if (override && saveData.saveStat) { - + let dontPopulate: Record = {}; for (let entry of Object.entries(KDConsentListBasic)) { @@ -8227,8 +8383,8 @@ function KDFirstRunMainmenu() { if (KDToggles.SkipIntro) { KinkyDungeonState = "Menu"; KDCheckedConsentAtStartup = false; - - + + } else { KDCheckedConsentAtStartup = false; KinkyDungeonState = "Intro"; @@ -8253,4 +8409,4 @@ function KDRunnewConsentCheck() { } } } -} \ No newline at end of file +} diff --git a/out/saveworker.js b/out/saveworker.js index e49e577be..02706fdf9 100644 --- a/out/saveworker.js +++ b/out/saveworker.js @@ -1,8 +1,72 @@ // eslint-disable-next-line strict importScripts('../Scripts/lib/LZString.js'); -onmessage = (e) => { - console.log("Compressing save in worker thread..."); - postMessage(LZString.compressToBase64(e.data)); +// Included for reference. +//enum SaveType { +// Game = 'game', +// Outfit = 'outfit', +// Wardrobe = 'wardrobe', // Complete outfit collection; TBD +//} +// +//interface SaveWorkerMsg { +// op: 'cmp' | 'err'; +// type: SaveType; +// data: string; +//} + +onmessage = async (ev) => { + const msg = ev?.data; + if (!msg || !msg.op || !msg.type || !msg.data) { + if (!msg) { + console.log ("Missing worker MessageEvent payload."); + } else { + console.log (`Missing worker message field(s); only got ${msg}`); + } + postMessage ({ op: 'err', data: '' }); + return; + } + + if (msg.op != "cmp" && msg.op != "cmp-legacy") { + postMessage ({ op: 'err', data: '' }); + return; + } + + console.log ("Compressing save in worker thread..."); + const mime_type = `application/vnd.straightlaced.kinkydungeon.save.${msg.type}+gzip;version=2`; + try { + /* + * FIXME: Temporary. Delete when all compression call sites updated. + */ + if (msg.op === 'cmp-legacy') { + throw new Error ("Legacy compression requested"); + } + // Blob. CompressionStream discards MIME-type; we'll add it later. + const save_b_js = new Blob ([msg.data]); + const save_pipe = save_b_js.stream().pipeThrough (new CompressionStream ('gzip')); + + // Compressed blob. + const resp = new Response (save_pipe, { headers: [["Content-Type", mime_type ]]}); + const save_b_z = await resp.blob(); + + // Snarfed from MDN Web docs. + async function toBase64DataURL (blob) { + return await new Promise ((resolve, reject) => { + const reader = Object.assign (new FileReader(), { + onload: () => resolve (reader.result), + onerror: () => reject (reader.error) + }); + reader.readAsDataURL (blob); + }); + } + + const save_z64 = await toBase64DataURL (save_b_z); + + msg.data = save_z64; + } catch (err) { + console.log (`Caught ${err}; falling back to LZString...`); + msg.data = LZString.compressToBase64 (msg.data); + } + postMessage (msg); + // Is this necessary? close(); -}; \ No newline at end of file +}; From 19e9a2482094adb557ca4478ce1da394262af328 Mon Sep 17 00:00:00 2001 From: "Leo L. Schwab" Date: Mon, 5 May 2025 17:21:39 -0700 Subject: [PATCH 2/4] Alter all call sites for compressed saves. There are a bunch of places where wardrobe outfit codes are loaded and stored, and I had to update all of them. Tried to be judicious about where to place `async` blocks, but there's a good chance there are synchronization errors. Initial tests promising, however; hence this checkin. Save codes still forced to legacy LZStrings. --- Game/src/KinkyDungeonTests.ts | 5 +- Game/src/base/KinkyDungeon.ts | 273 +++++++-------- Game/src/base/game/KinkyDungeonGame.ts | 19 +- Game/src/collection/KinkyDungeonCollection.ts | 22 +- Game/src/player/KinkyDungeonDress.ts | 44 ++- Game/src/player/wardrobe/KDWardrobe.ts | 326 +++++++++--------- Scripts/Appearance.ts | 7 +- 7 files changed, 352 insertions(+), 344 deletions(-) diff --git a/Game/src/KinkyDungeonTests.ts b/Game/src/KinkyDungeonTests.ts index 545b611fb..2f0921146 100644 --- a/Game/src/KinkyDungeonTests.ts +++ b/Game/src/KinkyDungeonTests.ts @@ -40,8 +40,7 @@ function KDTestFullRunthrough(GameLoops: number, Init: boolean, NGP: boolean): b if (Init) { KDSetWorldSlot(0, 1, 0, 0); MiniGameKinkyDungeonCheckpoint = "grv"; - KinkyDungeonInitialize(1); - KDInitPerks(); + KinkyDungeonInitialize(1) .then(() => KDInitPerks()); } for (let i = 0; i < KinkyDungeonMaxLevel * GameLoops; i++) { // Run through the stairs @@ -189,4 +188,4 @@ function KDGetMissingSpellNames() { } return ret; -} \ No newline at end of file +} diff --git a/Game/src/base/KinkyDungeon.ts b/Game/src/base/KinkyDungeon.ts index 753c472d1..2a93a37e8 100644 --- a/Game/src/base/KinkyDungeon.ts +++ b/Game/src/base/KinkyDungeon.ts @@ -1127,44 +1127,47 @@ function KDReloadMainData(force: boolean) { } - KDReloadChallenge(); + (async () => { + KDReloadChallenge(); + + KinkyDungeonNewDress = true; + KDCurrentOutfit = parseInt(localStorage.getItem("kdcurrentoutfit") || 0); + let appearance = await KinkyDungeonDecompressSave (localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit), SaveType.Outfit); + if (!appearance + // No appearance, or legacy + || (StandalonePatched && JSON.parse(appearance).length && JSON.parse(appearance)[0]?.Asset)) + { + KinkyDungeonNewDress = false; + if (StandalonePatched) { + appearance = + '[{"Model":"BanditBoots","Difficulty":0,"Color":"#ffffff","Filters":{"ShoeLeft":{"gamma":1.4000000000000001,"saturation":0.03333333333333333,"contrast":1.5833333333333333,"brightness":1.6833333333333333,"red":1,"green":1,"blue":1,"alpha":1},"ShoeRight":{"gamma":1.4000000000000001,"saturation":0.03333333333333333,"contrast":1.5833333333333333,"brightness":1.6833333333333333,"red":1,"green":1,"blue":1,"alpha":1}}},{"Model":"WitchBlouse","Difficulty":0,"Color":"#ffffff"},{"Model":"WitchCorset","Difficulty":0,"Color":"#ffffff","Filters":{"Corset":{"gamma":1.45,"saturation":0.4666666666666667,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1,"alpha":1}}},{"Model":"MaidSkirt","Difficulty":0,"Color":"#ffffff","Filters":{"Skirt":{"gamma":1,"saturation":0.23333333333333334,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1.55,"alpha":1.0166666666666666}}},{"Model":"MaidSocks","Difficulty":0,"Color":"#ffffff","Filters":{"SockRight":{"gamma":1,"saturation":0,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1,"alpha":1},"SockLeft":{"gamma":1,"saturation":0,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1,"alpha":1}}},{"Model":"StrappyBikini","Difficulty":0,"Color":"Default"},{"Model":"LatexBra","Difficulty":0,"Color":"Default"}]'; + } + } - KinkyDungeonNewDress = true; - KDCurrentOutfit = parseInt(localStorage.getItem("kdcurrentoutfit") || 0); - let appearance = DecompressB64(localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit)); - if (!appearance - // No appearance, or legacy - || (StandalonePatched && JSON.parse(appearance).length && JSON.parse(appearance)[0]?.Asset)) { - KinkyDungeonNewDress = false; - if (StandalonePatched) - appearance = - '[{"Model":"BanditBoots","Difficulty":0,"Color":"#ffffff","Filters":{"ShoeLeft":{"gamma":1.4000000000000001,"saturation":0.03333333333333333,"contrast":1.5833333333333333,"brightness":1.6833333333333333,"red":1,"green":1,"blue":1,"alpha":1},"ShoeRight":{"gamma":1.4000000000000001,"saturation":0.03333333333333333,"contrast":1.5833333333333333,"brightness":1.6833333333333333,"red":1,"green":1,"blue":1,"alpha":1}}},{"Model":"WitchBlouse","Difficulty":0,"Color":"#ffffff"},{"Model":"WitchCorset","Difficulty":0,"Color":"#ffffff","Filters":{"Corset":{"gamma":1.45,"saturation":0.4666666666666667,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1,"alpha":1}}},{"Model":"MaidSkirt","Difficulty":0,"Color":"#ffffff","Filters":{"Skirt":{"gamma":1,"saturation":0.23333333333333334,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1.55,"alpha":1.0166666666666666}}},{"Model":"MaidSocks","Difficulty":0,"Color":"#ffffff","Filters":{"SockRight":{"gamma":1,"saturation":0,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1,"alpha":1},"SockLeft":{"gamma":1,"saturation":0,"contrast":1,"brightness":1,"red":1,"green":1,"blue":1,"alpha":1}}},{"Model":"StrappyBikini","Difficulty":0,"Color":"Default"},{"Model":"LatexBra","Difficulty":0,"Color":"Default"}]'; - - } + await CharacterAppearanceRestore(KinkyDungeonPlayer, appearance, false, true); - CharacterAppearanceRestore(KinkyDungeonPlayer, appearance, false, true); + CharacterReleaseTotal(KinkyDungeonPlayer); - CharacterReleaseTotal(KinkyDungeonPlayer); + CharacterRefresh(KinkyDungeonPlayer); - CharacterRefresh(KinkyDungeonPlayer); + KinkyDungeonInitializeDresses(); + KinkyDungeonDressSet(); - KinkyDungeonInitializeDresses(); - KinkyDungeonDressSet(); + CharacterNaked(KinkyDungeonPlayer); - CharacterNaked(KinkyDungeonPlayer); - - DrawCharacter(KinkyDungeonPlayer, 0, 0, 0.01); - - KDRefreshCharacter.set(KinkyDungeonPlayer, true); - KinkyDungeonDressPlayer(); + DrawCharacter(KinkyDungeonPlayer, 0, 0, 0.01); - KDInitProtectedGroups(KinkyDungeonPlayer); + KDRefreshCharacter.set(KinkyDungeonPlayer, true); + KinkyDungeonDressPlayer(); + KDInitProtectedGroups(KinkyDungeonPlayer); + + })(); } if (localStorage.getItem("KinkyDungeonKeybindings") && JSON.parse(localStorage.getItem("KinkyDungeonKeybindings"))) { @@ -1297,8 +1300,10 @@ function KinkyDungeonLoad(): void { } else { KinkyDungeonState = "Game"; if (!KinkyDungeonGameData) { - KDSetWorldSlot(0, 1, 0, 0); - KinkyDungeonInitialize(1); + (async () => { + KDSetWorldSlot(0, 1, 0, 0); + await KinkyDungeonInitialize(1); + })(); } } @@ -1867,7 +1872,7 @@ function KinkyDungeonRun() { loadedsaveslots[num - 1] = code; code_z = code; //let decoded = LZString.decompressFromBase64(code); - return KinkyDungeonDecompressSave (code); + return KinkyDungeonDecompressSave (code, SaveType.Game); }).then ((decoded) => { if (!decoded) { return; @@ -1912,7 +1917,7 @@ function KinkyDungeonRun() { loadedsaveslots[num - 1] = code; //let decoded = LZString.decompressFromBase64(code); - return KinkyDungeonDecompressSave (code); + return KinkyDungeonDecompressSave (code, SaveType.Game); }).then ((decoded) => { if (decoded) { const playerName = JSON.parse(decoded)?.KDGameData?.PlayerName; @@ -2115,16 +2120,14 @@ function KinkyDungeonRun() { let newValue = ElementValue("saveInputField"); if (newValue != KDOldSaveCodeValue) { - - KDOldSaveCodeValue = newValue; - let itt = localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); - let orig = itt ? - JSON.parse(LZString.decompressFromBase64(itt)).appearance - || itt : ""; - if (orig != ElementValue("saveInputField")) KDOriginalValue = orig; - //let decompressed = DecompressB64(ElementValue("saveInputField")); - KinkyDungeonDecompressSave (ElementValue("saveInputField"), SaveType.Game) - .then ((decompressed) => { + (async () => { + KDOldSaveCodeValue = newValue; + let itt = localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); + let orig = itt ? JSON.parse (await KinkyDungeonDecompressSave (itt, SaveType.Outfit)).appearance || itt + : ""; + if (orig != ElementValue("saveInputField")) KDOriginalValue = orig; + //let decompressed = DecompressB64(ElementValue("saveInputField")); + const decompressed = await KinkyDungeonDecompressSave (ElementValue("saveInputField"), SaveType.Game); if (decompressed) { let origAppearance = KinkyDungeonPlayer.Appearance; try { @@ -2134,7 +2137,7 @@ function KinkyDungeonRun() { KDCurrentModels.get(KinkyDungeonPlayer).Poses = decodeSave.saveStat.poses; } let appearanceFromSave = JSON.stringify(decodeSave.saveStat.appearance); - CharacterAppearanceRestore(KinkyDungeonPlayer, appearanceFromSave, false, false); + await CharacterAppearanceRestore(KinkyDungeonPlayer, appearanceFromSave, false, false); KinkyDungeonPlayer.Palette = decodeSave.saveStat.Palette; KinkyDungeonPlayer.metadata = decodeSave.saveStat.metadata; CharacterRefresh(KinkyDungeonPlayer); @@ -2152,7 +2155,7 @@ function KinkyDungeonRun() { KDRefreshSelectedModel(KinkyDungeonPlayer); } } - }); + })(); } DrawButtonKDEx( @@ -2193,27 +2196,28 @@ function KinkyDungeonRun() { let newValue = ElementValue("saveInputField"); if (newValue != KDOldValue) { - - let itt = localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); - let orig = itt ? - JSON.parse(LZString.decompressFromBase64(itt)).appearance - || itt : ""; - if (orig != ElementValue("saveInputField")) KDOriginalValue = orig; - let decompressed = DecompressB64(ElementValue("saveInputField")); - if (decompressed) { - try { - CharacterAppearanceRestore(Char, decompressed, true, false); - CharacterRefresh(Char); - KDOldValue = newValue; - KDInitProtectedGroups(Char); - KinkyDungeonDressPlayer(Char, true); - - if (Char.Appearance.length == 0) - throw new DOMException(); - } catch (e) { - console.log("Invalid code."); + (async () => { + let itt = localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); + let orig = itt ? JSON.parse(await KinkyDungeonDecompressSave (itt, SaveType.Outfit)).appearance || itt + : ""; + if (orig != ElementValue("saveInputField")) KDOriginalValue = orig; + //let decompressed = DecompressB64(ElementValue("saveInputField")); + const decompressed = await KinkyDungeonDecompressSave (ElementValue("saveInputField"), SaveType.Outfit); + if (decompressed) { + try { + await CharacterAppearanceRestore(Char, decompressed, true, false); + CharacterRefresh(Char); + KDOldValue = newValue; + KDInitProtectedGroups(Char); + KinkyDungeonDressPlayer(Char, true); + + if (Char.Appearance.length == 0) + throw new DOMException(); + } catch (e) { + console.log("Invalid code."); + } } - } + })(); } ElementPosition("saveInputField", 1250, 350, 1000, 230); @@ -5360,30 +5364,28 @@ function KDDrawLoadMenu() { hotkeyPress: KinkyDungeonKeySkip[0], }); // Play Game with current save data! - DrawButtonKDEx("KDLoadGame", () => { + DrawButtonKDEx("KDLoadGame", async () => { if (LoadMenuCurrentSave != "") { KinkyDungeonKeybindingsTemp = Object.assign({}, KinkyDungeonKeybindingsTemp); KinkyDungeonNewGame = 0; KDMapData.Grid = ""; - KinkyDungeonInitialize(1, true); + await KinkyDungeonInitialize(1, true); MiniGameKinkyDungeonCheckpoint = "grv"; - KinkyDungeonLoadGame (LoadMenuCurrentSave, KDToggles.OverrideConsent) - .then ((success) => { - if (success) { - KDGenMapCallback = () => { - if (KDMapData.Grid == "") - KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], - KDMapData.RoomType || "", KDMapData.MapMod || "", MiniGameKinkyDungeonLevel, false, true); - KinkyDungeonState = "Game"; - if (KinkyDungeonKeybindings) { - KDCommitKeybindings(); - } - KDModsAfterGameStart(); - return "Game"; - }; - KinkyDungeonState = "GenMap"; - } - }); + const success = await KinkyDungeonLoadGame (LoadMenuCurrentSave, KDToggles.OverrideConsent); + if (success) { + KDGenMapCallback = () => { + if (KDMapData.Grid == "") + KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], + KDMapData.RoomType || "", KDMapData.MapMod || "", MiniGameKinkyDungeonLevel, false, true); + KinkyDungeonState = "Game"; + if (KinkyDungeonKeybindings) { + KDCommitKeybindings(); + } + KDModsAfterGameStart(); + return "Game"; + }; + KinkyDungeonState = "GenMap"; + } LoadMenuCurrentSave = undefined; LoadMenuCurrentSlot = undefined; ElementRemove("saveInputField"); @@ -5858,48 +5860,51 @@ async function KinkyDungeonLoadPreview(String: string): Promise KDSendEvent('loadGame')); - } else { - KDSendEvent('newGame'); - KDGameData.RoomType = "JourneyFloor";//KinkyDungeonStatsChoice.get("easyMode") ? "ShopStart" : "JourneyFloor"; - KDSetWorldSlot(0, 0, 0, 0); - KDInitializeJourney(""); + (async () => { + KinkyDungeonSendEvent("beforeNewGame", {Load: Load}); + KinkyDungeonNewGame = 0; + let cp = KinkyDungeonMapIndex.grv; + KDUpdateHardMode(); + await KinkyDungeonInitialize(1, Load); + MiniGameKinkyDungeonCheckpoint = "grv"; + KDMapData.Grid = ""; + if (Load) { + await KinkyDungeonLoadGame(undefined, true); + KDSendEvent('loadGame'); + } else { + KDSendEvent('newGame'); + KDGameData.RoomType = "JourneyFloor";//KinkyDungeonStatsChoice.get("easyMode") ? "ShopStart" : "JourneyFloor"; + KDSetWorldSlot(0, 0, 0, 0); + KDInitializeJourney(""); - if (KDTileToTest) { - KinkyDungeonMapIndex.grv = cp; - } + if (KDTileToTest) { + KinkyDungeonMapIndex.grv = cp; + } - KDGameData.PlayerName = localStorage.getItem("PlayerName") || "Ada"; - KinkyDungeonPlayer.Name = KDGameData.PlayerName; - } - if (!KDMapData.Grid) { - KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], "JourneyFloor", "", MiniGameKinkyDungeonLevel, false, Load); - KDInitPerks(); - } - KinkyDungeonState = "Game"; + KDGameData.PlayerName = localStorage.getItem("PlayerName") || "Ada"; + KinkyDungeonPlayer.Name = KDGameData.PlayerName; + } + if (!KDMapData.Grid) { + KinkyDungeonCreateMap(KinkyDungeonMapParams[(KinkyDungeonMapIndex[MiniGameKinkyDungeonCheckpoint] || MiniGameKinkyDungeonCheckpoint)], "JourneyFloor", "", MiniGameKinkyDungeonLevel, false, Load); + KDInitPerks(); + } + KinkyDungeonState = "Game"; - if (KinkyDungeonKeybindings) { - KDCommitKeybindings(); - } - if (KDSoundEnabled()) AudioPlayInstantSoundKD(KinkyDungeonRootDirectory + "Audio/StoneDoor_Close.ogg"); + if (KinkyDungeonKeybindings) { + KDCommitKeybindings(); + } + if (KDSoundEnabled()) AudioPlayInstantSoundKD(KinkyDungeonRootDirectory + "Audio/StoneDoor_Close.ogg"); - KDModsAfterGameStart(); - if (!Load) - KinkyDungeonSendEvent("afterNewGame", {Load: Load}); - else KinkyDungeonSendEvent("afterModsLoadedAndLoadGame", {Load: Load}); + KDModsAfterGameStart(); + if (!Load) + KinkyDungeonSendEvent("afterNewGame", {Load: Load}); + else + KinkyDungeonSendEvent("afterModsLoadedAndLoadGame", {Load: Load}); + })(); } let KDConsentPerkTypes = ["Red", "Yellow", "Green"]; @@ -6118,14 +6123,15 @@ function KinkyDungeonHandleClick(event: MouseEvent) { KDHandleTileEditor(); } else if (KinkyDungeonState == "Load"){ if (MouseIn(875, 750, 350, 64)) { - KinkyDungeonNewGame = 0; - KDMapData.Grid = ""; - if (!KDToggles.OverrideOutfit) - KinkyDungeonConfigAppearance = false; - KinkyDungeonInitialize(1, true); - MiniGameKinkyDungeonCheckpoint = "grv"; - KinkyDungeonLoadGame (ElementValue("saveInputField"), KDToggles.OverrideConsent) - .then ((success) => { + (async () => { + KinkyDungeonNewGame = 0; + KDMapData.Grid = ""; + if (!KDToggles.OverrideOutfit) + KinkyDungeonConfigAppearance = false; + await KinkyDungeonInitialize(1, true); + MiniGameKinkyDungeonCheckpoint = "grv"; + //if (KinkyDungeonLoadGame(ElementValue("saveInputField"))) { + const success = await KinkyDungeonLoadGame (ElementValue("saveInputField"), KDToggles.OverrideConsent) if (success) { KDSendEvent('loadGame'); //KDInitializeJourney(KDJourney); @@ -6138,7 +6144,7 @@ function KinkyDungeonHandleClick(event: MouseEvent) { } KDModsAfterGameStart(); } - }); + })(); return true; } } else if (KinkyDungeonState == "LoadOutfit"){ @@ -6879,7 +6885,7 @@ let KDSaveTimeout = 600000; // 10 minutes async function KinkyDungeonCompressSave(save: string, type = SaveType.Game): Promise { if (window.Worker) { const workerMsg: SaveWorkerMsg = { - op: 'cmp', + op: 'cmp-legacy', type: type, data: save }; @@ -6942,7 +6948,7 @@ async function KinkyDungeonCompressSave(save: string, type = SaveType.Game): Pro /** * Decompress a save code into JSON. * - * @param save - Compressed save code, either old-style LZString, or new `data:` URL. + * @param save - Compressed save code, either legacy LZString, or new `data:` URL. * @param expected_type - Expected type of save. * @return - JSON string, suitable for parsing; or null if the expected_type doesn't match the code's type. */ @@ -6960,7 +6966,7 @@ async function KinkyDungeonDecompressSave (save: string, expected_type = SaveTyp if (!save.startsWith ('data:')) { /* Legacy LSZtring save code. */ - console.log ("Decompressing old-style LZString save code..."); + console.log ("Decompressing legacy LZString save code..."); return LZString.decompressFromBase64 (save); } @@ -7029,8 +7035,7 @@ async function KinkyDungeonLoadGame(String: string = null, kdloadconsent = false const src_str = String ?? localStorage.getItem('KinkyDungeonSave') ?? loadedsaveslots[KDSaveSlot-1]; if (src_str) { - //const str = DecompressB64 (src_str); - const str = await KinkyDungeonDecompressSave (src_str); + const str = await KinkyDungeonDecompressSave (src_str, SaveType.Game); let saveData: KinkyDungeonSave = JSON.parse(str); if ( saveData @@ -8300,7 +8305,7 @@ function KDTextReplace(text: string, replacestrings: string[], FromSuff?: string let KinkyDungeonPreviousState = ""; function KDDrawWardrobeButton() { - DrawButtonKDEx("GoToWardrobe", (_bdata) => { + DrawButtonKDEx("GoToWardrobe", async (_bdata) => { if (StandalonePatched) { KDSpeakerNPC = null; @@ -8319,13 +8324,12 @@ function KDDrawWardrobeButton() { KDUpdateModelList(); KDRefreshOutfitInfo(); let itt = localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); - let orig = itt ? - JSON.parse(LZString.decompressFromBase64(itt)).appearance - || itt : ""; - let current = LZString.compressToBase64(AppearanceItemStringify(KinkyDungeonPlayer.Appearance)); + let orig = itt ? JSON.parse(await KinkyDungeonDecompressSave (itt, SaveType.Outfit)).appearance || itt + : ""; + let current = await KinkyDungeonCompressSave (AppearanceItemStringify(KinkyDungeonPlayer.Appearance), SaveType.Outfit); if (orig != current) KDOriginalValue = orig; } - let appearance = DecompressB64(localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit)); + let appearance = await KinkyDungeonDecompressSave (localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit), SaveType.Outfit); if (appearance) { CharacterAppearanceRestore(KinkyDungeonPlayer, appearance, false, true); let parsed = JSON.parse(appearance); @@ -8354,7 +8358,6 @@ function KDDrawWardrobeButton() { } return true; }, true, 30, 942, 440, 50, TextGet("KinkyDungeonDressPlayer"), KDBaseWhite, ""); - } function KDLoadConsentFromSave(saveData: KinkyDungeonSave, override) { diff --git a/Game/src/base/game/KinkyDungeonGame.ts b/Game/src/base/game/KinkyDungeonGame.ts index ca2041db2..9e465734f 100644 --- a/Game/src/base/game/KinkyDungeonGame.ts +++ b/Game/src/base/game/KinkyDungeonGame.ts @@ -462,7 +462,7 @@ function KDResetEventData(Data?: any) { -function KinkyDungeonInitialize(Level: number, Load?: any) { +async function KinkyDungeonInitialize(Level: number, Load?: any) { KDWorldMap = {}; KDMapData = KDDefaultMapData(0, 0); KDCurrentWorldSlot = {x: 0, y: 0}; @@ -497,9 +497,10 @@ function KinkyDungeonInitialize(Level: number, Load?: any) { KinkyDungeonDressSet(); // Refresh the character - CharacterAppearanceRestore(KinkyDungeonPlayer, CharacterAppearanceStringify(KinkyDungeonPlayer, - KDGetCharMetadata(KinkyDungeonPlayer) - ), false, true); + await CharacterAppearanceRestore (KinkyDungeonPlayer, + CharacterAppearanceStringify (KinkyDungeonPlayer, + KDGetCharMetadata(KinkyDungeonPlayer)), + false, true); KinkyDungeonDrawState = "Game"; KDResetAlternateInventoryRender(); KDRefreshCharacter.set(KinkyDungeonPlayer, true); @@ -2372,7 +2373,7 @@ function KinkyDungeonGameKeyDown() { //case KinkyDungeonKeyMenu[6]: KinkyDungeonDrawState = "Collection"; break; //case KinkyDungeonKeyMenu[7]: KinkyDungeonDrawState = "Facilities"; break; /*case KinkyDungeonKeyMenu[9]: { - KinkyDungeonDrawState = "JourneyMap"; + KinkyDungeonDrawState = "JourneyMap"; KDGameData.UseJourneyTarget = false; break;}*/ case KinkyDungeonKeySkip[0]: @@ -3738,8 +3739,8 @@ function KDAddAppearance ( C.Appearance.push(NA); return NA; } - - + + return null; } @@ -4155,7 +4156,7 @@ function KDSprintCost(sprintdata?: any, sprintCost?: number, accountForSlow: boo boost: 0, sprintCostOverride: sprintCost, }; - data.cost = (-KDSprintCostBase - KDSprintCostSlowLevel[Math.min(KDSprintCostSlowLevel.length-1, + data.cost = (-KDSprintCostBase - KDSprintCostSlowLevel[Math.min(KDSprintCostSlowLevel.length-1, Math.round(KinkyDungeonSlowLevel))] + ( (accountForSlow && KinkyDungeonSlowLevel > 1) ? -KDSprintAdjustSlowed : 0 )); @@ -4423,4 +4424,4 @@ let KDCustomKeyDown = [ }, ]; let KDCustomKeyUp = [ -]; \ No newline at end of file +]; diff --git a/Game/src/collection/KinkyDungeonCollection.ts b/Game/src/collection/KinkyDungeonCollection.ts index 65d791cf4..9bb233856 100644 --- a/Game/src/collection/KinkyDungeonCollection.ts +++ b/Game/src/collection/KinkyDungeonCollection.ts @@ -509,15 +509,15 @@ function KDDrawSelectedCollectionMember(value: KDCollectionEntry, x: number, y: KDRefreshCharacter.set(KDSpeakerNPC, true); KinkyDungeonCheckClothesLoss = true; KinkyDungeonDressPlayer(KDSpeakerNPC, false, false, KDGameData.NPCRestraints ? KDGameData.NPCRestraints[value.id + ''] : undefined); - + //} }; if (value.customOutfit) { let outfit = value.customOutfit; - KDWardrobeRevertCallback = () => { + KDWardrobeRevertCallback = async () => { KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; if (outfit) - CharacterAppearanceRestore(KDSpeakerNPC, DecompressB64(outfit),false, true); + await CharacterAppearanceRestore(KDSpeakerNPC, DecompressB64(outfit),false, true); CharacterRefresh(KDSpeakerNPC); KDInitProtectedGroups(KDSpeakerNPC); KDRefreshCharacter.set(KDSpeakerNPC, true); @@ -697,7 +697,7 @@ function KDDrawSelectedCollectionMember(value: KDCollectionEntry, x: number, y: NPCTags.set(KDSpeakerNPC, KinkyDungeonUpdateRestraints(KDSpeakerNPC, value.id, 0)); KDEntityRestraintMetadata.set(value.id, KDUpdateRestraintMetadata(value.id, 0)); } - KinkyDungeonDressPlayer(KDSpeakerNPC, false, false, + KinkyDungeonDressPlayer(KDSpeakerNPC, false, false, KDGameData.NPCRestraints ? KDGameData.NPCRestraints[value.id + ''] : undefined); DrawCharacter(KDSpeakerNPC, x + 20 + (KDToggleBigView ? 0 : 100), @@ -780,7 +780,7 @@ function KDDrawSelectedCollectionMember(value: KDCollectionEntry, x: number, y: } else { - + KDDraw(kdcanvas, kdpixisprites, value.name + "_coll," + value.id, KinkyDungeonRootDirectory + dir + sp + ".png", x + 20, y + 80, @@ -1157,8 +1157,8 @@ function KDDrawCollectionInventory(x: number, y: number, drawCallback?: (value: DrawCharacter(char, XX + size/2.7, YY + size*0.2, - size/1300, false, kdcanvas, undefined, - CHIBIMOD, 101, false, undefined, + size/1300, false, kdcanvas, undefined, + CHIBIMOD, 101, false, undefined, value.name + "_coll," + value.id, CHIBIMODEND); } @@ -1275,8 +1275,8 @@ function KDDrawCollectionInventory(x: number, y: number, drawCallback?: (value: DrawCharacter(char, XX + size/2.7, YY + size*0.2, - size/1300, false, kdcanvas, undefined, - CHIBIMOD, 101, false, undefined, + size/1300, false, kdcanvas, undefined, + CHIBIMOD, 101, false, undefined, value.name + "_coll," + value.id, CHIBIMODEND); } @@ -1938,5 +1938,5 @@ function KDGenCharForCollection(value: KDCollectionEntry, enemyType: enemy) { KDSpeakerNPC, true); } KDRefreshCharacter.set(KDSpeakerNPC, true); - } -} \ No newline at end of file + } +} diff --git a/Game/src/player/KinkyDungeonDress.ts b/Game/src/player/KinkyDungeonDress.ts index 3c12726b8..66a7643cc 100644 --- a/Game/src/player/KinkyDungeonDress.ts +++ b/Game/src/player/KinkyDungeonDress.ts @@ -144,7 +144,7 @@ function KinkyDungeonDressPlayer ( customPlayerTags?: Map, customFaction?: string, noDressOutfit?: boolean, - forceUseOutfit?: boolean + forceUseOutfit?: boolean ) { if (!Character) Character = KinkyDungeonPlayer; @@ -187,7 +187,7 @@ function KinkyDungeonDressPlayer ( let forceCustomFaction = !!customFaction; if ((KinkyDungeonState != "Wardrobe" || KDShowCharacterPalette)) { - + if (Character == KinkyDungeonPlayer) { let outfit = KDOutfit({name: KinkyDungeonCurrentDress}); let restraintPalette = KDToggles.RestraintPalette ? KDGetRestraintsPalette(Character) : ""; @@ -201,18 +201,17 @@ function KinkyDungeonDressPlayer ( } } else { if (customFaction == undefined) { - let palette = + let palette = (Character.metadata?.palette || Character.Palette); if (palette) { customFaction = palette; } } } - + } try { - if (!KDGameData.NPCRestraints) KDGameData.NPCRestraints = {}; let data = { hideShrines: {}, @@ -545,7 +544,7 @@ function KinkyDungeonDressPlayer ( } - let item = KDInventoryWear(Character, clothes.Item, clothes.Group, undefined, + let item = KDInventoryWear(Character, clothes.Item, clothes.Group, undefined, clothes.Color, filters, clothes.Properties, clothes.factionFilters); alreadyClothed[clothes.Group || clothes.Item] = true; if (item) { @@ -614,10 +613,10 @@ function KinkyDungeonDressPlayer ( } } } - - KDInventoryWear(Character, clothes.Item, clothes.Group, undefined, - clothes.Color, filters, + + KDInventoryWear(Character, clothes.Item, clothes.Group, undefined, + clothes.Color, filters, clothes.Properties, clothes.factionFilters); alreadyClothed[clothes.Group || clothes.Item] = true; } @@ -845,7 +844,7 @@ function KinkyDungeonDressPlayer ( } if (!KDCurrentModels.get(Character)?.Poses?.Hair && KDModelHair[hairstyle]) { for (let hair of Object.values(KDModelHair[hairstyle])) { - KDInventoryWear(Character, hair.Item, undefined, undefined, undefined, hair.Filters, + KDInventoryWear(Character, hair.Item, undefined, undefined, undefined, hair.Filters, hair.Properties, hair.factionFilters); ReUpdate = true; } @@ -867,7 +866,7 @@ function KinkyDungeonDressPlayer ( } if (!KDCurrentModels.get(Character)?.Poses?.Cosplay && KDModelCosplay[cosplaystyle]) { for (let cosplay of Object.values(KDModelCosplay[cosplaystyle])) { - KDInventoryWear(Character, cosplay.Item, undefined, undefined, undefined, + KDInventoryWear(Character, cosplay.Item, undefined, undefined, undefined, cosplay.Filters, cosplay.Properties, cosplay.factionFilters); ReUpdate = true; @@ -960,8 +959,8 @@ function KinkyDungeonWearForcedClothes(C: Character, restraints?: item[], extraF } } } - KDInventoryWear(C, dress.Model, undefined, undefined, undefined, - dress.inheritFilters ? KDRestraint(inv).Filters : (filters), + KDInventoryWear(C, dress.Model, undefined, undefined, undefined, + dress.inheritFilters ? KDRestraint(inv).Filters : (filters), Properties); }); } @@ -1000,7 +999,7 @@ function KDInventoryWear(Character: Character, AssetName: string, AssetGroup: st ): Item { const M = StandalonePatched ? ModelDefs[AssetName] : undefined; if (!M) return; - let item = KDAddModel(Character, AssetGroup, M, color || "Default", filters, undefined, + let item = KDAddModel(Character, AssetGroup, M, color || "Default", filters, undefined, Properties, factionFilters); //CharacterAppearanceSetItem(KinkyDungeonPlayer, AssetGroup, A, color || A.DefaultColor,0,-1, false); CharacterRefresh(Character, true); @@ -1056,7 +1055,7 @@ function KDApplyItem(C: Character, inv: item, tags: any, customFaction: string = let restraint = KDRestraint(inv); let AssetGroup = restraint.AssetGroup ? restraint.AssetGroup : restraint.Group; let faction = (inv.forceFaction != undefined) ? inv.forceFaction : (((forceCustomFaction || !inv.faction) && customFaction) ? customFaction : (inv.faction ? inv.faction : "")); - + // faction color system let filters = (restraint.Filters || (ModelDefs[restraint.Model || restraint.Asset])?.Filters) ? JSON.parse(JSON.stringify(restraint.Filters || (ModelDefs[restraint.Model || restraint.Asset])?.Filters)) @@ -1217,7 +1216,7 @@ function KDGetExtraPoses(C: Character): string[] { poses.push("Pulled"); } } - + } } return poses; @@ -1281,23 +1280,22 @@ function KDUpdateTempPoses(Character: Character) { } /** - * + * * @param C character to get palettes for (optional) * @param safe safe = deep copy, otherwise expecting reference only (no modify) */ function KDGetPalettes(C: Character, safe?: boolean, includeDefault: boolean = true, defaultOverride?: Record>): Record> { if (!defaultOverride) defaultOverride = KDDefaultWardrobePalettes; - + if (C?.metadata?.customColors) { let newPalettes: Record> = {}; for (let palette in KinkyDungeonFactionFilters) { newPalettes[palette] = KinkyDungeonFactionFilters[palette]; - } for (let palette in C.metadata.customColors) { newPalettes[palette] = C.metadata.customColors[palette]; } - + if (includeDefault) { for (let palette in defaultOverride) { if (newPalettes[palette]) continue; @@ -1316,7 +1314,7 @@ function KDGetPalettes(C: Character, safe?: boolean, includeDefault: boolean = t if (includeDefault) { for (let palette in defaultOverride) { if (newPalettes[palette]) continue; - newPalettes[palette] = defaultOverride[palette]; + newPalettes[palette] = defaultOverride[palette]; } } return safe ? structuredClone(newPalettes) : newPalettes; @@ -1425,5 +1423,5 @@ function KDGetPlayerPalette(C: Character) { } let DefaultStyles = [ - "BlueHair","GreenHair", "WhiteHair", "Ice", "Water", "Earth", "Air", "Fire", "RedHair", -]; \ No newline at end of file + "BlueHair","GreenHair", "WhiteHair", "Ice", "Water", "Earth", "Air", "Fire", "RedHair", +]; diff --git a/Game/src/player/wardrobe/KDWardrobe.ts b/Game/src/player/wardrobe/KDWardrobe.ts index cfe5c9a98..d2f3f3172 100644 --- a/Game/src/player/wardrobe/KDWardrobe.ts +++ b/Game/src/player/wardrobe/KDWardrobe.ts @@ -690,7 +690,7 @@ function KDDrawColorSliders(X: number, Y: number, C: Character, Model: Model): v bb}`); ElementValue("KDCopyFilter", JSON.stringify(Model.Filters[KDCurrentLayer])); ForceRefreshModels(C); - }, + }, (key) => { KDChangeWardrobe(C); if (!Model.Filters) Model.Filters = {}; @@ -731,7 +731,7 @@ function KDDrawColorSliders(X: number, Y: number, C: Character, Model: Model): v Model.Filters[KDCurrentLayer].green = g; Model.Filters[KDCurrentLayer].blue = b; Object.assign(KDCurrentModels.get(C).Models.get(Model.Name), JSON.parse(JSON.stringify(Model))); - + UpdateModels(C); }, (key, override, desaturate) => { KDChangeWardrobe(C); @@ -750,7 +750,7 @@ function KDDrawColorSliders(X: number, Y: number, C: Character, Model: Model): v } Model.factionFilters[KDCurrentLayer] = {color: key, override: override, desaturate: desaturate}; } - + Object.assign(KDCurrentModels.get(C).Models.get(Model.Name), JSON.parse(JSON.stringify(Model))); lastGlobalRefresh = CommonTime() - GlobalRefreshInterval + 10; ForceRefreshModels(C); @@ -770,16 +770,16 @@ function KDDrawColorSliders(X: number, Y: number, C: Character, Model: Model): v KDToggles.PaletteColorPicker = false; KDPropsSlider = false; return true; - }, true, X - 240, YY + 40, 140, 30, TextGet("KDColorPickerSimple"), + }, true, X - 240, YY + 40, 140, 30, TextGet("KDColorPickerSimple"), KDBaseWhite, undefined, undefined, undefined, KDToggles.PaletteColorPicker || KDPropsSlider || !KDToggles.SimpleColorPicker, KDButtonColor); DrawButtonKDEx("tab_ColorPickerPalette", (_b) => { KDToggles.PaletteColorPicker = true; KDPropsSlider = false; return true; - }, true, X - 240 + 290, YY + 40, 140, 30, + }, true, X - 240 + 290, YY + 40, 140, 30, TextGet("KDColorPickerPalette"), KDBaseWhite, undefined, undefined, undefined, - KDPropsSlider || !KDToggles.PaletteColorPicker, + KDPropsSlider || !KDToggles.PaletteColorPicker, (Model.factionFilters && Model.factionFilters[KDCurrentLayer]) ? KDTextGray3 : KDButtonColor); DrawButtonKDEx("tab_ColorPickerAdvanced", (_b) => { KDToggles.SimpleColorPicker = false; @@ -1271,7 +1271,7 @@ function KDDrawModelList(X: number, C: Character) { , true, X+220, 100 + buttonSpacing * i, 190, buttonHeight, !toplevel ? "" : TextGet("m_" + toplevel), (KDCurrentModels.get(C).Models.has(toplevel) || hasTopLevel[toplevel]) ? KDBaseWhite : faded, "", - undefined, undefined, + undefined, undefined, KDCategoryFilterSpecialTopNoBorder[mainCat] ? KDCategoryFilterSpecialTopNoBorder[mainCat](C, toplevel, index_top, toplevel) : index_top != KDModelList_Toplevel_index, KDButtonColor); @@ -1285,7 +1285,7 @@ function KDDrawModelList(X: number, C: Character) { : clickSublevel(sublevel, index_sub, sublevel), true, X+440, 100 + buttonSpacing * i, 190, buttonHeight, !sublevel ? "" : TextGet("m_" + sublevel), KDCurrentModels.get(C).Models.has(sublevel) ? KDBaseWhite : faded, "", - undefined, undefined, + undefined, undefined, KDCategoryFilterSpecialSubNoBorder[mainCat] ? KDCategoryFilterSpecialSubNoBorder[mainCat](C, sublevel, index_sub, sublevel) : index_sub != KDModelList_Sublevel_index, KDButtonColor); @@ -1427,7 +1427,7 @@ let KDDefaultWardrobePalettes: Record> = { let KDWardrobePreviewRestraints = ""; function KDDrawSubmeshEditor() { - let CF = KDTextField("KDSubmesh", 10, 60, 480, 30, + let CF = KDTextField("KDSubmesh", 10, 60, 480, 30, undefined, undefined, "300"); if (CF.Created) { SubmeshEditorBuffer = null; @@ -1455,7 +1455,7 @@ function KDDrawWardrobe(_screen: string, Character: Character) { let C = Character || KinkyDungeonPlayer; if (KDDebugMode) { - DrawButtonKDEx("togglewireframeeditor", + DrawButtonKDEx("togglewireframeeditor", () => { KDSubmeshEditor = !KDSubmeshEditor; SubmeshEditorBuffer = null; @@ -1489,7 +1489,7 @@ function KDDrawWardrobe(_screen: string, Character: Character) { KDDrawSubmeshEditor(); } if (SubmeshEditorBufferOrig && SubmeshEditorBuffer) - DrawButtonKDEx("exportwireframediff", + DrawButtonKDEx("exportwireframediff", () => { let data = [...SubmeshEditorBuffer].map((a, index) => { return a - KDTemplateEmptyMesh[index]; @@ -1520,16 +1520,15 @@ function KDDrawWardrobe(_screen: string, Character: Character) { ElementValue("KDOutfitName", KDOutfitInfo[KDCurrentOutfit]); } if (KDShowCharacterPalette) { - KDCurrentCharacterPalettes = KDGetPalettes(C, true, true, + KDCurrentCharacterPalettes = KDGetPalettes(C, true, true, C == KinkyDungeonPlayer ? KDDefaultWardrobePalettes : ( - (KinkyDungeonPlayer.metadata?.customColors) ? Object.assign(Object.assign({}, KDDefaultWardrobePalettes), + (KinkyDungeonPlayer.metadata?.customColors) ? Object.assign(Object.assign({}, KDDefaultWardrobePalettes), KinkyDungeonPlayer.metadata.customColors) : KDDefaultWardrobePalettes ) ); - - - + + let YY = 55 let size = 48; let spacing = 50; @@ -1560,7 +1559,7 @@ function KDDrawWardrobe(_screen: string, Character: Character) { KDDressWardrobeChar(C); return true; }, true, 1150, YY, size, size, - TextGet("KDVisualOpt_PreviewRestraints_" + preview), KDWardrobePreviewRestraints == preview, + TextGet("KDVisualOpt_PreviewRestraints_" + preview), KDWardrobePreviewRestraints == preview, false, KDBaseWhite, undefined, { maxWidth: 350, fontSize: Math.max(24, size/2), @@ -1592,9 +1591,9 @@ function KDDrawWardrobe(_screen: string, Character: Character) { pp[p._1] = p._2; } } - + KDDrawCustomPalettes(pp, pid, - 750, 200, KDPaletteWidth, 72, + 750, 200, KDPaletteWidth, 72, selectedPalette, (pal) => { C.Palette = pal; if (!C.metadata) { @@ -1627,19 +1626,19 @@ function KDDrawWardrobe(_screen: string, Character: Character) { if (palettetemp) { palette[palettelayer] = palettetemp[palettelayer]; } - + } } - + if (palette && selectedPalette) { - + let top = 100; let X = 1625; - let res = KDDrawColorPicker("Default", palettelayer, palette[palettelayer], palette, + let res = KDDrawColorPicker("Default", palettelayer, palette[palettelayer], palette, top, X, 300); - + DrawButtonKDEx("tab_ColorPickerSimple", (_b) => { KDToggles.SimpleColorPicker = true; KDPropsSlider = false; @@ -1652,38 +1651,38 @@ function KDDrawWardrobe(_screen: string, Character: Character) { return true; }, true, X - 200 + 250, res.YY + 20, 240, 30, TextGet("KDColorPickerAdvanced"), KDBaseWhite, undefined, undefined, undefined, KDPropsSlider || KDToggles.SimpleColorPicker, KDButtonColor); - + DrawButtonKDEx("KDResetAllLayers", (_bdata) => { if (C.metadata?.customColors[selectedPalette]) { delete C.metadata.customColors[selectedPalette]; delete KDCurrentCharacterPalettes[selectedPalette]; KDRefreshCharacter.set(C, true); KDDressWardrobeChar(C); - + } return true; - }, true, X + 300/2 + 10, top - 40, 300/2 - 10, 30, + }, true, X + 300/2 + 10, top - 40, 300/2 - 10, 30, TextGet("KDResetAllLayers"), KDTextWhite); DrawBoxKD(1625 - 225, top - 50, 350 + 200, res.YY - top + 125, KDButtonColor, false, 0.5, -10); - + let spacing = 64; let yyColor = top - 50 + (res.YY - top + 100) / 2 - spacing/2*GenericPaletteLayers.length; let xxColor = 1625 - 215; - + let ii = 0; for (let col of GenericPaletteLayers) { let sprite = GenericPaletteLayerSprites[col] || GenericPaletteLayerSprites.DarkNeutral; - DrawButtonKDEx("paletteLayer" + col, + DrawButtonKDEx("paletteLayer" + col, () => { KDSelectedPaletteLayer = col; return true; - }, - true, xxColor, yyColor, 200, 60, TextGet("KDPaletteLayer_" + col), - KDTextWhite, sprite, undefined, undefined, col != KDSelectedPaletteLayer, + }, + true, xxColor, yyColor, 200, 60, TextGet("KDPaletteLayer_" + col), + KDTextWhite, sprite, undefined, undefined, col != KDSelectedPaletteLayer, KDButtonColor, undefined, true, { // @ts-ignore filters: KDPIXIPaletteFilters.get(pid + selectedPalette) ? KDPIXIPaletteFilters.get(pid + selectedPalette)[ii++] : undefined, @@ -1691,20 +1690,19 @@ function KDDrawWardrobe(_screen: string, Character: Character) { yyColor += spacing; } - + if (CommonTime() > lastFastPaletteUpdate + 200) { lastFastPaletteUpdate = CommonTime(); if (KDPIXIPaletteFilters.has(pid + selectedPalette)) KDPIXIPaletteFilters.delete(pid + selectedPalette) - if (C.metadata?.customColors) for (let palette in C.metadata.customColors) { // finally update KDCurrentCharacterPalettes[palette] = C.metadata.customColors[palette]; } } - + if (res.updated) { if (KDPIXIPaletteFilters.has(pid + selectedPalette)) KDPIXIPaletteFilters.delete(pid + selectedPalette) @@ -1713,7 +1711,7 @@ function KDDrawWardrobe(_screen: string, Character: Character) { } if (palette && Object.values(palette).length > 0) { if (!C.metadata.customColors) C.metadata.customColors = {}; - if (!C.metadata.customColors[selectedPalette]) + if (!C.metadata.customColors[selectedPalette]) C.metadata.customColors[selectedPalette] = KDCurrentCharacterPalettes[selectedPalette] || {}; C.metadata.customColors[selectedPalette][palettelayer] = palette[palettelayer]; } else { @@ -1734,8 +1732,8 @@ function KDDrawWardrobe(_screen: string, Character: Character) { delete KDCurrentCharacterPalettes[selectedPalette]; } } - - + + } else { KDSelectedPaletteLayer = "Highlight"; KDCurrentCharacterPalettes = null; @@ -1826,53 +1824,56 @@ function KDDrawWardrobe(_screen: string, Character: Character) { // Return anon function anonymously let clickButton = (index: number) => { return (_bdata: any) => { - KDSelectedModel = null; - if (C == KinkyDungeonPlayer) { - KDOutfitStore[KDCurrentOutfit] = LZString.compressToBase64(CharacterAppearanceStringify(C || KinkyDungeonPlayer, - KDGetCharMetadata(C || KinkyDungeonPlayer) - )); - KDOutfitOriginalStore[KDCurrentOutfit] = KDOriginalValue; - ElementValue("KDOutfitName", ""); - } - KDCurrentOutfit = index; - if (C == KinkyDungeonPlayer) - localStorage.setItem("kdcurrentoutfit", KDCurrentOutfit + ""); + (async () => { + KDSelectedModel = null; + if (C == KinkyDungeonPlayer) { + KDOutfitStore[KDCurrentOutfit] = + await KinkyDungeonCompressSave (CharacterAppearanceStringify (C || KinkyDungeonPlayer, + KDGetCharMetadata(C || KinkyDungeonPlayer)), + SaveType.Outfit); + KDOutfitOriginalStore[KDCurrentOutfit] = KDOriginalValue; + ElementValue("KDOutfitName", ""); + } + KDCurrentOutfit = index; + if (C == KinkyDungeonPlayer) + localStorage.setItem("kdcurrentoutfit", KDCurrentOutfit + ""); - let NewOutfit = KDOutfitStore[KDCurrentOutfit] || localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); + let NewOutfit = KDOutfitStore[KDCurrentOutfit] || localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); - if (NewOutfit) { - KDOriginalValue = KDOutfitOriginalStore[KDCurrentOutfit] || ""; - KinkyDungeonSetDress("None", "None", C, true); - KDRefreshCharacter.set(C, true); - KinkyDungeonDressPlayer(C, true, false, undefined, undefined, undefined, C.metadata?.palette || C.Palette, undefined, - true); - let newOut = DecompressB64(NewOutfit); - CharacterAppearanceRestore(C, newOut, C != KinkyDungeonPlayer, false); - let newParsed = JSON.parse(newOut); - C.metadata = newParsed?.metadata || DefaultOutfitMetadata(); - if (newParsed?.metadata) { - C.Palette = newParsed.metadata.palette; - } else C.Palette = ""; - CharacterRefresh(C); - KDInitProtectedGroups(C); - KDRefreshCharacter.set(C, true); - KinkyDungeonDressPlayer(C, true, undefined, undefined, undefined, - undefined, C.metadata?.palette || C.Palette, true - ); - } else if (C == KinkyDungeonPlayer) { - KDGetDressList().Default = KinkyDungeonDefaultDefaultDress; - CharacterAppearanceRestore(KinkyDungeonPlayer, - CharacterAppearanceStringify(DefaultPlayer, - KDGetCharMetadata(DefaultPlayer) - ), false, true); - CharacterReleaseTotal(KinkyDungeonPlayer); - KinkyDungeonSetDress("Default", "Default", C, true); - C.Palette = ""; - C.metadata = DefaultOutfitMetadata(); - KDRefreshCharacter.set(C, true); - KinkyDungeonDressPlayer(); - KDInitProtectedGroups(KinkyDungeonPlayer); - } + if (NewOutfit) { + KDOriginalValue = KDOutfitOriginalStore[KDCurrentOutfit] || ""; + KinkyDungeonSetDress("None", "None", C, true); + KDRefreshCharacter.set(C, true); + KinkyDungeonDressPlayer(C, true, false, undefined, undefined, undefined, C.metadata?.palette || C.Palette, undefined, + true); + let newOut = await KinkyDungeonDecompressSave (NewOutfit, SaveType.Outfit); + await CharacterAppearanceRestore(C, newOut, C != KinkyDungeonPlayer, false); + let newParsed = JSON.parse(newOut); + C.metadata = newParsed?.metadata || DefaultOutfitMetadata(); + if (newParsed?.metadata) { + C.Palette = newParsed.metadata.palette; + } else C.Palette = ""; + CharacterRefresh(C); + KDInitProtectedGroups(C); + KDRefreshCharacter.set(C, true); + KinkyDungeonDressPlayer(C, true, undefined, undefined, undefined, + undefined, C.metadata?.palette || C.Palette, true + ); + } else if (C == KinkyDungeonPlayer) { + KDGetDressList().Default = KinkyDungeonDefaultDefaultDress; + await CharacterAppearanceRestore(KinkyDungeonPlayer, + CharacterAppearanceStringify(DefaultPlayer, + KDGetCharMetadata(DefaultPlayer) + ), false, true); + CharacterReleaseTotal(KinkyDungeonPlayer); + KinkyDungeonSetDress("Default", "Default", C, true); + C.Palette = ""; + C.metadata = DefaultOutfitMetadata(); + KDRefreshCharacter.set(C, true); + KinkyDungeonDressPlayer(); + KDInitProtectedGroups(KinkyDungeonPlayer); + } + })(); return true; }; }; @@ -1940,7 +1941,7 @@ function KDDrawWardrobe(_screen: string, Character: Character) { KinkyDungeonSetDress("Bikini", "Bikini", C, true); } else KinkyDungeonSetDress("None", "None", C, true); - KinkyDungeonDressPlayer(C, true, false, undefined, undefined, + KinkyDungeonDressPlayer(C, true, false, undefined, undefined, undefined, C.metadata?.palette || C.Palette, undefined, true); if (C == KinkyDungeonPlayer) { KDInitProtectedGroups(C); @@ -2031,20 +2032,22 @@ function KDDrawWardrobe(_screen: string, Character: Character) { if (KDConfirmType == "reset" && KinkyDungeonReplaceConfirm > 0) { KDSelectedModel = null; if (C == KinkyDungeonPlayer) { - KDChangeWardrobe(C); - KDGetDressList().Default = KinkyDungeonDefaultDefaultDress; - CharacterAppearanceRestore(KinkyDungeonPlayer, - CharacterAppearanceStringify(DefaultPlayer, - KDGetCharMetadata(KinkyDungeonPlayer) - ), false, false - ); - CharacterReleaseTotal(KinkyDungeonPlayer); - KinkyDungeonSetDress("Default", "Default", C, true); - KinkyDungeonDressPlayer(); - KDInitProtectedGroups(KinkyDungeonPlayer); - UpdateModels(KinkyDungeonPlayer); - KinkyDungeonConfigAppearance = true; - KinkyDungeonReplaceConfirm = 0; + (async () => { + KDChangeWardrobe(C); + KDGetDressList().Default = KinkyDungeonDefaultDefaultDress; + await CharacterAppearanceRestore(KinkyDungeonPlayer, + CharacterAppearanceStringify(DefaultPlayer, + KDGetCharMetadata(KinkyDungeonPlayer) + ), false, false + ); + CharacterReleaseTotal(KinkyDungeonPlayer); + KinkyDungeonSetDress("Default", "Default", C, true); + KinkyDungeonDressPlayer(); + KDInitProtectedGroups(KinkyDungeonPlayer); + UpdateModels(KinkyDungeonPlayer); + KinkyDungeonConfigAppearance = true; + KinkyDungeonReplaceConfirm = 0; + })(); } else if (C == KDSpeakerNPC) { let value = KDNPCStyle.get(KDSpeakerNPC); if (!value) return false; @@ -2245,42 +2248,45 @@ function KDSaveCodeOutfit(C: Character, clothesOnly: boolean = false): void { if (!C) C = KinkyDungeonPlayer; // Save outfit KDChangeWardrobe(C); - let decompressed = DecompressB64(ElementValue("saveInputField")); - if (decompressed) { + (async () => { + let decompressed = await KinkyDungeonDecompressSave (ElementValue ("saveInputField"), SaveType.Outfit); + if (decompressed) { - // Strips first - KDChangeWardrobe(C); - CharacterReleaseTotal(C); - CharacterNaked(C); - KDRefreshCharacter.set(C, true); - KinkyDungeonSetDress("None", "None", C, true); - KinkyDungeonDressPlayer(C, true); - KDInitProtectedGroups(C); - KinkyDungeonConfigAppearance = true; - KinkyDungeonReplaceConfirm = 0; - - // Then decompresses - CharacterAppearanceRestore(C, decompressed, clothesOnly, !clothesOnly); - CharacterRefresh(C); - KDInitProtectedGroups(C); - } + // Strips first + KDChangeWardrobe(C); + CharacterReleaseTotal(C); + CharacterNaked(C); + KDRefreshCharacter.set(C, true); + KinkyDungeonSetDress("None", "None", C, true); + KinkyDungeonDressPlayer(C, true); + KDInitProtectedGroups(C); + KinkyDungeonConfigAppearance = true; + KinkyDungeonReplaceConfirm = 0; - KDRefreshCharacter.set(C, true); - KinkyDungeonDressPlayer(C, true, undefined, undefined, undefined, - undefined, C.metadata?.palette || C.Palette, true); + // Then decompresses + await CharacterAppearanceRestore(C, decompressed, clothesOnly, !clothesOnly); + CharacterRefresh(C); + KDInitProtectedGroups(C); + } + KDRefreshCharacter.set(C, true); + KinkyDungeonDressPlayer(C, true, undefined, undefined, undefined, + undefined, C.metadata?.palette || C.Palette, true); - //KinkyDungeonNewDress = true; + //KinkyDungeonNewDress = true; + })(); } function KDRestoreOutfit() { - // Restore the original outfit - if (KDOriginalValue) { - CharacterAppearanceRestore(KinkyDungeonPlayer, DecompressB64(KDOriginalValue) || KDOriginalValue, false, true); - CharacterRefresh(KinkyDungeonPlayer); - KDInitProtectedGroups(KinkyDungeonPlayer); - KinkyDungeonDressPlayer(); - } + (async () => { + // Restore the original outfit + if (KDOriginalValue) { + await CharacterAppearanceRestore (KinkyDungeonPlayer, await KinkyDungeonDecompressSave (KDOriginalValue, SaveType.Outfit) || KDOriginalValue, false, true); + CharacterRefresh(KinkyDungeonPlayer); + KDInitProtectedGroups(KinkyDungeonPlayer); + KinkyDungeonDressPlayer(); + } + })(); } function KDSaveOutfitInfo() { @@ -2410,14 +2416,14 @@ function KDLoadOutfitDirect(files: File[], Char: Character) { KDSaveName = f.name; try { const reader = new FileReader(); - reader.addEventListener('load', (event) => { + reader.addEventListener('load', async (event) => { str = event.target.result.toString(); - let decompressed = DecompressB64(str); + let decompressed = await KinkyDungeonDecompressSave (str, SaveType.Outfit); if (decompressed) { let origAppearance = Char.Appearance; try { - CharacterAppearanceRestore(Char, decompressed, Char == KDSpeakerNPC, Char != KDSpeakerNPC); + await CharacterAppearanceRestore(Char, decompressed, Char == KDSpeakerNPC, Char != KDSpeakerNPC); let newParsed = JSON.parse(decompressed); if (newParsed && newParsed.metadata) { Char.metadata = newParsed.metadata; @@ -2426,7 +2432,7 @@ function KDLoadOutfitDirect(files: File[], Char: Character) { CharacterRefresh(Char); KDOldValue = str; KDInitProtectedGroups(Char); - KinkyDungeonDressPlayer(Char, true, undefined, undefined, + KinkyDungeonDressPlayer(Char, true, undefined, undefined, undefined, undefined, Char.metadata?.palette || Char.Palette, true); if (Char.Appearance.length == 0) @@ -3031,9 +3037,9 @@ function KDDrawColorPicker(id: string, currentLayerName: string, targetFilter: L KDRefreshProps = true; lastGlobalRefresh = CommonTime() - GlobalRefreshInterval + 10; } - + return true; - }, true, X + width/2 + 10, YY, width/2 - 10, 30, + }, true, X + width/2 + 10, YY, width/2 - 10, 30, TextGet("KDResetLayer"), KDBaseWhite) && MouseClicked) res.updated = true; @@ -3117,26 +3123,26 @@ function KDDrawColorPicker(id: string, currentLayerName: string, targetFilter: L let selectedLayer = factionFilterDef?.color || "None"; for (let key of ["None", ...GenericPaletteLayers]) { //DrawTextFitKD(TextGet("KDPaletteLayer_" + key), X + width/2, YY, width, KDBaseWhite, KDBaseBlack, 20); - + let sprite = GenericPaletteLayerSprites[key] || GenericPaletteLayerSprites.DarkNeutral; - - DrawButtonKDEx("paletteLayer" + key, + + DrawButtonKDEx("paletteLayer" + key, () => { lastFilterUpdate = CommonTime(); if (callback_palette) callback_palette(key, factionFilterDef?.override, factionFilterDef?.desaturate != undefined ? true : undefined); - + lastGlobalRefresh = CommonTime() - GlobalRefreshInterval + 10; return true; - }, - true, X, YY - 15, width, 60, TextGet("KDPaletteLayer_" + key), - KDTextWhite, sprite, undefined, undefined, key != selectedLayer, + }, + true, X, YY - 15, width, 60, TextGet("KDPaletteLayer_" + key), + KDTextWhite, sprite, undefined, undefined, key != selectedLayer, KDButtonColor, undefined, true, { // @ts-ignore filters: (ii >= 0 && KDPIXIPaletteFilters.get(pid + palette)) ? KDPIXIPaletteFilters.get(pid + palette)[ii++] : undefined, }); - + YY += 61; } YY += 15; @@ -3144,11 +3150,11 @@ function KDDrawColorPicker(id: string, currentLayerName: string, targetFilter: L DrawCheckboxKDEx("overridePaletteLayer", () => { lastFilterUpdate = CommonTime(); if (callback_palette) callback_palette(selectedLayer, !factionFilterDef?.override, factionFilterDef?.desaturate != undefined ? true : undefined); - + lastGlobalRefresh = CommonTime() - GlobalRefreshInterval + 10; return true; - }, selectedLayer != "None", - X, YY - 15, 50, 50, TextGet("KDWardrobeOverridePaletteLayer"), + }, selectedLayer != "None", + X, YY - 15, 50, 50, TextGet("KDWardrobeOverridePaletteLayer"), factionFilterDef?.override, selectedLayer == "None", KDTextWhite, undefined, { fontSize: 18, maxWidth: 200, @@ -3158,17 +3164,17 @@ function KDDrawColorPicker(id: string, currentLayerName: string, targetFilter: L DrawCheckboxKDEx("desaturatePaletteLayer", () => { lastFilterUpdate = CommonTime(); if (callback_palette) callback_palette(selectedLayer, factionFilterDef?.override, factionFilterDef?.desaturate != undefined ? undefined : true); - + lastGlobalRefresh = CommonTime() - GlobalRefreshInterval + 10; return true; - }, selectedLayer != "None", - X, YY - 15, 50, 50, TextGet("KDWardrobeDesaturatePaletteLayer"), + }, selectedLayer != "None", + X, YY - 15, 50, 50, TextGet("KDWardrobeDesaturatePaletteLayer"), !factionFilterDef?.desaturate, selectedLayer == "None", KDTextWhite, undefined, { fontSize: 18, maxWidth: 200, }); - + YY += 20; } @@ -3211,7 +3217,7 @@ function KDDrawColorPicker(id: string, currentLayerName: string, targetFilter: L bb}`); ElementValue("KDCopyFilter", JSON.stringify(targetFilters[currentLayerName])); } - + lastGlobalRefresh = CommonTime() - GlobalRefreshInterval + 10; } @@ -3391,7 +3397,7 @@ function KDDrawColorPicker(id: string, currentLayerName: string, targetFilter: L TF.Element.oninput = (_event: any) => { let value = ElementValue("KDSelectedColor"); let RegExp = /^#[0-9A-Fa-f]{6}$/i; - + if (RegExp.test(value)) { let hex = KDhexToRGB(value); if (hex) { @@ -3417,15 +3423,15 @@ function KDDrawColorPicker(id: string, currentLayerName: string, targetFilter: L }; } } else if (!palette) { - DrawTextFitKD(TextGet("KDPaletteColorPickerInfo"),X + width/2, YY - 10, + DrawTextFitKD(TextGet("KDPaletteColorPickerInfo"),X + width/2, YY - 10, 300, KDBaseWhite, KDTextGray0, 16, "center"); - + } else { - DrawTextFitKD(TextGet("KDPaletteColorPickerInfo2"),X + width/2, YY - 10, + DrawTextFitKD(TextGet("KDPaletteColorPickerInfo2"),X + width/2, YY - 10, 300, KDBaseWhite, KDTextGray0, 16, "center"); - + } - + res.YY = YY; @@ -3443,8 +3449,8 @@ function KDDressWardrobeChar(C: Character, forcedress?: boolean) { // show with preview restraints let selectedPalette = C.metadata?.palette || C.Palette || ""; let prevR = KDGetPreviewRestraints(KDWardrobePreviewRestraints); - KinkyDungeonDressPlayer(C, false, false, - prevR, undefined, + KinkyDungeonDressPlayer(C, false, false, + prevR, undefined, KDGetNPCRestraintTags(prevR, undefined, undefined, false, false), selectedPalette, true); return; @@ -3456,9 +3462,9 @@ function KDDressWardrobeChar(C: Character, forcedress?: boolean) { } else if (KDNPCChar_ID.get(KDSpeakerNPC)) { // show with restraints //let selectedPalette = C.metadata?.palette || C.Palette; - KinkyDungeonDressPlayer(KDSpeakerNPC, false, false, + KinkyDungeonDressPlayer(KDSpeakerNPC, false, false, KDGameData.NPCRestraints ? KDGameData.NPCRestraints[KDNPCChar_ID.get(KDSpeakerNPC) + ''] : undefined, - undefined, + undefined, KDGameData.NPCRestraints ? KDGetNPCRestraintTags(KDGameData.NPCRestraints[KDNPCChar_ID.get(KDSpeakerNPC) + ''], undefined, undefined, false, false) : undefined, undefined, !forcedress); diff --git a/Scripts/Appearance.ts b/Scripts/Appearance.ts index 6bf68c3f9..d16ca5db7 100644 --- a/Scripts/Appearance.ts +++ b/Scripts/Appearance.ts @@ -76,8 +76,9 @@ function AppearanceItemStringify(Item: Item[]): string { * @param backup - The serialised appearance to restore * @param clothesOnly - The serialised appearance to restore */ -function CharacterAppearanceRestore(C: Character, backup: string, clothesOnly: boolean = false, noProtected: boolean = false): void { - let parsed = JSON.parse(LZString.decompressFromBase64(backup) || backup); +async function CharacterAppearanceRestore(C: Character, backup: string, clothesOnly: boolean = false, noProtected: boolean = false): Promise +{ + let parsed = JSON.parse(await KinkyDungeonDecompressSave (backup, SaveType.Outfit) || backup); let newAppearance = AppearanceItemParse(parsed?.metadata ? parsed.appearance : backup); C.metadata = parsed.metadata; if (!clothesOnly) { @@ -149,4 +150,4 @@ function AppearanceCleanup(C: Character) { A -= 1; } } -} \ No newline at end of file +} From 90aee421b109b54468db0905b4b1c5465dcc62d4 Mon Sep 17 00:00:00 2001 From: "Leo L. Schwab" Date: Tue, 6 May 2025 00:29:37 -0700 Subject: [PATCH 3/4] Update all remaining compress/decompress call sites. Locate and update all remaining calls to `compressToBase64()`, `decompressFromBase64()`, and `DecompressB64()`. Also supply SaveType arguments at all `KinkyDungeon{,De}CompressSave()` call sites. --- Game/src/base/KinkyDungeon.ts | 24 ++-- Game/src/base/KinkyDungeonErrors.ts | 12 +- Game/src/base/game/KinkyDungeonHUD.ts | 13 +- Game/src/collection/KinkyDungeonCollection.ts | 121 +++++++++--------- Game/src/map/KDStairActions.ts | 19 +-- Game/src/map/KinkyDungeonAlt.ts | 2 +- Game/src/map/KinkyDungeonTiles.ts | 4 +- Game/src/player/KinkyDungeonDress.ts | 68 +++++----- Game/src/player/wardrobe/KDWardrobe.ts | 42 +++--- 9 files changed, 160 insertions(+), 145 deletions(-) diff --git a/Game/src/base/KinkyDungeon.ts b/Game/src/base/KinkyDungeon.ts index 2a93a37e8..97f9c5dda 100644 --- a/Game/src/base/KinkyDungeon.ts +++ b/Game/src/base/KinkyDungeon.ts @@ -1486,7 +1486,7 @@ function KinkyDungeonRun() { let ss = KDSaveSlot; KDSendMusicToast(TextGet("KDSaving")); let sd = JSON.stringify(KDSaveQueue.splice(0, 1)[0]); - KinkyDungeonCompressSave(sd).then( + KinkyDungeonCompressSave(sd, SaveType.Game).then( (data) => { try { localStorage.setItem('KinkyDungeonSave', data); @@ -4576,16 +4576,18 @@ function KinkyDungeonDBSave(saveslot: number, gamecode?: string) { console.error("Save slot is not defined"); return; // This is an invalid call or the save slot has not been set. } - if (gamecode == undefined) { - // We are going to use the current game state as a save code. - save = LZString.compressToBase64(JSON.stringify(KinkyDungeonGenerateSaveData())); - } - else { - save = gamecode; - } - - // Get the savegame database - KinkyDungeonDBOpen().then((db) => { + (async () => { + if (gamecode == undefined) { + // We are going to use the current game state as a save code. + save = await KinkyDungeonCompressSave (JSON.stringify (KinkyDungeonGenerateSaveData()), SaveType.Game); + } + else { + save = gamecode; + } + })().then (() => { + // Get the savegame database + return KinkyDungeonDBOpen(); + }).then ((db) => { // Create a transaction const transaction = db.transaction(KDGameSaveDBStoreName, "readwrite"); const store = transaction.objectStore(KDGameSaveDBStoreName); diff --git a/Game/src/base/KinkyDungeonErrors.ts b/Game/src/base/KinkyDungeonErrors.ts index bfc056569..05ae5c069 100644 --- a/Game/src/base/KinkyDungeonErrors.ts +++ b/Game/src/base/KinkyDungeonErrors.ts @@ -18,8 +18,8 @@ function KinkyDungeonTeardownCrashHandler(): void { * Error event handler for uncaught errors * @param event - The error event */ -function KinkyDungeonOnUncaughtError(event: ErrorEvent): void { - const report = KinkyDungeonGenerateErrorReport(event); +async function KinkyDungeonOnUncaughtError(event: ErrorEvent): Promise { + const report = await KinkyDungeonGenerateErrorReport(event); KinkyDungeonShowCrashReportModal(report); } @@ -28,13 +28,13 @@ function KinkyDungeonOnUncaughtError(event: ErrorEvent): void { * @param event - The error event * @returns The report */ -function KinkyDungeonGenerateErrorReport(event: ErrorEvent): string { +async function KinkyDungeonGenerateErrorReport(event: ErrorEvent): Promise { return [ KinkyDungeonCrashReportErrorDetails(event), KinkyDungeonCrashReportStateData(), KinkyDungeonCrashReportDiagnostics(), KinkyDungeonCrashReportDeviceDetails(), - KinkyDungeonCrashReportSaveData(), + await KinkyDungeonCrashReportSaveData(), ].join("\n\n"); } @@ -107,11 +107,11 @@ function KinkyDungeonCrashReportErrorDetails(event: ErrorEvent): string { * Generates a report string containing the current save state of the game * @returns The report */ -function KinkyDungeonCrashReportSaveData(): string { +async function KinkyDungeonCrashReportSaveData(): Promise { let saveData = localStorage.getItem("KinkyDungeonSave"); if (!saveData) { try { - saveData = LZString.compressToBase64(JSON.stringify(KinkyDungeonGenerateSaveData())); + saveData = await KinkyDungeonCompressSave (JSON.stringify (KinkyDungeonGenerateSaveData()), SaveType.Game); } catch (error) { saveData = "Could not locate or generate save data"; } diff --git a/Game/src/base/game/KinkyDungeonHUD.ts b/Game/src/base/game/KinkyDungeonHUD.ts index 6500b6d64..4d213dc0c 100644 --- a/Game/src/base/game/KinkyDungeonHUD.ts +++ b/Game/src/base/game/KinkyDungeonHUD.ts @@ -1941,11 +1941,12 @@ function KinkyDungeonHandleHUD() { return true; } if (MouseIn(1500, 320, 300, 64)) { - let saveData = LZString.compressToBase64(JSON.stringify(KinkyDungeonSaveGame(true))); - KinkyDungeonState = "Save"; - ElementCreateTextArea("saveDataField"); - ElementValue("saveDataField", saveData); - + (async () => { + let saveData = await KinkyDungeonCompressSave (JSON.stringify (KinkyDungeonSaveGame(true)), SaveType.Game); + KinkyDungeonState = "Save"; + ElementCreateTextArea("saveDataField"); + ElementValue("saveDataField", saveData); + })(); return true; } @@ -3121,7 +3122,7 @@ function KDDrawBuffIcons(minXX: number, minYY: number, statsDraw: Record minXX) || (KDMinBuffX && XX > KDMinBuffX)) && + if (((!KDMinBuffX && XX > minXX) || (KDMinBuffX && XX > KDMinBuffX)) && (KDStatsSkipLine[currCategory] || KDStatsSkipLineBefore[stat.category]) && currCategory != stat.category) { if (KDToggleShowAllBuffs) { diff --git a/Game/src/collection/KinkyDungeonCollection.ts b/Game/src/collection/KinkyDungeonCollection.ts index 9bb233856..05055d28d 100644 --- a/Game/src/collection/KinkyDungeonCollection.ts +++ b/Game/src/collection/KinkyDungeonCollection.ts @@ -486,71 +486,72 @@ function KDDrawSelectedCollectionMember(value: KDCollectionEntry, x: number, y: DrawTextFitKD(TextGet("KDZoomNPC"), x + 220, y + 750, 500, KDBaseWhite, KDTextGray0); } if (KDGameData.Collection[value.id + ""] && DrawButtonKDEx("dressNPC", () => { - if (KDSoundEnabled()) - AudioPlayInstantSoundKD(KinkyDungeonRootDirectory + "Audio/" + "LightJingle" + ".ogg"); - //KDSpeakerNPC = null; - KinkyDungeonState = "Wardrobe"; - KDCanRevertFlag = value.customOutfit != undefined; - ForceRefreshModels(KDSpeakerNPC); - KDOriginalValue = ""; - CharacterReleaseTotal(KDSpeakerNPC); - KDWardrobeCallback = () => { - UpdateModels(KDSpeakerNPC); - KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; - KDRefreshCharacter.set(KDSpeakerNPC, true); - KDDressWardrobeChar(KDSpeakerNPC, false); - - let value2 = value; - //if (KDOriginalValue) { - value2.customOutfit = LZString.compressToBase64(AppearanceItemStringify(KDSpeakerNPC.Appearance)); - value2.Palette = KDSpeakerNPC.Palette; - value2.metadata = KDSpeakerNPC.metadata; - - KDRefreshCharacter.set(KDSpeakerNPC, true); - KinkyDungeonCheckClothesLoss = true; - KinkyDungeonDressPlayer(KDSpeakerNPC, false, false, KDGameData.NPCRestraints ? KDGameData.NPCRestraints[value.id + ''] : undefined); - - //} - }; - if (value.customOutfit) { - let outfit = value.customOutfit; - KDWardrobeRevertCallback = async () => { + (async () => { + if (KDSoundEnabled()) + AudioPlayInstantSoundKD(KinkyDungeonRootDirectory + "Audio/" + "LightJingle" + ".ogg"); + //KDSpeakerNPC = null; + KinkyDungeonState = "Wardrobe"; + KDCanRevertFlag = value.customOutfit != undefined; + ForceRefreshModels(KDSpeakerNPC); + KDOriginalValue = ""; + CharacterReleaseTotal(KDSpeakerNPC); + KDWardrobeCallback = async () => { + UpdateModels(KDSpeakerNPC); KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; - if (outfit) - await CharacterAppearanceRestore(KDSpeakerNPC, DecompressB64(outfit),false, true); - CharacterRefresh(KDSpeakerNPC); - KDInitProtectedGroups(KDSpeakerNPC); KDRefreshCharacter.set(KDSpeakerNPC, true); - KinkyDungeonDressPlayer(KDSpeakerNPC, true); - }; - KDWardrobeResetCallback = () => { - KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; - delete value.customOutfit; - }; - } else { - KDWardrobeRevertCallback = () => { - KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; - delete value.customOutfit; + KDDressWardrobeChar(KDSpeakerNPC, false); + + /* FIXME: After setting up `value2`, nothing is done with it? */ + let value2 = value; + //if (KDOriginalValue) { + value2.customOutfit = await KinkyDungeonCompressSave (AppearanceItemStringify(KDSpeakerNPC.Appearance), SaveType.Outfit); + value2.Palette = KDSpeakerNPC.Palette; + value2.metadata = KDSpeakerNPC.metadata; + KDRefreshCharacter.set(KDSpeakerNPC, true); - KinkyDungeonDressPlayer(KDSpeakerNPC, true); + KinkyDungeonCheckClothesLoss = true; + KinkyDungeonDressPlayer(KDSpeakerNPC, false, false, KDGameData.NPCRestraints ? KDGameData.NPCRestraints[value.id + ''] : undefined); + //} }; - KDWardrobeResetCallback = null; - } + if (value.customOutfit) { + let outfit = value.customOutfit; + KDWardrobeRevertCallback = async () => { + KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; + if (outfit) + await CharacterAppearanceRestore(KDSpeakerNPC, await KinkyDungeonDecompressSave (outfit, SaveType.Outfit),false, true); + CharacterRefresh(KDSpeakerNPC); + KDInitProtectedGroups(KDSpeakerNPC); + KDRefreshCharacter.set(KDSpeakerNPC, true); + KinkyDungeonDressPlayer(KDSpeakerNPC, true); + }; + KDWardrobeResetCallback = () => { + KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; + delete value.customOutfit; + }; + } else { + KDWardrobeRevertCallback = () => { + KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; + delete value.customOutfit; + KDRefreshCharacter.set(KDSpeakerNPC, true); + KinkyDungeonDressPlayer(KDSpeakerNPC, true); + }; + KDWardrobeResetCallback = null; + } - KDPlayerSetPose = false; - KDInitCurrentPose(true,KDSpeakerNPC); - KinkyDungeonInitializeDresses(); - KDUpdateModelList(); - KDRefreshOutfitInfo(); - let itt = localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); - let orig = itt ? - JSON.parse(LZString.decompressFromBase64(itt)).appearance - || itt : ""; - let current = LZString.compressToBase64(AppearanceItemStringify(KinkyDungeonPlayer.Appearance)); - if (orig != current) KDOriginalValue = orig; - ForceRefreshModelsAsync(KDSpeakerNPC); - KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; - KDDressWardrobeChar(KDSpeakerNPC, true); + KDPlayerSetPose = false; + KDInitCurrentPose(true,KDSpeakerNPC); + KinkyDungeonInitializeDresses(); + KDUpdateModelList(); + KDRefreshOutfitInfo(); + let itt = localStorage.getItem("kinkydungeonappearance" + KDCurrentOutfit); + let orig = itt ? JSON.parse (await KinkyDungeonDecompressSave (itt, SaveType.Outfit)).appearance || itt + : ""; + let current = await KinkyDungeonCompressSave (AppearanceItemStringify (KinkyDungeonPlayer.Appearance), SaveType.Outfit); + if (orig != current) KDOriginalValue = orig; + ForceRefreshModelsAsync(KDSpeakerNPC); + KDShowCharacterPalette = false; KDWardrobePreviewRestraints = ""; + KDDressWardrobeChar(KDSpeakerNPC, true); + })(); return true; }, true, x - 90, y + 90, 80, 80, "", KDBaseWhite, KinkyDungeonRootDirectory + "UI/Dress.png", undefined, undefined, diff --git a/Game/src/map/KDStairActions.ts b/Game/src/map/KDStairActions.ts index 4175e8e31..6cccd6293 100644 --- a/Game/src/map/KDStairActions.ts +++ b/Game/src/map/KDStairActions.ts @@ -133,7 +133,7 @@ function KDGoThruTile(x: number, y: number, suppressCheckPoint: boolean, force: if (KDGameData.PriorJailbreaks > 0) KDGameData.PriorJailbreaksDecay = (KDGameData.PriorJailbreaksDecay + 1) || 1; if (MiniGameKinkyDungeonLevel > 1) { - + KDAdvanceOneFloor(); } @@ -167,7 +167,7 @@ function KDGoThruTile(x: number, y: number, suppressCheckPoint: boolean, force: let movedUp = MiniGameKinkyDungeonLevel > KDGameData.HighestLevelCurrent; KDGameData.HighestLevelCurrent = Math.max(KDGameData.HighestLevelCurrent || 1, MiniGameKinkyDungeonLevel); KDGameData.HighestLevel = Math.max(KDGameData.HighestLevel || 1, MiniGameKinkyDungeonLevel); - + //if (KinkyDungeonTilesGet(KinkyDungeonPlayerEntity.x + "," + KinkyDungeonPlayerEntity.y)) { let MapMod = data.mapMod; @@ -264,15 +264,18 @@ function KDGoThruTile(x: number, y: number, suppressCheckPoint: boolean, force: function KDPostStairSave() { if (KDGameData.RoomType == "PerkRoom" && MiniGameKinkyDungeonLevel >= 1) { // && Math.floor(MiniGameKinkyDungeonLevel / 3) == MiniGameKinkyDungeonLevel / 3 - if ((!KinkyDungeonStatsChoice.get("saveMode"))) { - let saveData = LZString.compressToBase64(JSON.stringify(KinkyDungeonSaveGame(true))); + if ((!KinkyDungeonStatsChoice.get("saveMode"))) { + (async() => { + let saveData = await KinkyDungeonCompressSave (JSON.stringify (KinkyDungeonSaveGame (true)), SaveType.Game); KinkyDungeonState = "Save"; KDTextArea("saveDataField", 750, 100, 1000, 230); ElementValue("saveDataField", saveData); - } else KinkyDungeonSaveGame(); - } - else KinkyDungeonSaveGame(); - } + })(); + } else KinkyDungeonSaveGame(); + } + else KinkyDungeonSaveGame(); +} + function KinkyDungeonHandleStairs(toTile: string, suppressCheckPoint?: boolean) { if (KinkyDungeonFlags.get("stairslocked")) { KinkyDungeonSendActionMessage(10, TextGet("KDStairsLocked").replace("NMB", "" + KinkyDungeonFlags.get("stairslocked")), KDBaseWhite, 1); diff --git a/Game/src/map/KinkyDungeonAlt.ts b/Game/src/map/KinkyDungeonAlt.ts index 9c450cae4..2b865f5ca 100644 --- a/Game/src/map/KinkyDungeonAlt.ts +++ b/Game/src/map/KinkyDungeonAlt.ts @@ -3284,4 +3284,4 @@ function KDEnumerateShopVisitors() { options.AdaLovelock = 100; } return options; -} \ No newline at end of file +} diff --git a/Game/src/map/KinkyDungeonTiles.ts b/Game/src/map/KinkyDungeonTiles.ts index f98283a83..a38eb99d4 100644 --- a/Game/src/map/KinkyDungeonTiles.ts +++ b/Game/src/map/KinkyDungeonTiles.ts @@ -1028,7 +1028,7 @@ function KDAdvanceOneFloor() { KinkyDungeonChangeRep("Conjure", -1); KinkyDungeonChangeRep("Illusion", -1); } - + if (KDGameData.PrisonerState == "jail") { KDGameData.PrisonerState = ""; // Increase security if escaping jail @@ -1050,4 +1050,4 @@ function KDRoomUnwanderable(altType: string) { return true; } return !altRoom; // bc is main -} \ No newline at end of file +} diff --git a/Game/src/player/KinkyDungeonDress.ts b/Game/src/player/KinkyDungeonDress.ts index 66a7643cc..39267f14f 100644 --- a/Game/src/player/KinkyDungeonDress.ts +++ b/Game/src/player/KinkyDungeonDress.ts @@ -162,26 +162,28 @@ function KinkyDungeonDressPlayer ( let restraintModels = {}; + let dressJob: Promise; let CurrentDress = Character == KinkyDungeonPlayer ? KinkyDungeonCurrentDress : (Character == KDPreviewModel ? KinkyDungeonCurrentDress : (KDCharacterDress.get(Character) || "Bandit")); let DressList = noDressOutfit ? [] : KDGetDressList()[CurrentDress]; if (!noDressOutfit && !forceUseOutfit && KDNPCStyle.get(Character)?.customOutfit) { - DressList = []; - for (let a of JSON.parse(DecompressB64(KDNPCStyle.get(Character)?.customOutfit))) { - if (a.Model && !KDModelIsProtected(a.Model) && !a.Model.Restraint && !a.Model.Cosplay) { - DressList.push({ - Item: a.Model.Name || a.Model, - Group: a.Model.Group || a.Model.Name || a.Model, - Color: KDBaseWhite, - Lost: false, - Filters: a.Model.Filters || a.Filters, - Properties: a.Model.Properties || a.Properties, - factionFilters: a.Model.factionFilters || a.factionFilters, - - },); + dressJob = (async() => { + DressList = []; + for (let a of JSON.parse(await KinkyDungeonDecompressSave (KDNPCStyle.get (Character)?.customOutfit, SaveType.Outfit))) { + if (a.Model && !KDModelIsProtected(a.Model) && !a.Model.Restraint && !a.Model.Cosplay) { + DressList.push({ + Item: a.Model.Name || a.Model, + Group: a.Model.Group || a.Model.Name || a.Model, + Color: KDBaseWhite, + Lost: false, + Filters: a.Model.Filters || a.Filters, + Properties: a.Model.Properties || a.Properties, + factionFilters: a.Model.factionFilters || a.factionFilters, + },); + } } - } + })(); } let forceCustomFaction = !!customFaction; @@ -221,30 +223,34 @@ function KinkyDungeonDressPlayer ( updateExpression: false, Character: Character, extraForceDress: undefined, - Wornitems: (npcRestraints || (KDGetCharacterID(Character) && KDGameData.NPCRestraints[KDGetCharacterID(Character)])) ? - Object.values(npcRestraints || KDGameData.NPCRestraints[KDGetCharacterID(Character)]) - .filter((rest) => {return rest.events}) - .map((rest) => {return rest.id;}) - : [], - NPCRestraintEvents: KDGetCharacterID(Character) ? - KDGameData.NPCRestraints[KDGetCharacterID(Character)] - : undefined, + Wornitems: (npcRestraints || (KDGetCharacterID(Character) && KDGameData.NPCRestraints[KDGetCharacterID(Character)])) + ? Object.values(npcRestraints || KDGameData.NPCRestraints[KDGetCharacterID(Character)]) + .filter((rest) => {return rest.events}) + .map((rest) => {return rest.id;}) + : [], + NPCRestraintEvents: KDGetCharacterID(Character) + ? KDGameData.NPCRestraints[KDGetCharacterID(Character)] + : undefined, }; - if (DressList) { - for (let clothes of DressList) { - if (clothes.Properties && !clothes.Lost) { - for (let p of Object.values(clothes.Properties)) { - if (p.HideRestraintsTags) { - for (let t of p.HideRestraintsTags) { - data.hideShrines[t] = true; + // Ensure the DressList has been populated before dancing on it. + if (dressJob) { + dressJob.then (() => { + if (DressList) { + for (let clothes of DressList) { + if (clothes.Properties && !clothes.Lost) { + for (let p of Object.values(clothes.Properties)) { + if (p.HideRestraintsTags) { + for (let t of p.HideRestraintsTags) { + data.hideShrines[t] = true; + } + } } } } } - } - + }); } diff --git a/Game/src/player/wardrobe/KDWardrobe.ts b/Game/src/player/wardrobe/KDWardrobe.ts index d2f3f3172..c2d1d9898 100644 --- a/Game/src/player/wardrobe/KDWardrobe.ts +++ b/Game/src/player/wardrobe/KDWardrobe.ts @@ -1793,10 +1793,12 @@ function KDDrawWardrobe(_screen: string, Character: Character) { DrawButtonKDEx("BackupOutfit", (_bdata) => { - downloadFile((ElementValue("savename") || KDOutfitInfo[KDCurrentOutfit] || "Outfit") + KDOUTFITBACKUP, - LZString.compressToBase64(CharacterAppearanceStringify(C || KinkyDungeonPlayer, - KDGetCharMetadata(C || KinkyDungeonPlayer) - ))); + (async () => { + downloadFile ((ElementValue("savename") || KDOutfitInfo[KDCurrentOutfit] || "Outfit") + KDOUTFITBACKUP, + await KinkyDungeonCompressSave (CharacterAppearanceStringify (C || KinkyDungeonPlayer, + KDGetCharMetadata (C || KinkyDungeonPlayer)), + SaveType.Outfit)); + })(); return true; }, true, 715, 930, 115, 50, TextGet("KDBackupOutfits"), KDBaseWhite, KinkyDungeonRootDirectory + "UI/Safe.png", "", false, false, @@ -1962,15 +1964,15 @@ function KDDrawWardrobe(_screen: string, Character: Character) { KDBaseWhite, KinkyDungeonRootDirectory + "UI/X.png", undefined, undefined, undefined, undefined, undefined, true); DrawButtonKDEx("LoadFromCode", (_bdata) => { - KinkyDungeonState = "LoadOutfit"; - KDSelectedModel = null; - + (async () => { + KinkyDungeonState = "LoadOutfit"; + KDSelectedModel = null; - CharacterReleaseTotal(C || KinkyDungeonPlayer); - ElementCreateTextArea("saveInputField"); - ElementValue("saveInputField", LZString.compressToBase64( - AppearanceItemStringify((C || KinkyDungeonPlayer).Appearance) - )); + CharacterReleaseTotal(C || KinkyDungeonPlayer); + ElementCreateTextArea("saveInputField"); + ElementValue ("saveInputField", + await KinkyDungeonCompressSave (AppearanceItemStringify((C || KinkyDungeonPlayer).Appearance), SaveType.Outfit)); + })(); return true; }, true,465, 875, 240, 50, TextGet("KinkyDungeonDressPlayerImport"), KDBaseWhite, KinkyDungeonRootDirectory + "UI/Load.png", undefined, undefined, undefined, @@ -2005,22 +2007,22 @@ function KDDrawWardrobe(_screen: string, Character: Character) { KDSaveOutfitInfo(); } KinkyDungeonReplaceConfirm = 0; - localStorage.setItem("kinkydungeonappearance" + KDCurrentOutfit, - LZString.compressToBase64( - CharacterAppearanceStringify(C || KinkyDungeonPlayer, - KDGetCharMetadata(C || KinkyDungeonPlayer) - ) - )); + KinkyDungeonCompressSave (CharacterAppearanceStringify (C || KinkyDungeonPlayer, + KDGetCharMetadata(C || KinkyDungeonPlayer)), + SaveType.Outfit) + .then ((compressed) => { + localStorage.setItem ("kinkydungeonappearance" + KDCurrentOutfit, compressed); + }); + //localStorage.setItem("kdcurrentoutfit", KDCurrentOutfit + ""); KinkyDungeonDressSet(); KDOriginalValue = ""; KDRefreshOutfitInfo(); - return true; } else { KDConfirmType = "save"; KinkyDungeonReplaceConfirm = 2; - return true; } + return true; }, true, 465, 930, 240, 50, TextGet((KinkyDungeonReplaceConfirm > 0 && KDConfirmType == 'save') ? "KDWardrobeSaveOutfitConfirm" : From 0e2ed5b966974fdf6f192adaa704afe311ec55ad Mon Sep 17 00:00:00 2001 From: "Leo L. Schwab" Date: Tue, 6 May 2025 02:40:50 -0700 Subject: [PATCH 4/4] Enable saves in new format. Newly generated save codes will be in the new format. --- Game/src/base/KinkyDungeon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Game/src/base/KinkyDungeon.ts b/Game/src/base/KinkyDungeon.ts index 97f9c5dda..3fe45f24f 100644 --- a/Game/src/base/KinkyDungeon.ts +++ b/Game/src/base/KinkyDungeon.ts @@ -6887,7 +6887,7 @@ let KDSaveTimeout = 600000; // 10 minutes async function KinkyDungeonCompressSave(save: string, type = SaveType.Game): Promise { if (window.Worker) { const workerMsg: SaveWorkerMsg = { - op: 'cmp-legacy', + op: 'cmp', type: type, data: save };