UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

404 lines (403 loc) • 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.deepCopy = deepCopy; const DefaultMap_1 = require("../classes/DefaultMap"); 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 isaacAPIClass_1 = require("./isaacAPIClass"); const log_1 = require("./log"); const serialization_2 = require("./serialization"); const sort_1 = require("./sort"); const tstlClass_1 = require("./tstlClass"); const types_1 = require("./types"); const utils_1 = require("./utils"); function deepCopy(value, serializationType = SerializationType_1.SerializationType.NONE, traversalDescription = "", classConstructors = new LuaMap(), insideMap = false) { if (constants_1.SAVE_DATA_MANAGER_DEBUG) { let logString = `deepCopy is operating on: ${traversalDescription}`; if (serializationType === SerializationType_1.SerializationType.SERIALIZE) { logString += " (serializing)"; } else if (serializationType === SerializationType_1.SerializationType.DESERIALIZE) { logString += " (deserializing)"; } logString += `: ${value}`; (0, log_1.log)(logString); } const valueType = type(value); switch (valueType) { // First, handling the trivial case of primitives. case "nil": case "boolean": case "number": case "string": { return value; } // Second, handle values that cannot be serialized. case "function": case "thread": { if (serializationType === SerializationType_1.SerializationType.SERIALIZE) { error(`The deep copy function does not support serialization of "${traversalDescription}", since it is type: ${valueType}`); } if (serializationType === SerializationType_1.SerializationType.DESERIALIZE) { error(`The deep copy function does not support deserialization of "${traversalDescription}", since it is type: ${valueType}`); } // We cannot copy this, so simply return the reference. return value; } case "table": { const luaMap = value; return deepCopyTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap); } case "userdata": { return deepCopyUserdata(value, serializationType, traversalDescription); } } } function deepCopyTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap) { // First, handle the cases of TSTL classes or serialized TSTL classes. if ((0, tstlClass_1.isDefaultMap)(luaMap) || luaMap.has(SerializationBrand_1.SerializationBrand.DEFAULT_MAP)) { return deepCopyDefaultMap(luaMap, serializationType, traversalDescription, classConstructors, insideMap); } if ((0, tstlClass_1.isTSTLMap)(luaMap) || luaMap.has(SerializationBrand_1.SerializationBrand.MAP)) { return deepCopyMap(luaMap, serializationType, traversalDescription, classConstructors, insideMap); } if ((0, tstlClass_1.isTSTLSet)(luaMap) || luaMap.has(SerializationBrand_1.SerializationBrand.SET)) { return deepCopySet(luaMap, serializationType, traversalDescription, classConstructors, insideMap); } const className = (0, tstlClass_1.getTSTLClassName)(luaMap); if (className === "WeakMap") { error(`The deep copy function does not support copying the "WeakMap" class for: ${traversalDescription}`); } if (className === "WeakSet") { error(`The deep copy function does not support copying the "WeakSet" class for: ${traversalDescription}`); } if (className !== undefined || luaMap.has(SerializationBrand_1.SerializationBrand.TSTL_CLASS)) { return deepCopyTSTLClass(luaMap, serializationType, traversalDescription, classConstructors, insideMap); } // This is not a TSTL Map/Set/class. If it has a metatable, abort. checkMetatable(luaMap, traversalDescription); // Handle the special case of serialized Isaac API classes. if ((0, serialization_2.isSerializedIsaacAPIClass)(luaMap) && serializationType === SerializationType_1.SerializationType.DESERIALIZE) { return (0, serialization_2.deserializeIsaacAPIClass)(luaMap); } // Handle the special case of an array. if ((0, array_1.isArray)(luaMap)) { return deepCopyArray(luaMap, serializationType, traversalDescription, classConstructors, insideMap); } // Base case: copy a normal Lua table return deepCopyNormalLuaTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap); } function deepCopyDefaultMap(defaultMap, serializationType, traversalDescription, classConstructors, insideMap) { if (constants_1.SAVE_DATA_MANAGER_DEBUG) { (0, log_1.log)("deepCopy is copying a DefaultMap."); } const constructorArg = (0, tstlClass_1.isDefaultMap)(defaultMap) ? defaultMap.getConstructorArg() : undefined; // The undefined case is handled explicitly in the "getNewDefaultMap" function. // First, handle the special case of serializing a DefaultMap instantiated with a factory // function. If this is the case, then we cannot serialize it (because there is no way to // serialize a function). if (serializationType === SerializationType_1.SerializationType.SERIALIZE && !(0, types_1.isPrimitive)(constructorArg)) { if (insideMap) { // The case of a DefaultMap within another map is complicated. Unlike a DefaultMap attached to // a "normal" object, the `merge` function will have no reference to the factory function that // was used to instantiate it. Thus, there is no way to copy this object. In this case, we // throw a run-time error to immediately alert the end-user that their data structure is // invalid. error("Failed to deep copy a DefaultMap because it was instantiated with a factory function and was also inside of an array, map, or set. For more information, see: https://isaacscript.github.io/main/gotchas#failed-to-deep-copy-a-defaultmap"); } else { // In most cases, the DefaultMap will be attached to a normal table element. In this case, if // we serialize it as a normal `Map`, then everything will work out fine, because the `merge` // function only needs to copy the values (and not instantiate the object itself). return deepCopyMap(defaultMap, serializationType, traversalDescription, classConstructors, insideMap); } } const newDefaultMap = getNewDefaultMap(defaultMap, serializationType, traversalDescription, constructorArg); insideMap = true; const { entries, convertedNumberKeysToStrings } = getCopiedEntries(defaultMap, serializationType, traversalDescription, classConstructors, insideMap); if (convertedNumberKeysToStrings) { // Differentiating between the two types looks superfluous but is necessary for TSTL to produce // the proper set method call. if ((0, tstlClass_1.isDefaultMap)(newDefaultMap)) { newDefaultMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, ""); } else { newDefaultMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, ""); } } for (const [key, value] of entries) { // Differentiating between the two types looks superfluous but is necessary for TSTL to produce // the proper set method call. if ((0, tstlClass_1.isDefaultMap)(newDefaultMap)) { newDefaultMap.set(key, value); } else { newDefaultMap.set(key, value); } } return newDefaultMap; } /** * The new copied default map with either be a TSTL `DefaultMap` class or a Lua table, depending on * whether we are serializing. */ function getNewDefaultMap(defaultMap, serializationType, traversalDescription, constructorArg) { switch (serializationType) { case SerializationType_1.SerializationType.NONE: { // eslint-disable-next-line isaacscript/no-invalid-default-map return new DefaultMap_1.DefaultMap(constructorArg); } case SerializationType_1.SerializationType.SERIALIZE: { // Since we are serializing, the new object will be a Lua table. (At this point, we already // handled the special case of a DefaultMap instantiated with a factory function.) const newDefaultMap = new LuaMap(); newDefaultMap.set(SerializationBrand_1.SerializationBrand.DEFAULT_MAP, ""); newDefaultMap.set(SerializationBrand_1.SerializationBrand.DEFAULT_MAP_VALUE, constructorArg); return newDefaultMap; } case SerializationType_1.SerializationType.DESERIALIZE: { if ((0, tstlClass_1.isDefaultMap)(defaultMap)) { error(`Failed to deserialize a default map of "${traversalDescription}", since it was not a Lua table.`); } const defaultMapValue = defaultMap.get(SerializationBrand_1.SerializationBrand.DEFAULT_MAP_VALUE); (0, utils_1.assertDefined)(defaultMapValue, `Failed to deserialize a default map of "${traversalDescription}", since there was no serialization brand of: ${SerializationBrand_1.SerializationBrand.DEFAULT_MAP_VALUE}`); // eslint-disable-next-line isaacscript/no-invalid-default-map return new DefaultMap_1.DefaultMap(defaultMapValue); } } } function deepCopyMap(map, serializationType, traversalDescription, classConstructors, insideMap) { if (constants_1.SAVE_DATA_MANAGER_DEBUG) { (0, log_1.log)("deepCopy is copying a Map."); } let newMap; if (serializationType === SerializationType_1.SerializationType.SERIALIZE) { // Since we are serializing, the new object will be a Lua table. newMap = new LuaMap(); newMap.set(SerializationBrand_1.SerializationBrand.MAP, ""); } else { newMap = new Map(); } insideMap = true; const { entries, convertedNumberKeysToStrings } = getCopiedEntries(map, serializationType, traversalDescription, classConstructors, insideMap); if (convertedNumberKeysToStrings) { // Differentiating between the two types looks superfluous but is necessary for TSTL to produce // the proper set method call. if ((0, tstlClass_1.isTSTLMap)(newMap)) { newMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, ""); } else { newMap.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, ""); } } for (const [key, value] of entries) { // Differentiating between the two types looks superfluous but is necessary for TSTL to produce // the proper set method call. if ((0, tstlClass_1.isTSTLMap)(newMap)) { newMap.set(key, value); } else { newMap.set(key, value); } } return newMap; } function deepCopySet(set, serializationType, traversalDescription, classConstructors, insideMap) { if (constants_1.SAVE_DATA_MANAGER_DEBUG) { (0, log_1.log)("deepCopy is copying a Set."); } let newSet; if (serializationType === SerializationType_1.SerializationType.SERIALIZE) { // For serialization purposes, we represent a `Set` as a table with keys that match the // keys/values in the Set and values of an empty string. newSet = new LuaMap(); newSet.set(SerializationBrand_1.SerializationBrand.SET, ""); } else { newSet = new Set(); } const { entries, convertedNumberKeysToStrings } = getCopiedEntries(set, serializationType, traversalDescription, classConstructors, insideMap); if (convertedNumberKeysToStrings) { // Differentiating between the two types looks superfluous but is necessary for TSTL to produce // the proper set method call. if ((0, tstlClass_1.isTSTLSet)(newSet)) { // We should never be serializing an object of type `Set`. error("The deep copy function cannot convert number keys to strings for a Set."); } else { newSet.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, ""); } } for (const [key] of entries) { // Differentiating between the two types looks superfluous but is necessary for TSTL to produce // the proper set method call. if ((0, tstlClass_1.isTSTLSet)(newSet)) { newSet.add(key); } else { newSet.set(key, ""); } } return newSet; } function deepCopyTSTLClass(tstlClass, serializationType, traversalDescription, classConstructors, insideMap) { if (constants_1.SAVE_DATA_MANAGER_DEBUG) { (0, log_1.log)("deepCopy is copying a TSTL class."); } let newClass; switch (serializationType) { case SerializationType_1.SerializationType.NONE: { // We can use the class constructor from the old class. newClass = (0, tstlClass_1.newTSTLClass)(tstlClass); break; } case SerializationType_1.SerializationType.SERIALIZE: { newClass = new LuaMap(); // We brand it with the name of the class so that we can run the corresponding constructor // during deserialization. const tstlClassName = (0, tstlClass_1.getTSTLClassName)(tstlClass); if (tstlClassName !== undefined) { newClass.set(SerializationBrand_1.SerializationBrand.TSTL_CLASS, tstlClassName); } break; } case SerializationType_1.SerializationType.DESERIALIZE: { const tstlClassName = tstlClass.get(SerializationBrand_1.SerializationBrand.TSTL_CLASS); (0, utils_1.assertDefined)(tstlClassName, "Failed to deserialize a TSTL class since the brand did not contain the class name."); const classConstructor = classConstructors.get(tstlClassName); (0, utils_1.assertDefined)(classConstructor, `Failed to deserialize a TSTL class since there was no constructor registered for a class name of "${tstlClassName}". If this mod is using the save data manager, it must register the class constructor with the "saveDataManagerRegisterClass" method.`); // eslint-disable-next-line new-cap newClass = new classConstructor(); break; } } const { entries, convertedNumberKeysToStrings } = getCopiedEntries(tstlClass, serializationType, traversalDescription, classConstructors, insideMap); if (convertedNumberKeysToStrings) { newClass.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, ""); } for (const [key, value] of entries) { newClass.set(key, value); } return newClass; } function deepCopyArray(array, serializationType, traversalDescription, classConstructors, insideMap) { if (constants_1.SAVE_DATA_MANAGER_DEBUG) { (0, log_1.log)("deepCopy is copying an array."); } const newArray = []; for (const value of array) { const newValue = deepCopy(value, serializationType, traversalDescription, classConstructors, insideMap); newArray.push(newValue); } return newArray; } function deepCopyNormalLuaTable(luaMap, serializationType, traversalDescription, classConstructors, insideMap) { if (constants_1.SAVE_DATA_MANAGER_DEBUG) { (0, log_1.log)("deepCopy is copying a normal Lua table."); } const newTable = new LuaMap(); const { entries, convertedNumberKeysToStrings } = getCopiedEntries(luaMap, serializationType, traversalDescription, classConstructors, insideMap); if (convertedNumberKeysToStrings) { newTable.set(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS, ""); } for (const [key, value] of entries) { newTable.set(key, value); } return newTable; } /** * Recursively clones the object's entries, automatically converting number keys to strings, if * necessary. * * This should work on objects/tables, maps, sets, default maps, and classes. */ function getCopiedEntries(object, serializationType, traversalDescription, classConstructors, insideMap) { // First, shallow copy the entries. We cannot use "pairs" to iterate over a `Map` or `Set`. We // cannot use "[...pairs(object)]", as it results in a run-time error. const entries = []; if ((0, tstlClass_1.isTSTLMap)(object) || (0, tstlClass_1.isTSTLSet)(object) || (0, tstlClass_1.isDefaultMap)(object)) { for (const [key, value] of object.entries()) { entries.push([key, value]); } } else { for (const [key, value] of pairs(object)) { entries.push([key, value]); } } if (constants_1.SAVE_DATA_MANAGER_DEBUG) { entries.sort(sort_1.sortTwoDimensionalArray); } // 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 = serializationType === SerializationType_1.SerializationType.DESERIALIZE && entries.some(([key]) => key === (0, types_1.asString)(SerializationBrand_1.SerializationBrand.OBJECT_WITH_NUMBER_KEYS)); const hasNumberKeys = entries.some(([key]) => (0, types_1.isNumber)(key)); const convertNumberKeysToStrings = serializationType === SerializationType_1.SerializationType.SERIALIZE && hasNumberKeys; // Second, deep copy the entries. const copiedEntries = []; for (const [key, value] of entries) { // When deserializing, we do not need to copy the serialization brands that are used to denote // the object type. if ((0, serialization_1.isSerializationBrand)(key)) { continue; } traversalDescription = (0, utils_1.getTraversalDescription)(key, traversalDescription); const newValue = deepCopy(value, serializationType, traversalDescription, classConstructors, insideMap); let keyToUse = key; if (convertStringKeysToNumbers) { const numberKey = tonumber(key); if (numberKey !== undefined) { keyToUse = numberKey; } } if (convertNumberKeysToStrings) { keyToUse = tostring(key); } copiedEntries.push([keyToUse, newValue]); } return { entries: copiedEntries, convertedNumberKeysToStrings: convertNumberKeysToStrings, }; } /** * Lua tables can have metatables, which make writing a generic deep cloner impossible. The deep * copy function will refuse to copy a table type that has a metatable, outside of specifically * supported TSTL objects. */ function checkMetatable(luaMap, traversalDescription) { const metatable = getmetatable(luaMap); if (metatable === undefined) { return; } const tableDescription = traversalDescription === "" ? "the table to copy" : `"${traversalDescription}"`; error(`The deepCopy function detected that ${tableDescription} has a metatable. Copying tables with metatables is not supported, unless they are explicitly handled by the save data manager. (e.g. TypeScriptToLua Maps, TypeScriptToLua Sets, etc.)`); } /** Isaac API classes are of type "userdata". End-user code cannot create userdata. */ function deepCopyUserdata(value, serializationType, traversalDescription) { if (!(0, serialization_2.isCopyableIsaacAPIClass)(value)) { const className = (0, isaacAPIClass_1.getIsaacAPIClassName)(value) ?? "Unknown"; error(`The deep copy function does not support serializing "${traversalDescription}", since it is an Isaac API class of type: ${className}`); } switch (serializationType) { case SerializationType_1.SerializationType.NONE: { return (0, serialization_2.copyIsaacAPIClass)(value); } case SerializationType_1.SerializationType.SERIALIZE: { return (0, serialization_2.serializeIsaacAPIClass)(value); } case SerializationType_1.SerializationType.DESERIALIZE: { return error(`The deep copy function can not deserialize "${traversalDescription}", since it is userdata.`); } } }