@tts-tools/savefile
Version:
Module to extract a savefile from Tabletop Simulator into multiple files.
197 lines • 7.34 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeExtractedObject = exports.writeExtractedSave = exports.extractSave = exports.readSave = void 0;
const big_js_1 = __importDefault(require("big.js"));
const fs_1 = require("fs");
const json_stable_stringify_1 = __importDefault(require("json-stable-stringify"));
const io_1 = require("./io");
const unbundle_1 = require("./unbundle");
const HANDLED_KEYS = [
"LuaScript",
"LuaScriptState",
"XmlUI",
"ContainedObjects",
"ObjectStates",
"States",
"ChildObjects",
];
const FLOATING_MARKER = ">>floating-point<<";
const DEFAULT_ROUNDING = 4;
const state = {
files: new Map(),
};
const readSave = (path) => {
let content = (0, fs_1.readFileSync)(path, { encoding: "utf-8" });
content = content.replace(/^(\s*"[\w]+": )(-?\d+(?:\.\d+(?:[eE]-\d+)?)?)($|,)/gm, `$1"${FLOATING_MARKER}$2"$3`);
return JSON.parse(content);
};
exports.readSave = readSave;
/**
* Extracts the given `saveFile`, by splitting the data into a nested directory structure.
* It also returns an unbundled version of the save file.
*
* @param saveFile The save file to extract.
* @param options The [[Options]] to use.
* @returns The unbundled/normalized version of the save file
*/
const extractSave = (saveFile, options) => {
const unbundledSave = (0, unbundle_1.unbundleSave)(saveFile);
(0, exports.writeExtractedSave)(unbundledSave, options);
return unbundledSave;
};
exports.extractSave = extractSave;
const writeExtractedSave = (saveFile, options) => {
clearState();
(0, fs_1.mkdirSync)(options.output, { recursive: true });
extractScripts(saveFile, options.output, options);
extractContent(saveFile.ObjectStates, options.output + "/", options);
extractData(saveFile, options.output, options);
};
exports.writeExtractedSave = writeExtractedSave;
const writeExtractedObject = (object, options) => {
clearState();
const objectPath = `${options.output}/${getDirectoryName(object)}`;
(0, fs_1.mkdirSync)(objectPath, { recursive: true });
extractObject(object, objectPath, options);
};
exports.writeExtractedObject = writeExtractedObject;
const clearState = () => {
state.files.clear();
};
/**
* @param object The object to Extract
* @param path Current nested path where files for this object will be placed at
*/
const extractObject = (object, path, options) => {
(0, fs_1.mkdirSync)(path, { recursive: true });
extractScripts(object, path, options);
if (object.ContainedObjects) {
extractContent(object.ContainedObjects, path, options);
}
extractStates(object, path, options);
extractChildren(object, path, options);
extractData(object, path, options);
};
const extractScripts = (object, path, options) => {
if (object.LuaScript) {
(0, io_1.writeFile)(`${path}/Script.ttslua`, object.LuaScript);
}
if (object.LuaScriptState && options.withState) {
(0, io_1.writeFile)(`${path}/State.txt`, object.LuaScriptState);
}
if (options.metadataField) {
const metadata = object[options.metadataField];
if (metadata) {
(0, io_1.writeFile)(`${path}/Metadata.toml`, metadata);
}
}
if (object.XmlUI) {
(0, io_1.writeFile)(`${path}/UI.xml`, object.XmlUI);
}
};
const extractContent = (objects, path, options) => {
const contents = [];
objects.forEach((object) => {
const contentSubPath = options.contentsPath || ".";
const objectDirectory = getFreeDirectoryName(object, `${path}/${contentSubPath}`);
const contentsPath = `${contentSubPath}/${objectDirectory}`;
contents.push({
path: contentsPath,
});
extractObject(object, `${path}/${contentsPath}`, options);
});
(0, io_1.writeJson)(`${path}/Contents.json`, contents);
};
const extractStates = (object, path, options) => {
if (!object.States) {
return;
}
const states = {};
Object.entries(object.States).forEach(([id, state]) => {
const statesSubPath = options.statesPath || ".";
const objectDirectory = getDirectoryName(state);
const statePath = `${statesSubPath}/${id}-${objectDirectory}`;
states[id] = {
path: statePath,
};
extractObject(state, `${path}/${statePath}`, options);
});
(0, io_1.writeJson)(`${path}/States.json`, states);
};
const extractChildren = (object, path, options) => {
if (!object.ChildObjects) {
return;
}
const childObjects = [];
object.ChildObjects.forEach((child) => {
const childrenSubPath = options.childrenPath || ".";
const objectDirectory = getDirectoryName(child);
const childPath = `${childrenSubPath}/${objectDirectory}`;
childObjects.push({
path: childPath,
});
extractObject(child, `${path}/${childPath}`, options);
});
(0, io_1.writeJson)(`${path}/Children.json`, childObjects);
};
const extractData = (object, path, options) => {
const replacer = (key, value) => dataReplacer(key, value, options);
let dataContent;
if (options.keyOrder) {
dataContent = (0, json_stable_stringify_1.default)(object, {
replacer: replacer,
space: 2,
cmp: (a, b) => keyOrderer(a, b, options.keyOrder),
});
}
else {
dataContent = JSON.stringify(object, replacer, 2);
}
dataContent = dataContent.replace(new RegExp(`"${FLOATING_MARKER}([^"]+)"`, "g"), "$1");
(0, io_1.writeFile)(`${path}/Data.json`, dataContent);
};
const dataReplacer = (key, value, options) => {
if (HANDLED_KEYS.includes(key) || key === options.metadataField) {
return undefined;
}
if (options.normalize && typeof value === "string" && value.startsWith(FLOATING_MARKER)) {
const roundTo = typeof options.normalize === "number" ? options.normalize : DEFAULT_ROUNDING;
const actualValue = value.slice(FLOATING_MARKER.length);
const numericValue = (0, big_js_1.default)(actualValue).round(roundTo);
return `${FLOATING_MARKER}${numericValue}`;
}
return value;
};
const keyOrderer = (a, b, keyOrder) => {
const aOrder = keyOrder.indexOf(a.key);
const bOrder = keyOrder.indexOf(b.key);
if (aOrder > -1) {
return bOrder == -1 ? -1 : aOrder > bOrder ? 1 : -1;
}
return bOrder == -1 ? a.key.localeCompare(b.key) : 1;
};
const getDirectoryName = (object) => {
const baseName = object.Nickname && object.Nickname.length > 0 ? object.Nickname : object.Name;
return `${baseName}.${object.GUID}`.replace(/[^\w \^&'@{}\[\],$=!\-#()%\.+~_]/g, "-");
};
const getFreeDirectoryName = (object, path) => {
let objectPath = getDirectoryName(object);
let subFiles = state.files.get(path);
if (!subFiles) {
subFiles = new Map();
state.files.set(path, subFiles);
}
const existing = subFiles.get(objectPath);
if (existing) {
subFiles.set(objectPath, existing + 1);
objectPath += `.${existing}`;
}
else {
subFiles.set(objectPath, 1);
}
return objectPath;
};
//# sourceMappingURL=extract.js.map