You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tobspr_shapez.io/src/js/savegame/savegame.js

292 lines
8.2 KiB

import { ReadWriteProxy } from "../core/read_write_proxy";
import { ExplainedResult } from "../core/explained_result";
import { SavegameSerializer } from "./savegame_serializer";
import { BaseSavegameInterface } from "./savegame_interface";
import { createLogger } from "../core/logging";
import { globalConfig } from "../core/config";
import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry";
import { SavegameInterface_V1001 } from "./schemas/1001";
import { SavegameInterface_V1002 } from "./schemas/1002";
import { SavegameInterface_V1003 } from "./schemas/1003";
import { SavegameInterface_V1004 } from "./schemas/1004";
import { SavegameInterface_V1005 } from "./schemas/1005";
import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008";
const logger = createLogger("savegame");
/**
* @typedef {import("../application").Application} Application
* @typedef {import("../game/root").GameRoot} GameRoot
* @typedef {import("./savegame_typedefs").SavegameData} SavegameData
* @typedef {import("./savegame_typedefs").SavegameMetadata} SavegameMetadata
* @typedef {import("./savegame_typedefs").SavegameStats} SavegameStats
* @typedef {import("./savegame_typedefs").SerializedGame} SerializedGame
*/
export class Savegame extends ReadWriteProxy {
/**
*
* @param {Application} app
* @param {object} param0
* @param {string} param0.internalId
* @param {SavegameMetadata} param0.metaDataRef Handle to the meta data
*/
constructor(app, { internalId, metaDataRef }) {
super(app, "savegame-" + internalId + ".bin");
this.internalId = internalId;
this.metaDataRef = metaDataRef;
/** @type {SavegameData} */
this.currentData = this.getDefaultData();
assert(
savegameInterfaces[Savegame.getCurrentVersion()],
"Savegame interface not defined: " + Savegame.getCurrentVersion()
);
}
//////// RW Proxy Impl //////////
/**
* @returns {number}
*/
static getCurrentVersion() {
return 1008;
}
/**
* @returns {typeof BaseSavegameInterface}
*/
static getReaderClass() {
return savegameInterfaces[Savegame.getCurrentVersion()];
}
/**
* @returns {number}
*/
getCurrentVersion() {
return /** @type {typeof Savegame} */ (this.constructor).getCurrentVersion();
}
/**
* Returns the savegames default data
* @returns {SavegameData}
*/
getDefaultData() {
return {
version: this.getCurrentVersion(),
dump: null,
stats: {},
lastUpdate: Date.now(),
};
}
/**
* Migrates the savegames data
* @param {SavegameData} data
*/
migrate(data) {
if (data.version < 1000) {
return ExplainedResult.bad("Can not migrate savegame, too old");
}
if (data.version === 1000) {
SavegameInterface_V1001.migrate1000to1001(data);
data.version = 1001;
}
if (data.version === 1001) {
SavegameInterface_V1002.migrate1001to1002(data);
data.version = 1002;
}
if (data.version === 1002) {
SavegameInterface_V1003.migrate1002to1003(data);
data.version = 1003;
}
if (data.version === 1003) {
SavegameInterface_V1004.migrate1003to1004(data);
data.version = 1004;
}
if (data.version === 1004) {
SavegameInterface_V1005.migrate1004to1005(data);
data.version = 1005;
}
if (data.version === 1005) {
SavegameInterface_V1006.migrate1005to1006(data);
data.version = 1006;
}
if (data.version === 1006) {
SavegameInterface_V1007.migrate1006to1007(data);
data.version = 1007;
}
if (data.version === 1007) {
SavegameInterface_V1008.migrate1007to1008(data);
data.version = 1008;
}
return ExplainedResult.good();
}
/**
* Verifies the savegames data
* @param {SavegameData} data
*/
verify(data) {
if (!data.dump) {
// Well, guess that works
return ExplainedResult.good();
}
if (!this.getDumpReaderForExternalData(data).validate()) {
return ExplainedResult.bad("dump-reader-failed-validation");
}
return ExplainedResult.good();
}
//////// Subclasses interface ////////
/**
* Returns if this game can be saved on disc
* @returns {boolean}
*/
isSaveable() {
return true;
}
/**
* Returns the statistics of the savegame
* @returns {SavegameStats}
*/
getStatistics() {
return this.currentData.stats;
}
/**
* Returns the *real* last update of the savegame, not the one of the metadata
* which could also be the servers one
*/
getRealLastUpdate() {
return this.currentData.lastUpdate;
}
/**
* Returns if this game has a serialized game dump
*/
hasGameDump() {
return !!this.currentData.dump && this.currentData.dump.entities.length > 0;
}
/**
* Returns the current game dump
* @returns {SerializedGame}
*/
getCurrentDump() {
return this.currentData.dump;
}
/**
* Returns a reader to access the data
* @returns {BaseSavegameInterface}
*/
getDumpReader() {
if (!this.currentData.dump) {
logger.warn("Getting reader on null-savegame dump");
}
const cls = /** @type {typeof Savegame} */ (this.constructor).getReaderClass();
return new cls(this.currentData);
}
/**
* Returns a reader to access external data
* @returns {BaseSavegameInterface}
*/
getDumpReaderForExternalData(data) {
assert(data.version, "External data contains no version");
return getSavegameInterface(data);
}
///////// Public Interface ///////////
/**
* Updates the last update field so we can send the savegame to the server,
* WITHOUT Saving!
*/
setLastUpdate(time) {
this.currentData.lastUpdate = time;
}
/**
*
* @param {GameRoot} root
*/
updateData(root) {
// Construct a new serializer
const serializer = new SavegameSerializer();
// let timer = performance.now();
const dump = serializer.generateDumpFromGameRoot(root);
if (!dump) {
return false;
}
const shadowData = Object.assign({}, this.currentData);
shadowData.dump = dump;
shadowData.lastUpdate = new Date().getTime();
shadowData.version = this.getCurrentVersion();
const reader = this.getDumpReaderForExternalData(shadowData);
// Validate (not in prod though)
if (!G_IS_RELEASE) {
const validationResult = reader.validate();
if (!validationResult) {
return false;
}
}
// Save data
this.currentData = shadowData;
}
/**
* Writes the savegame as well as its metadata
*/
writeSavegameAndMetadata() {
return this.writeAsync().then(() => this.saveMetadata());
}
/**
* Updates the savegames metadata
*/
saveMetadata() {
this.metaDataRef.lastUpdate = new Date().getTime();
this.metaDataRef.version = this.getCurrentVersion();
if (!this.hasGameDump()) {
this.metaDataRef.level = 0;
} else {
this.metaDataRef.level = this.currentData.dump.hubGoals.level;
}
return this.app.savegameMgr.writeAsync();
}
/**
* @see ReadWriteProxy.writeAsync
* @returns {Promise<any>}
*/
writeAsync() {
if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
return Promise.resolve();
}
return super.writeAsync();
}
}