UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

159 lines (158 loc) • 8.67 kB
"use strict"; 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); }