isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
384 lines (383 loc) • 18.2 kB
JavaScript
"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);