UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

384 lines (383 loc) • 18.2 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SaveDataManager = void 0; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const decorators_1 = require("../../../decorators"); const ModCallbackCustom_1 = require("../../../enums/ModCallbackCustom"); const SaveDataKey_1 = require("../../../enums/SaveDataKey"); const SerializationType_1 = require("../../../enums/SerializationType"); const deepCopy_1 = require("../../../functions/deepCopy"); const frames_1 = require("../../../functions/frames"); const log_1 = require("../../../functions/log"); const stage_1 = require("../../../functions/stage"); const tstlClass_1 = require("../../../functions/tstlClass"); const types_1 = require("../../../functions/types"); const utils_1 = require("../../../functions/utils"); const ReadonlySet_1 = require("../../../types/ReadonlySet"); const Feature_1 = require("../../private/Feature"); const glowingHourGlass_1 = require("./saveDataManager/glowingHourGlass"); const loadFromDisk_1 = require("./saveDataManager/loadFromDisk"); const restoreDefaults_1 = require("./saveDataManager/restoreDefaults"); const saveToDisk_1 = require("./saveDataManager/saveToDisk"); const NON_USER_DEFINED_CLASS_NAMES = new ReadonlySet_1.ReadonlySet([ "Map", "Set", "DefaultMap", ]); class SaveDataManager extends Feature_1.Feature { /** * We store a local reference to the mod object so that we can access the corresponding methods * that read and write to the "save#.dat" file. */ mod; /** * The save data map is indexed by subscriber name. We use Lua tables instead of TypeScriptToLua * Maps for the master map so that we can access the variables via the in-game console when * debugging. (TSTL Maps don't expose the map keys as normal keys.) */ saveDataMap = new LuaMap(); /** * When mod feature data is initialized, we copy the initial values into a separate map so that we * can restore them later on. */ saveDataDefaultsMap = new LuaMap(); /** * Each mod feature can optionally provide a function that can control whether the save data is * written to disk. */ saveDataConditionalFuncMap = new LuaMap(); /** * We backup some save data keys on every new room for the purposes of restoring it when Glowing * Hour Glass is used. * * Note that the save data is backed up in serialized form so that we can use the `merge` function * to restore it. */ saveDataGlowingHourGlassMap = new LuaMap(); /** * End-users can register their classes with the save data manager for proper serialization when * contained in nested maps, sets, and arrays. */ classConstructors = new LuaMap(); // Other variables inARun = false; restoreGlowingHourGlassDataOnNextRoom = false; /** @internal */ constructor(mod) { super(); this.callbacksUsed = [ // 3 [ isaac_typescript_definitions_1.ModCallback.POST_USE_ITEM, this.postUseItemGlowingHourGlass, [isaac_typescript_definitions_1.CollectibleType.GLOWING_HOUR_GLASS], ], // 9 [isaac_typescript_definitions_1.ModCallback.POST_PLAYER_INIT, this.postPlayerInit], // 17 [isaac_typescript_definitions_1.ModCallback.PRE_GAME_EXIT, this.preGameExit], // 18 // We want to avoid a needless dependency on the `GameReorderedCallbacks` feature. // eslint-disable-next-line @typescript-eslint/no-deprecated [isaac_typescript_definitions_1.ModCallback.POST_NEW_LEVEL, this.postNewLevel], ]; this.customCallbacksUsed = [ [ModCallbackCustom_1.ModCallbackCustom.POST_NEW_ROOM_EARLY, this.postNewRoomEarly], ]; this.mod = mod; } // ModCallback.POST_USE_ITEM (3) // CollectibleType.GLOWING_HOUR_GLASS (422) postUseItemGlowingHourGlass = (_collectibleType, _rng, _player, _useFlags, _activeSlot, _customVarData) => { this.restoreGlowingHourGlassDataOnNextRoom = true; return undefined; }; // ModCallback.POST_PLAYER_INIT (9) postPlayerInit = (_player) => { // We want to only load data once per run to handle the case of a player using Genesis, a second // player joining the run, and so on. if (this.inARun) { return; } this.inARun = true; // Handle the race-condition of using the Glowing Hourglass and then resetting the run. this.restoreGlowingHourGlassDataOnNextRoom = false; // We want to unconditionally load save data on every new run since there might be persistent // data that is not tied to an individual run. (0, loadFromDisk_1.loadFromDisk)(this.mod, this.saveDataMap, this.classConstructors); const isContinued = (0, frames_1.isAfterGameFrame)(0); if (!isContinued) { (0, restoreDefaults_1.restoreDefaultsForAllFeaturesAndKeys)(this.saveDataMap, this.saveDataDefaultsMap); } // On continued runs, the `POST_NEW_LEVEL` callback will not fire, so we do not have to worry // about saved data based on level getting overwritten. }; // ModCallback.PRE_GAME_EXIT (17) preGameExit = () => { // We unconditionally save variables to disk (because regardless of a save & quit or a death, // persistent variables should be recorded). (0, saveToDisk_1.saveToDisk)(this.mod, this.saveDataMap, this.saveDataConditionalFuncMap); // Mark that we are going to the menu. (Technically, the `POST_ENTITY_REMOVE` callback may fire // before actually going to the menu, but that must be explicitly handled.) this.inARun = false; // At this point, we could blow away the existing save data or restore defaults, but it is not // necessary since we will have to do it again in the `POST_PLAYER_INIT` callback. Furthermore, // the `POST_ENTITY_REMOVE` callback may fire after the `PRE_GAME_EXIT` callback, so wiping data // now could result in bugs for features that depend on that (e.g. `PickupIndexCreation`). }; // ModCallback.POST_NEW_LEVEL (18) postNewLevel = () => { (0, restoreDefaults_1.restoreDefaultsForAllFeaturesKey)(this.saveDataMap, this.saveDataDefaultsMap, SaveDataKey_1.SaveDataKey.LEVEL); // We save data to disk at the beginning of every floor (for the 2nd floor and beyond) to // emulate what the game does internally. (This mitigates data loss in the event of a crash). if (!(0, stage_1.onFirstFloor)()) { (0, saveToDisk_1.saveToDisk)(this.mod, this.saveDataMap, this.saveDataConditionalFuncMap); } }; // ModCallbackCustom.POST_NEW_ROOM_EARLY postNewRoomEarly = () => { (0, restoreDefaults_1.restoreDefaultsForAllFeaturesKey)(this.saveDataMap, this.saveDataDefaultsMap, SaveDataKey_1.SaveDataKey.ROOM); // Handle the Glowing Hourglass. if (this.restoreGlowingHourGlassDataOnNextRoom) { this.restoreGlowingHourGlassDataOnNextRoom = false; (0, glowingHourGlass_1.restoreGlowingHourGlassBackup)(this.saveDataMap, this.saveDataConditionalFuncMap, this.saveDataGlowingHourGlassMap, this.classConstructors); } else { (0, glowingHourGlass_1.makeGlowingHourGlassBackup)(this.saveDataMap, this.saveDataConditionalFuncMap, this.saveDataGlowingHourGlassMap); } }; saveDataManager(key, v, conditionalFunc) { if ((0, tstlClass_1.isTSTLClass)(key)) { const className = (0, tstlClass_1.getTSTLClassName)(key); (0, utils_1.assertDefined)(className, 'Failed to get the class name for the submitted class (as part of the "key" parameter) when registering new data with the save data manager.'); key = className; } if (!(0, types_1.isString)(key)) { error(`The save data manager requires that keys are strings or TSTL classes. You tried to use a key of type: ${typeof key}`); } if (this.saveDataMap.has(key)) { error(`The save data manager is already managing save data for a key of: ${key}`); } // First, recursively look through the new save data for any classes, so we can register them // with the save data manager. this.storeClassConstructorsFromObject(v); // Add the new save data to the map. this.saveDataMap.set(key, v); // Convert the boolean to a function, if necessary. (Having the argument be a boolean is // necessary in order for the overloads to work properly.) if (conditionalFunc === false) { conditionalFunc = () => false; } // If the only key in the save data is "room", then we don't have to worry about saving this // data to disk (because the room would be reloaded upon resuming a continued run). const saveDataKeys = Object.keys(v); if (saveDataKeys.length === 1 && saveDataKeys[0] === "room") { conditionalFunc = () => false; } // Make a copy of the initial save data so that we can use it to restore the default values // later on. const saveDataCopy = (0, deepCopy_1.deepCopy)(v, SerializationType_1.SerializationType.NONE, key); this.saveDataDefaultsMap.set(key, saveDataCopy); // Store the conditional function for later, if present. if (conditionalFunc !== undefined) { this.saveDataConditionalFuncMap.set(key, conditionalFunc); } } /** * Recursively traverses an object, collecting all of the class constructors that it encounters. */ storeClassConstructorsFromObject(luaMap) { const tstlClassName = (0, tstlClass_1.getTSTLClassName)(luaMap); if (tstlClassName !== undefined && !NON_USER_DEFINED_CLASS_NAMES.has(tstlClassName)) { this.classConstructors.set(tstlClassName, luaMap); } for (const [_key, value] of luaMap) { if ((0, types_1.isTable)(value)) { this.storeClassConstructorsFromObject(value); } } } /** * The save data manager will automatically load variables from disk at the appropriate times * (i.e. when a new run is started). Use this function to explicitly force the save data manager * to load all of its variables from disk immediately. * * Obviously, doing this will overwrite the current data, so using this function can potentially * result in lost state. * * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`. * * @public */ saveDataManagerLoad() { (0, loadFromDisk_1.loadFromDisk)(this.mod, this.saveDataMap, this.classConstructors); } /** * The save data manager will automatically save variables to disk at the appropriate times (i.e. * when the run is exited). Use this function to explicitly force the save data manager to write * all of its variables to disk immediately. * * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`. * * @public */ saveDataManagerSave() { (0, saveToDisk_1.saveToDisk)(this.mod, this.saveDataMap, this.saveDataConditionalFuncMap); } /** * Sets the global variable of "g" equal to all of the save data variables for this mod. * * This can make debugging easier, as you can access the variables from the game's debug console. * e.g. `l print(g.feature1.run.foo)` * * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`. * * @public */ saveDataManagerSetGlobal() { g = this.saveDataMap; } /** * By default, the save data manager will not be able to serialize/deserialize classes that are * nested inside of maps, sets, and arrays, because it does not have access to the corresponding * class constructor. If you want to use nested classes in this way, then use this function to * register the class constructor with the save data manager. Once registered, the save data * manager will automatically run the constructor when deserializing (in addition to copying over * the data fields). * * This function is variadic, which means you can pass as many classes as you want to register. * * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`. * * @public */ saveDataManagerRegisterClass(...tstlClasses) { for (const tstlClass of tstlClasses) { const { name } = tstlClass; (0, utils_1.assertDefined)( // Since we are accepting untrusted user input, this might not be a real TSTL class. name, "Failed to register a class with the save data manager due to not being able to derive the name of the class."); this.classConstructors.set(name, tstlClass); } } /** * Removes a previously registered key from the save data manager. This is the opposite of the * "saveDataManager" method. * * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`. * * @public */ saveDataManagerRemove(key) { if (!(0, types_1.isString)(key)) { error(`The save data manager requires that keys are strings. You tried to use a key of type: ${typeof key}`); } if (!this.saveDataMap.has(key)) { error(`The save data manager is not managing save data for a key of: ${key}`); } // Delete the save data from the map. this.saveDataMap.delete(key); this.saveDataDefaultsMap.delete(key); this.saveDataConditionalFuncMap.delete(key); this.saveDataGlowingHourGlassMap.delete(key); } /** * The save data manager will automatically reset variables at the appropriate times, like when a * player enters a new room. Use this function to explicitly force the save data manager to reset * a specific variable group. * * For example: * * ```ts * const v = { * room: { * foo: 123, * }, * }; * * mod.saveDataManager("file1", v); * * // Then, later on, to explicit reset all of the "room" variables: * mod.saveDataManagerReset("file1", "room"); * ``` * * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`. * * @public */ saveDataManagerReset(key, childObjectKey) { if (!(0, types_1.isString)(key)) { error(`The save data manager requires that keys are strings. You tried to use a key of type: ${typeof key}`); } const saveData = this.saveDataMap.get(key); (0, utils_1.assertDefined)(saveData, `The save data manager is not managing save data for a key of: ${key}`); (0, restoreDefaults_1.restoreDefaultForFeatureKey)(this.saveDataDefaultsMap, key, saveData, childObjectKey); } /** * Helper function to check to see if the game is in the menu, as far as the save data manager is * concerned. This function will return true when the game is first opened until the * `POST_PLAYER_INIT` callback fires. It will also return true in between the `PRE_GAME_EXIT` * callback firing and the `POST_PLAYER_INIT` callback firing. * * This function is useful because the `POST_ENTITY_REMOVE` callback fires after the * `PRE_GAME_EXIT` callback. Thus, if save data needs to be updated from the `POST_ENTITY_REMOVE` * callback and the player is in the process of saving and quitting, the feature will have to * explicitly call the `saveDataManagerSave` function. * * In order to use this function, you must upgrade your mod with `ISCFeature.SAVE_DATA_MANAGER`. * * @public */ saveDataManagerInMenu() { return !this.inARun; } /** * Helper function to see all of the mod features that are using the save data manager. Useful for * debugging if a certain mod feature is not getting its data saved correctly. * * @public */ saveDataManagerLogSubscribers() { (0, log_1.log)("List of save data manager subscribers:"); const keys = Object.keys(this.saveDataMap); keys.sort(); for (const key of keys) { (0, log_1.log)(`- ${key}`); } } } exports.SaveDataManager = SaveDataManager; __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManager", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerLoad", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerSave", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerSetGlobal", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerRegisterClass", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerRemove", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerReset", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerInMenu", null); __decorate([ decorators_1.Exported ], SaveDataManager.prototype, "saveDataManagerLogSubscribers", null);