From 727a78efdd5457cb7038b72c0e3b29aa4aee8155 Mon Sep 17 00:00:00 2001
From: Frankly George <54015-franklygeorge@users.noreply.gitgud.io>
Date: Fri, 11 Oct 2024 19:40:12 +0000
Subject: [PATCH] Fix slave import and export not serializing correctly

---
 devTools/types/SugarCubeExtensions.d.ts       | 22 +++++++++++++++++
 src/002-config/fc-version.js                  |  2 +-
 .../backwardsCompatibility/datatypeCleanup.js |  6 ++---
 src/data/patches/patch.js                     | 19 +++++++--------
 .../releases/1261_fixBrokenEyesAndPartners.js |  4 ++--
 .../patches/releases/1266_fixPartnersAgain.js | 10 ++++++++
 src/data/verification/verifyHumanState.js     |  2 +-
 src/data/verification/verifyUtils.js          |  5 ++--
 src/data/verification/zVerify.js              |  1 -
 src/debugging/debugJS.js                      | 10 ++++++--
 src/events/intro/customizeSlaveTrade.js       |  4 ++--
 src/events/intro/introSummary.js              |  4 ++--
 src/facilities/dressingRoom/dressingRoom.js   |  4 ++--
 src/js/displayVariables.js                    |  2 +-
 src/js/releaseRules.js                        |  2 +-
 src/js/rulesAssistantOptions.js               | 10 ++++----
 src/js/rulesAssistantSummary.js               |  4 ++--
 src/js/states/001-GenePoolRecord.js           |  6 ++---
 src/js/storyJS.js                             |  2 +-
 src/js/utilsSlave.js                          |  2 +-
 src/npc/importSlave.js                        |  6 ++---
 src/npc/slaveBot/generateSlaveBot.js          | 24 +++++++++----------
 22 files changed, 92 insertions(+), 59 deletions(-)
 create mode 100644 src/data/patches/releases/1266_fixPartnersAgain.js

diff --git a/devTools/types/SugarCubeExtensions.d.ts b/devTools/types/SugarCubeExtensions.d.ts
index a4971ee3f33..ef0899024f2 100644
--- a/devTools/types/SugarCubeExtensions.d.ts
+++ b/devTools/types/SugarCubeExtensions.d.ts
@@ -12,11 +12,33 @@ declare module "twine-sugarcube" {
 	interface StateAPI {
 		expired: StoryMoment[];
 		clearTemporary(): void;
+		/**
+		 * Restores the game state to the last state it was in.
+		 * Usually the state that it was in when the latest passage was loaded
+		 */
+		restore(): void;
 	}
 
 	interface UIBarAPI {
 		update(): void;
 	}
+
+	interface SerialAPI {
+		/**
+		 * Converts a JavaScript Object Notation (JSON) string with custom revival methods into an object.
+		 * @param text A valid JSON string created using Serial.stringify().
+		 * @param reviver A function that transforms the results. This function is called for each member of the object.
+		 * If a member contains nested objects, the nested objects are transformed before the parent object is.
+		 */
+		parse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
+		/**
+		 * Converts a JavaScript value to a JavaScript Object Notation (JSON) string with custom revival methods.
+		 * @param value A JavaScript value, usually an object or array, to be converted.
+		 * @param replacer A function that transforms the results.
+		 * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
+		 */
+		stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string;
+	}
 }
 
 export {};
diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js
index e8d4759972f..7676d245b18 100644
--- a/src/002-config/fc-version.js
+++ b/src/002-config/fc-version.js
@@ -10,5 +10,5 @@ App.Version = {
 	 * The release numbers got messed up, this is corrected in `src/js/eventHandlers.js` and `/src/data/patches/patch.js`.
 	 * The two line above and this line should be safe to remove after release 2001.
 	 */
-	release: 1265,
+	release: 1266,
 };
diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js
index d07247ac764..fafad5265fd 100644
--- a/src/data/backwardsCompatibility/datatypeCleanup.js
+++ b/src/data/backwardsCompatibility/datatypeCleanup.js
@@ -2446,7 +2446,7 @@ App.Update.RARuleDatatypeCleanup = function() {
 					if (count === 0) {
 						count++;
 						cond.activation = [false];
-						console.log("no match", JSON.parse(JSON.stringify(cond)));
+						console.log("no match", Serial.parse(Serial.stringify(cond)));
 					}
 					cond.activation.push(count, "and");
 				}
@@ -2455,7 +2455,7 @@ App.Update.RARuleDatatypeCleanup = function() {
 					cond.activation = [false, 1, "and"];
 				}
 			} catch (e) {
-				console.log("condition broke", e.message, JSON.parse(JSON.stringify(cond)));
+				console.log("condition broke", e.message, Serial.parse(Serial.stringify(cond)));
 				cond.activation = [false, 1, "and"];
 			} finally {
 				delete cond.function;
@@ -2487,7 +2487,7 @@ App.Update.RARuleDatatypeCleanup = function() {
 					}
 				}
 			} catch (e) {
-				console.log("assignments broke", e.message, JSON.parse(JSON.stringify(cond)));
+				console.log("assignments broke", e.message, Serial.parse(Serial.stringify(cond)));
 			} finally {
 				// @ts-ignore
 				delete cond.assignment;
diff --git a/src/data/patches/patch.js b/src/data/patches/patch.js
index 4a6a712a25f..8c995fc9e4d 100644
--- a/src/data/patches/patch.js
+++ b/src/data/patches/patch.js
@@ -180,7 +180,6 @@ App.Patch.applyAll = () => {
 			if (App.Verify.Utils.verificationError) {
 				header.innerHTML += `<span class="error">Verification failed!</span>`;
 				console.error("Verification failed!");
-				// @ts-ignore
 				State.restore(); // restore the state to before patching
 			} else {
 				if (patchCount !== 0) {
@@ -231,7 +230,6 @@ App.Patch.applyAll = () => {
 				header.innerHTML += `<span class="error">Verification failed!</span>`;
 				console.error("Verification failed!");
 				App.Patch.Utils.current.div.append(App.UI.DOM.formatException(e));
-				// @ts-ignore
 				State.restore(); // restore the state to before patching
 				header.innerHTML += `<br><span>See below for details</span><hr>`;
 				// close the loading screen so that the user can see the error
@@ -269,14 +267,14 @@ App.Patch.applyAll = () => {
 				App.Patch.Utils.playerState("V.PC", V.PC, "V.PC", App.Patch.Utils.current.div, patchID);
 				wombs.push({identifier: `V.PC`, actor: V.PC, div: App.Patch.Utils.current.div});
 
-			// SlaveState
-			const slaves = getSlaves();
-			for (i in slaves ?? []) {
-				App.Patch.Utils.slaveState(
-					`getSlaves()[${i}]`, slaves[i], "main slave pool", App.Patch.Utils.current.div, patchID
-				);
-				wombs.push({identifier: `getSlaves()[${i}]`, actor: slaves[i], div: App.Patch.Utils.current.div});
-			}
+				// SlaveState
+				const slaves = getSlaves();
+				for (i in slaves ?? []) {
+					App.Patch.Utils.slaveState(
+						`getSlaves()[${i}]`, slaves[i], "main slave pool", App.Patch.Utils.current.div, patchID
+					);
+					wombs.push({identifier: `getSlaves()[${i}]`, actor: slaves[i], div: App.Patch.Utils.current.div});
+				}
 
 				if (V.hostage) {
 					App.Patch.Utils.slaveState(
@@ -361,7 +359,6 @@ App.Patch.applyAll = () => {
 			} catch (e) {
 				header.innerHTML += `<span class="error">Patching Failed when applying patch ${App.Patch.Utils.current.patch}["${App.Patch.Utils.current.type}"] to ${App.Patch.Utils.current.identifier}!</span>`;
 				App.Patch.Utils.current.div.append(App.UI.DOM.formatException(e));
-				// @ts-ignore
 				State.restore(); // restore the state to before patching
 				header.innerHTML += `<br><span>See below for details</span><hr>`;
 				// close the loading screen so that the user can see the error
diff --git a/src/data/patches/releases/1261_fixBrokenEyesAndPartners.js b/src/data/patches/releases/1261_fixBrokenEyesAndPartners.js
index 9daad3875cb..96ff6f736a7 100644
--- a/src/data/patches/releases/1261_fixBrokenEyesAndPartners.js
+++ b/src/data/patches/releases/1261_fixBrokenEyesAndPartners.js
@@ -10,7 +10,7 @@ App.Patch.register({
 			if (Array.isArray(actor.partners)) {
 				actor.partners = new Set(getProp(actor, "partners"));
 			} else {
-				App.Patch.log(`partners was '${JSON.stringify(actor.partners)}' of type '${typeof actor.partners}', setting partners to an empty set`);
+				App.Patch.log(`partners was '${Serial.stringify(actor.partners)}' of type '${typeof actor.partners}', setting partners to an empty set`);
 				actor.partners = new Set([]);
 			}
 		}
@@ -47,7 +47,7 @@ App.Patch.register({
 			App.Utils.assignMissingDefaults(actor.eye.right, defaultEye.right);
 		}
 		actor.eye.origColor = actor.eye.origColor ?? origColor;
-		if (JSON.stringify(origEye) !== JSON.stringify(actor.eye)) {
+		if (Serial.stringify(origEye) !== Serial.stringify(actor.eye)) {
 			App.Patch.log('Fixed corrupted eye object');
 		}
 		deleteProps(actor, "eyeColor");
diff --git a/src/data/patches/releases/1266_fixPartnersAgain.js b/src/data/patches/releases/1266_fixPartnersAgain.js
new file mode 100644
index 00000000000..359c8724022
--- /dev/null
+++ b/src/data/patches/releases/1266_fixPartnersAgain.js
@@ -0,0 +1,10 @@
+App.Patch.register({
+	releaseID: 1266,
+	descriptionOfChanges: `Recent changes to SC meant that imported/exported slaves were not serialized correctly. This 'fixes' (data is lost) those slaves.`,
+	humanState: (div, actor, location) => {
+		if (!(actor.partners instanceof Set)) {
+			actor.partners = new Set([]);
+		}
+		return actor;
+	}
+});
diff --git a/src/data/verification/verifyHumanState.js b/src/data/verification/verifyHumanState.js
index cb210f2caed..69cf4d65821 100644
--- a/src/data/verification/verifyHumanState.js
+++ b/src/data/verification/verifyHumanState.js
@@ -76,7 +76,7 @@ App.Verify.I.humanHealth = (actor, location) => {
 	 * @see updateHealth
 	 * To get around this issue we are only running updateHealth(actor) if the values were actually changed by this function
 	 */
-	if (JSON.stringify(oldHealth) !== JSON.stringify(actor.health)) {
+	if (Serial.stringify(oldHealth) !== Serial.stringify(actor.health)) {
 		updateHealth(actor);
 	}
 	return actor;
diff --git a/src/data/verification/verifyUtils.js b/src/data/verification/verifyUtils.js
index be0b47d88eb..90d21cf568b 100644
--- a/src/data/verification/verifyUtils.js
+++ b/src/data/verification/verifyUtils.js
@@ -29,7 +29,7 @@ App.Verify.Utils.findSlaveId = (predicate) => {
 App.Verify.Utils.changeSummary = (obj1, obj2, setKey, instructionID, identifier) => {
 	const temp = `When executing 'App.Verify.instructions.${setKey}.${instructionID}': '{path}' {change}`;
 
-	if (JSON.stringify(obj1) === JSON.stringify(obj2)) { return undefined; } // Quick and dirty comparision, don't do work when it isn't needed
+	if (Serial.stringify(obj1) === Serial.stringify(obj2)) { return undefined; } // Quick and dirty comparision, don't do work when it isn't needed
 
 	// something has changed, deep comparision needed
 	/**
@@ -42,7 +42,7 @@ App.Verify.Utils.changeSummary = (obj1, obj2, setKey, instructionID, identifier)
 		/** @type {App.Verify.Utils.ChangeRecord[]} */
 		let changes = [];
 
-		if (JSON.stringify(obj1) === JSON.stringify(obj2)) { return changes; } // don't do work when it isn't needed
+		if (Serial.stringify(obj1) === Serial.stringify(obj2)) { return changes; } // don't do work when it isn't needed
 
 		let keys1 = Object.keys(obj1);
 		let keys2 = Object.keys(obj2);
@@ -178,7 +178,6 @@ App.Verify.Utils.verify = (setKey, identifier, obj, extra, div) => {
 			}
 			if (isV) {
 				// rollback the game state
-				// @ts-ignore restore does exist on our custom SugarCube
 				State.restore();
 				// and return nothing
 				return;
diff --git a/src/data/verification/zVerify.js b/src/data/verification/zVerify.js
index 5972d8589c1..fbb53f4296d 100644
--- a/src/data/verification/zVerify.js
+++ b/src/data/verification/zVerify.js
@@ -283,7 +283,6 @@ App.Verify.everything = (div, callback) => {
 				console.error(e);
 				console.error("Verification failed!");
 			}
-			// @ts-ignore
 			State.restore(); // restore the state to before patching
 			// close the loading screen (if it is open) so that the user can see the error
 			App.Patch.Utils.loadingScreen.end();
diff --git a/src/debugging/debugJS.js b/src/debugging/debugJS.js
index dfc06f84247..61fda059a51 100644
--- a/src/debugging/debugJS.js
+++ b/src/debugging/debugJS.js
@@ -107,12 +107,18 @@ App.Debug.dumpGameState = function() {
 	}
 
 	const handler = (save) => {
-		downloadToFile(JSON.stringify(save, null, 2), save.id + ".json", "text/plain");
+		let name = save.id + ".json";
+		try {
+			name = App.Utils.getSaveFilename(undefined, "json");
+		} catch (ex) {
+			console.error(ex);
+		}
+		downloadToFile(Serial.stringify(save, null, 2), name, "text/plain");
 	};
 
 	Save.onSave.add(handler);
 	try {
-		SugarCube.Save.base64.save();
+		Save.base64.save();
 	} finally {
 		Save.onSave.delete(handler);
 	}
diff --git a/src/events/intro/customizeSlaveTrade.js b/src/events/intro/customizeSlaveTrade.js
index aceb9beccb7..3d0483c9888 100644
--- a/src/events/intro/customizeSlaveTrade.js
+++ b/src/events/intro/customizeSlaveTrade.js
@@ -394,7 +394,7 @@ App.Intro.CustomSlaveTrade = function() {
 	 */
 	function settingsExport(container) {
 		let textArea = document.createElement("textarea");
-		textArea.value = JSON.stringify(V.nationalities);
+		textArea.value = Serial.stringify(V.nationalities);
 		$(container).empty().append(textArea);
 	}
 
@@ -407,7 +407,7 @@ App.Intro.CustomSlaveTrade = function() {
 		button.append("Load");
 		button.onclick = () => {
 			try {
-				V.nationalities = JSON.parse(textArea.value);
+				V.nationalities = Serial.parse(textArea.value);
 			} catch (SyntaxError) {
 				Dialog.create("Invalid Input");
 				Dialog.append("The input is not a valid nationalities object.");
diff --git a/src/events/intro/introSummary.js b/src/events/intro/introSummary.js
index a1720a0ce8b..06983579f46 100644
--- a/src/events/intro/introSummary.js
+++ b/src/events/intro/introSummary.js
@@ -26,7 +26,7 @@ App.Intro.getNondefaultOptionsAsObject = function(exportFrom = V, defaults = App
 };
 
 App.Intro.getNondefaultOptionsAsJSON = function() {
-	return JSON.stringify(App.Intro.getNondefaultOptionsAsObject(), null, 2);
+	return Serial.stringify(App.Intro.getNondefaultOptionsAsObject(), null, 2);
 };
 
 App.Intro.summary = function() {
@@ -436,7 +436,7 @@ App.Intro.summary = function() {
 			App.UI.DOM.link(
 				"Import game options",
 				() => {
-					const optionsFromJSON = JSON.parse(textareaElement.value);
+					const optionsFromJSON = Serial.parse(textareaElement.value);
 					const tooltipsEnabled = V.tooltipsEnabled;
 					App.Intro.assignOnlyMatchingKeys(V, optionsFromJSON, App.Data.defaultGameOptions);
 					if (tooltipsEnabled !== V.tooltipsEnabled) {
diff --git a/src/facilities/dressingRoom/dressingRoom.js b/src/facilities/dressingRoom/dressingRoom.js
index 68250d55381..a75b874acf1 100644
--- a/src/facilities/dressingRoom/dressingRoom.js
+++ b/src/facilities/dressingRoom/dressingRoom.js
@@ -418,7 +418,7 @@ App.UI.DressingRoom.render = function() {
 						cont.appendChild(element);
 					}
 					// @ts-ignore
-					element.value = JSON.stringify(V.customClothesPrompts, null, 2);
+					element.value = Serial.stringify(V.customClothesPrompts, null, 2);
 				}
 			),
 			App.UI.DOM.link(
@@ -438,7 +438,7 @@ App.UI.DressingRoom.render = function() {
 						submitBtn.onclick = () => {
 							try {
 								// @ts-ignore
-								V.customClothesPrompts = JSON.parse(element.value);
+								V.customClothesPrompts = Serial.parse(element.value);
 								options.refresh();
 							} catch (e) {
 								alert(`Couldn't import prompts:\n${e.message}`);
diff --git a/src/js/displayVariables.js b/src/js/displayVariables.js
index c80544ca7f8..c065dc7786a 100644
--- a/src/js/displayVariables.js
+++ b/src/js/displayVariables.js
@@ -8,7 +8,7 @@ App.checkVars = function() {
 			case"number":
 				return isNaN(value) ? "NaN" : isFinite(value) ? String(value) : "Infinity";
 			case"string":
-				return JSON.stringify(value);
+				return Serial.stringify(value);
 			case"function":
 				return "(function)";
 			default:
diff --git a/src/js/releaseRules.js b/src/js/releaseRules.js
index 892774cbb9a..e08dffc2319 100644
--- a/src/js/releaseRules.js
+++ b/src/js/releaseRules.js
@@ -336,7 +336,7 @@ App.Utils.testAllReleaseText = function testAllReleaseText() {
 		rule.master = Number(bits[4]);
 		slave.rules.release = rule;
 
-		r += JSON.stringify(rule) + "\n";
+		r += Serial.stringify(rule) + "\n";
 		r += App.Utils.releaseSummaryShort(slave) + "\n";
 		r += App.Utils.releaseSummaryLong(slave) + "\n";
 		r += App.Desc.releaseDesc(slave) + "\n";
diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js
index 1d8eae27a20..996bfa1ed13 100644
--- a/src/js/rulesAssistantOptions.js
+++ b/src/js/rulesAssistantOptions.js
@@ -418,7 +418,7 @@ App.RA.options = (function() {
 			} else {
 				selected = this.children.filter(listItem => this.dataEqual(listItem.data, dataValue));
 			}
-			if (selected.length > 1) { throw Error(`Multiple shortcuts matched ${JSON.stringify(dataValue)}`); }
+			if (selected.length > 1) { throw Error(`Multiple shortcuts matched ${Serial.stringify(dataValue)}`); }
 			if (selected.length === 1) {
 				const listItem = selected[0];
 				listItem.select(false);
@@ -1214,7 +1214,7 @@ App.RA.options = (function() {
 		loadNewRule() {
 			const text = this.textarea.value;
 			try {
-				const rule = JSON.parse(text);
+				const rule = Serial.parse(text);
 				if (Array.isArray(rule)) {
 					rule.forEach(r => {
 						r.ID = generateNewID();
@@ -1385,12 +1385,12 @@ App.RA.options = (function() {
 
 	class ExportField extends Element {
 		render(...args) {
-			let element = document.getElementById("exportfield");
+			let element = /** @type {HTMLTextAreaElement} */ (document.getElementById("exportfield"));
 			if (element === null) {
 				element = document.createElement("textarea");
 				element.id = "exportfield";
 			}
-			element.value = JSON.stringify(args, null, 2);
+			element.value = Serial.stringify(args, null, 2);
 			return element;
 		}
 	}
@@ -4548,7 +4548,7 @@ App.RA.options = (function() {
 			} else {
 				selected = this.children.filter(listItem => this.dataEqual(listItem.data, dataValue));
 			}
-			if (selected.length > 1) { throw Error(`Multiple shortcuts matched ${JSON.stringify(dataValue)}`); }
+			if (selected.length > 1) { throw Error(`Multiple shortcuts matched ${Serial.stringify(dataValue)}`); }
 			if (selected.length === 1) {
 				const listItem = selected[0];
 				listItem.select(false);
diff --git a/src/js/rulesAssistantSummary.js b/src/js/rulesAssistantSummary.js
index aaac0045430..c1720ceaec5 100644
--- a/src/js/rulesAssistantSummary.js
+++ b/src/js/rulesAssistantSummary.js
@@ -104,10 +104,10 @@ App.RA.summary = function() {
 					} else if (v.hasOwnProperty('min') && v.hasOwnProperty('max')) {
 						return `${v.min} to ${v.max}`;
 					} else {
-						return JSON.stringify(v);
+						return Serial.stringify(v);
 					}
 				} else if (Array.isArray(v)) {
-					return JSON.stringify(v);
+					return Serial.stringify(v);
 				}
 				return `${v}`;
 			}
diff --git a/src/js/states/001-GenePoolRecord.js b/src/js/states/001-GenePoolRecord.js
index 68a157eca9d..b09d57dbb79 100644
--- a/src/js/states/001-GenePoolRecord.js
+++ b/src/js/states/001-GenePoolRecord.js
@@ -26,7 +26,7 @@ globalThis.getGenePoolRecord = (key, missingOkay=false, write=false) => {
 	} else if (typeof key === "object" && "ID" in key) {
 		ID = String(key.ID);
 	} else {
-		throw new Error(`key must be an FC.HumanState object or valid HumanState.ID! Got: ${JSON.stringify(key)}`);
+		throw new Error(`key must be an FC.HumanState object or valid HumanState.ID! Got: ${Serial.stringify(key)}`);
 	}
 	if (ID === "0") {
 		console.warn(new Error("getGenePoolRecord: actors with an ID equal to 0 cannot exist in the gene pool"));
@@ -94,7 +94,7 @@ globalThis.isInGenePool = (key) => {
 	} else if (typeof key === "object" && "ID" in key) {
 		ID = String(key.ID);
 	} else {
-		throw new Error(`key must be an FC.HumanState object or valid HumanState.ID! Got: ${JSON.stringify(key)}`);
+		throw new Error(`key must be an FC.HumanState object or valid HumanState.ID! Got: ${Serial.stringify(key)}`);
 	}
 	if (ID in V.genePool) {
 		return true;
@@ -169,7 +169,7 @@ globalThis.addToGenePool = (actor) => {
 				continue;
 			}
 			if (key in template) {
-				if (JSON.stringify(obj[key]) === JSON.stringify(template[key])) {
+				if (Serial.stringify(obj[key]) === Serial.stringify(template[key])) {
 					delete obj[key];
 				} else if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
 					if (template[key] && typeof template[key] === "object" && !Array.isArray(template[key])) {
diff --git a/src/js/storyJS.js b/src/js/storyJS.js
index 8704a782666..cbb8e0bca46 100644
--- a/src/js/storyJS.js
+++ b/src/js/storyJS.js
@@ -459,7 +459,7 @@ globalThis.bodyguardSuccessorEligible = function(slave) {
  * @returns {string}
  */
 globalThis.toJson = function(obj) {
-	let jsontext = JSON.stringify(obj);
+	let jsontext = Serial.stringify(obj);
 	jsontext = jsontext.replace(/^{/, "");
 	jsontext = jsontext.replace(/}$/, "");
 	return jsontext;
diff --git a/src/js/utilsSlave.js b/src/js/utilsSlave.js
index c4fc5638987..d568e58f5d3 100644
--- a/src/js/utilsSlave.js
+++ b/src/js/utilsSlave.js
@@ -3831,7 +3831,7 @@ globalThis.getSlave = function(ID) {
  */
 globalThis.overwriteSlave = function(ID, slave) {
 	if (!slave || !(typeof slave === "object") || slave === null || Array.isArray(slave)) {
-		throw new Error(`Passed object is not a SlaveState object. Object: ${JSON.stringify(slave)}`);
+		throw new Error(`Passed object is not a SlaveState object. Object: ${Serial.stringify(slave)}`);
 	}
 	const index = V.slaveIndices[ID];
 	if (index !== undefined) {
diff --git a/src/npc/importSlave.js b/src/npc/importSlave.js
index 507504b57bb..7754d3e6291 100644
--- a/src/npc/importSlave.js
+++ b/src/npc/importSlave.js
@@ -44,7 +44,7 @@ App.UI.SlaveInteract.importSlaveFromCharacterCard = () => {
 
 		const pngAb = await pngFile.arrayBuffer();
 		const allTextContent = App.UI.SlaveInteract.SlaveBot.Png.Parse(pngAb);
-		const slaveBotObj = JSON.parse(allTextContent);
+		const slaveBotObj = Serial.parse(allTextContent);
 		/**
 		 * Parser returns the stuff we care about under "data".
 		 * @type {FC.SlaveBot.CharacterBook}
@@ -95,7 +95,7 @@ App.UI.SlaveInteract.importSlaveFromString = (slaveSrc, div) => {
 	if (slaveSrc[0] !== "{") { slaveSrc = "{" + slaveSrc; }
 	if (slaveSrc.slice(-1) !== "}") { slaveSrc += "}"; }
 	// generate slave
-	let slave = JSON.parse(slaveSrc);
+	let slave = Serial.parse(slaveSrc);
 	slave.ID = generateSlaveID();
 	App.Verify.slaveState("<imported>", slave, "none", div);
 	if (slave?.assignment) {
@@ -112,7 +112,7 @@ App.UI.SlaveInteract.exportSlave = function(slave) {
 	const exportSlave = _.cloneDeep(slave);
 	App.Verify.slaveState("<exported>", exportSlave, "none");
 	exportSlave.ID = 0;
-	return JSON.stringify(exportSlave, null, 2);
+	return Serial.stringify(exportSlave, null, 2);
 };
 
 /**
diff --git a/src/npc/slaveBot/generateSlaveBot.js b/src/npc/slaveBot/generateSlaveBot.js
index 9c8af462b0a..ab60f0e75a2 100644
--- a/src/npc/slaveBot/generateSlaveBot.js
+++ b/src/npc/slaveBot/generateSlaveBot.js
@@ -23,7 +23,7 @@ App.UI.SlaveInteract.SlaveBot.createSlaveBot = (slave) => {
 
 
 /**
- * 
+ *
  * @param {File} pngFile File from <input type="file" />
  */
 App.UI.SlaveInteract.SlaveBot.importSlavePNG = async (pngFile) => {
@@ -34,7 +34,7 @@ App.UI.SlaveInteract.SlaveBot.importSlavePNG = async (pngFile) => {
 	/**
 	 * @type {FC.SlaveBot.CharacterBook}
 	 */
-	const slaveBotData = JSON.parse(allTextContent);
+	const slaveBotData = Serial.parse(allTextContent);
 
 	if (!slaveBotData.extensions.freecitiesJSON) {
 		alert("No Free Cities data found from this character card. Maybe it is corrupted?");
@@ -297,7 +297,7 @@ App.UI.SlaveInteract.SlaveBot.downloadFile = (file) => {
  * @returns {File}
  */
 App.UI.SlaveInteract.SlaveBot.exportJsonFile = (characterCard) => {
-	const file = new File([JSON.stringify(characterCard, undefined, '\t')], `${characterCard.data.name || 'character'}.card.json`, { type: 'application/json;charset=utf-8' });
+	const file = new File([Serial.stringify(characterCard, undefined, '\t')], `${characterCard.data.name || 'character'}.card.json`, {type: 'application/json;charset=utf-8'});
 	return file;
 };
 
@@ -312,7 +312,7 @@ App.UI.SlaveInteract.SlaveBot.exportFile = async (slaveState) => {
 		if (V.aiCachingStrategy === 'static') {
 			const displayedIdx = slaveState.custom.aiDisplayImageIdx;
 			const imageDbId = slaveState.custom.aiImageIds[displayedIdx];
-			const { data } = await App.Art.GenAI.staticImageDB.getImage(imageDbId);
+			const {data} = await App.Art.GenAI.staticImageDB.getImage(imageDbId);
 
 			imageUrl = data;
 		} else {
@@ -335,23 +335,23 @@ App.UI.SlaveInteract.SlaveBot.exportFile = async (slaveState) => {
 
 
 
-		const json = JSON.stringify(characterCardObj, undefined, '\t');
+		const json = Serial.stringify(characterCardObj, undefined, '\t');
 		const png = App.UI.SlaveInteract.Png.Generate(imageArrayBuffer, json);
 
-		const file = new File([png], `${characterCardObj.data.name || 'character'}.card.png`, { type: 'image/png' });
+		const file = new File([png], `${characterCardObj.data.name || 'character'}.card.png`, {type: 'image/png'});
 		return file;
 	} catch (e) {
-		const { fallbackImage, createCharacterDataFromSlave } = App.UI.SlaveInteract.SlaveBot;
+		const {fallbackImage, createCharacterDataFromSlave} = App.UI.SlaveInteract.SlaveBot;
 
 		const imageRes = await fetch(fallbackImage);
 		const imageArrayBuffer = await imageRes.arrayBuffer();
 
 		const characterCardObj = createCharacterDataFromSlave(slaveState);
 
-		const json = JSON.stringify(characterCardObj, undefined, '\t');
+		const json = Serial.stringify(characterCardObj, undefined, '\t');
 		const png = App.UI.SlaveInteract.SlaveBot.Png.Generate(imageArrayBuffer, json);
 
-		const file = new File([png], `${characterCardObj.data.name || 'character'}.card.png`, { type: 'image/png' });
+		const file = new File([png], `${characterCardObj.data.name || 'character'}.card.png`, {type: 'image/png'});
 		return file;
 	}
 };
@@ -428,7 +428,7 @@ App.UI.SlaveInteract.SlaveBot.createCharacterDataFromSlave = (slave) => {
  */
 App.UI.SlaveInteract.SlaveBot.Generate.description = (slave) => {
 	let r = [];
-	const { he, his } = getPronouns(slave);
+	const {he, his} = getPronouns(slave);
 	let descParts = [];
 
 	// NAME
@@ -869,7 +869,7 @@ App.UI.SlaveInteract.SlaveBot.Generate.standardPrompt = () => {
  */
 App.UI.SlaveInteract.SlaveBot.Generate.firstMessage = (slave) => {
 	let r = [];
-	const { He, His, he, his } = getPronouns(slave);
+	const {He, His, he, his} = getPronouns(slave);
 	// Set up basic scenario
 	r.push("I am sitting in my office as {{char}} arrives for inspection.\r\n");
 
@@ -1319,7 +1319,7 @@ App.UI.SlaveInteract.SlaveBot.Generate.lorebookHeadGirl = (startId) => {
 			keys: ["Head Girl", "headgirl", S.HeadGirl.slaveName], // either/or title, first name
 			secondary_keys: [], // not used
 			comment: "HeadGirl",
-			content: App.UI.SlaveInteract.SlaveBot.Generate.description(S.HeadGirl) + "/nThe head girl manages the health, training, and wellbeing of all slaves in your penthouse, and ensures your businesses are running smoothly.",// slave description minus relationships
+			content: App.UI.SlaveInteract.SlaveBot.Generate.description(S.HeadGirl) + "/nThe head girl manages the health, training, and wellbeing of all slaves in your penthouse, and ensures your businesses are running smoothly.", // slave description minus relationships
 			constant: false, // selectively applied
 			selective: false,
 			insertion_order: 100,
-- 
GitLab