isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
404 lines (403 loc) • 20.7 kB
JavaScript
;
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.`);
}
}
}