isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
159 lines (158 loc) • 8.67 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.merge = merge;
const constants_1 = require("../classes/features/other/saveDataManager/constants");
const SerializationBrand_1 = require("../enums/private/SerializationBrand");
const SerializationType_1 = require("../enums/SerializationType");
const serialization_1 = require("../serialization");
const array_1 = require("./array");
const deepCopy_1 = require("./deepCopy");
const log_1 = require("./log");
const serialization_2 = require("./serialization");
const table_1 = require("./table");
const tstlClass_1 = require("./tstlClass");
const types_1 = require("./types");
const utils_1 = require("./utils");
/**
* `merge` takes the values from a new table and recursively merges them into an old object (while
* performing appropriate deserialization).
*
* This function is used to merge incoming data from the "save#.dat" file into a mod's variables.
* Merging is useful instead of blowing away a table entirely because mod code often relies on the
* local table/object references.
*
* This function always assumes that the new table is serialized data and will attempt to perform
* deserialization on the objects within. In other words, unlike the `deepCopy` function, the
* `merge` function will always operates in the mode of `SerializationType.DESERIALIZE`. For the
* types of objects that will be deserialized, see the documentation for the `deepCopy` function.
*
* This function does not iterate over the old object, like you would naively expect. This is
* because it is common for a variable to have a type of `something | null`. If this is the case,
* the key would not appear when iterating over the old object (because a value of null transpiles
* to nil, which means the table key does not exist). Thus, we must instead iterate over the new
* object and copy the values backwards. The consequence of this is that `merge` can copy over old
* variables that are no longer used in the code, or copy over old variables of a different type,
* which can cause run-time errors. In such cases, users will have to manually delete their save
* data.
*
* @param oldObject The old object to merge the values into. This can be either a Lua table, a TSTL
* map, or a TSTL set.
* @param newTable The new table to merge the values from. This must be a Lua table that represents
* serialized data. In other words, it should be created with the `deepCopy`
* function using `SerializationType.SERIALIZE`.
* @param traversalDescription Used to track the current key that we are operating on for debugging
* purposes. Use a name that corresponds to the name of the merging
* table.
* @param classConstructors Optional. A Lua table that maps the name of a user-defined TSTL class to
* its corresponding constructor. If the `deepCopy` function finds any
* user-defined TSTL classes when recursively iterating through the given
* object, it will use this map to instantiate a new class. Default is an
* empty Lua table.
*/
function merge(oldObject, newTable, traversalDescription, classConstructors = new LuaMap()) {
if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
(0, log_1.log)(`merge is traversing: ${traversalDescription}`);
}
if (!(0, types_1.isTable)(oldObject)) {
error("The first argument given to the merge function is not a table.");
}
if (!(0, types_1.isTable)(newTable)) {
error("The second argument given to the merge function is not a table.");
}
// First, handle the special case of an array with a shallow copy.
if ((0, array_1.isArray)(oldObject) && (0, array_1.isArray)(newTable)) {
mergeSerializedArray(oldObject, newTable, traversalDescription, classConstructors);
return;
}
// Depending on whether we are working on a Lua table or a TypeScriptToLua object, we need to
// iterate in a specific way.
if ((0, tstlClass_1.isTSTLMap)(oldObject) || (0, tstlClass_1.isTSTLSet)(oldObject) || (0, tstlClass_1.isDefaultMap)(oldObject)) {
mergeSerializedTSTLObject(oldObject, newTable, traversalDescription, classConstructors);
}
else {
mergeSerializedTable(oldObject, newTable, traversalDescription, classConstructors);
}
}
function mergeSerializedArray(oldArray, newArray, traversalDescription, classConstructors) {
if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
(0, log_1.log)(`merge encountered an array: ${traversalDescription}`);
}
// Assume that we should blow away all array values with whatever is present in the incoming
// array.
(0, table_1.clearTable)(oldArray);
(0, table_1.iterateTableInOrder)(newArray, (key, value) => {
const deserializedValue = (0, deepCopy_1.deepCopy)(value, SerializationType_1.SerializationType.DESERIALIZE, traversalDescription, classConstructors);
oldArray.set(key, deserializedValue);
}, constants_1.SAVE_DATA_MANAGER_DEBUG);
}
function mergeSerializedTSTLObject(
// eslint-disable-next-line complete/prefer-readonly-parameter-types
oldObject, newTable, traversalDescription, classConstructors) {
if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
(0, log_1.log)(`merge encountered a TSTL object: ${traversalDescription}`);
}
// We blow away the old object and recursively copy over all of the incoming values.
oldObject.clear();
// During serialization, we brand some Lua tables with a special identifier to signify that it has
// keys that should be deserialized to numbers.
const convertStringKeysToNumbers = newTable.has(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS);
(0, table_1.iterateTableInOrder)(newTable, (key, value) => {
if ((0, serialization_1.isSerializationBrand)(key)) {
return;
}
let keyToUse = key;
if (convertStringKeysToNumbers) {
const numberKey = tonumber(key);
if (numberKey === undefined) {
return;
}
keyToUse = numberKey;
}
if ((0, tstlClass_1.isTSTLMap)(oldObject) || (0, tstlClass_1.isDefaultMap)(oldObject)) {
const deserializedValue = (0, deepCopy_1.deepCopy)(value, SerializationType_1.SerializationType.DESERIALIZE, traversalDescription, classConstructors);
oldObject.set(keyToUse, deserializedValue);
}
else if ((0, tstlClass_1.isTSTLSet)(oldObject)) {
oldObject.add(keyToUse);
}
}, constants_1.SAVE_DATA_MANAGER_DEBUG);
}
function mergeSerializedTable(oldTable, newTable, traversalDescription, classConstructors) {
if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
(0, log_1.log)(`merge encountered a Lua table: ${traversalDescription}`);
}
(0, table_1.iterateTableInOrder)(newTable, (key, value) => {
if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
const valueToPrint = value === "" ? "(empty string)" : `${value}`;
(0, log_1.log)(`merge is merging: ${traversalDescription} --> ${valueToPrint}`);
}
if ((0, serialization_1.isSerializationBrand)(key)) {
return;
}
// Handle the special case of serialized Isaac API classes.
if ((0, serialization_2.isSerializedIsaacAPIClass)(value)) {
if (constants_1.SAVE_DATA_MANAGER_DEBUG) {
(0, log_1.log)("merge found a serialized Isaac API class.");
}
const deserializedObject = (0, serialization_2.deserializeIsaacAPIClass)(value);
oldTable.set(key, deserializedObject);
return;
}
if ((0, types_1.isTable)(value)) {
let oldValue = oldTable.get(key);
if (!(0, types_1.isTable)(oldValue)) {
// The child table does not exist on the old table. However, we still need to copy over
// the new table, because we need to handle data types like `Foo | null`. Thus, set up a
// blank sub-table on the old table, and continue to recursively merge.
oldValue = new LuaMap();
oldTable.set(key, oldValue);
}
traversalDescription = (0, utils_1.getTraversalDescription)(key, traversalDescription);
merge(oldValue, value, traversalDescription, classConstructors);
}
else {
// Base case: copy the value
oldTable.set(key, value);
}
}, constants_1.SAVE_DATA_MANAGER_DEBUG);
}