diff --git a/project/src/callbacks/GameCallbacks.ts b/project/src/callbacks/GameCallbacks.ts index 3f27b17b..df851bc8 100644 --- a/project/src/callbacks/GameCallbacks.ts +++ b/project/src/callbacks/GameCallbacks.ts @@ -69,12 +69,12 @@ export class GameCallbacks implements OnLoad { * Save profiles on game close * @returns IGameLogoutResponseData */ - public gameLogout( + public async gameLogout( url: string, info: IEmptyRequestData, sessionID: string, - ): IGetBodyResponseData { - this.saveServer.save(); + ): Promise> { + await this.saveServer.saveProfile(sessionID); return this.httpResponse.getBody({ status: "ok" }); } diff --git a/project/src/callbacks/LauncherCallbacks.ts b/project/src/callbacks/LauncherCallbacks.ts index e60cc5f1..48371b4a 100644 --- a/project/src/callbacks/LauncherCallbacks.ts +++ b/project/src/callbacks/LauncherCallbacks.ts @@ -27,8 +27,8 @@ export class LauncherCallbacks { return !output ? "FAILED" : output; } - public register(url: string, info: IRegisterData, sessionID: string): "FAILED" | "OK" { - const output = this.launcherController.register(info); + public async register(url: string, info: IRegisterData, sessionID: string): Promise<"FAILED" | "OK"> { + const output = await this.launcherController.register(info); return !output ? "FAILED" : "OK"; } @@ -60,8 +60,8 @@ export class LauncherCallbacks { return this.httpResponse.noBody("pong!"); } - public removeProfile(url: string, info: IRemoveProfileData, sessionID: string): string { - return this.httpResponse.noBody(this.saveServer.removeProfile(sessionID)); + public async removeProfile(url: string, info: IRemoveProfileData, sessionID: string): Promise { + return this.httpResponse.noBody(await this.saveServer.removeProfile(sessionID)); } public getCompatibleTarkovVersion(): string { diff --git a/project/src/callbacks/ProfileCallbacks.ts b/project/src/callbacks/ProfileCallbacks.ts index 4203dff2..bf892381 100644 --- a/project/src/callbacks/ProfileCallbacks.ts +++ b/project/src/callbacks/ProfileCallbacks.ts @@ -33,13 +33,12 @@ export class ProfileCallbacks { /** * Handle client/game/profile/create */ - public createProfile( + public async createProfile( url: string, info: IProfileCreateRequestData, sessionID: string, - ): IGetBodyResponseData { - const id = this.profileController.createProfile(info, sessionID); - return this.httpResponse.getBody({ uid: id }); + ): Promise> { + return this.httpResponse.getBody({ uid: await this.profileController.createProfile(info, sessionID) }); } /** diff --git a/project/src/callbacks/SaveCallbacks.ts b/project/src/callbacks/SaveCallbacks.ts index e32f1ef9..26cf30f5 100644 --- a/project/src/callbacks/SaveCallbacks.ts +++ b/project/src/callbacks/SaveCallbacks.ts @@ -20,8 +20,8 @@ export class SaveCallbacks implements OnLoad, OnUpdate { } public async onLoad(): Promise { - this.backupService.init(); - this.saveServer.load(); + await this.backupService.init(); + await this.saveServer.load(); } public getRoute(): string { @@ -31,7 +31,7 @@ export class SaveCallbacks implements OnLoad, OnUpdate { public async onUpdate(secondsSinceLastRun: number): Promise { // run every 15 seconds if (secondsSinceLastRun > this.coreConfig.profileSaveIntervalSeconds) { - this.saveServer.save(); + await this.saveServer.save(); return true; } return false; diff --git a/project/src/controllers/LauncherController.ts b/project/src/controllers/LauncherController.ts index 84f44a28..98b2f59a 100644 --- a/project/src/controllers/LauncherController.ts +++ b/project/src/controllers/LauncherController.ts @@ -88,17 +88,17 @@ export class LauncherController { return ""; } - public register(info: IRegisterData): string { + public async register(info: IRegisterData): Promise { for (const sessionID in this.saveServer.getProfiles()) { if (info.username === this.saveServer.getProfile(sessionID).info.username) { return ""; } } - return this.createAccount(info); + return await this.createAccount(info); } - protected createAccount(info: IRegisterData): string { + protected async createAccount(info: IRegisterData): Promise { const profileId = this.generateProfileId(); const scavId = this.generateProfileId(); const newProfileDetails: Info = { @@ -112,8 +112,8 @@ export class LauncherController { }; this.saveServer.createProfile(newProfileDetails); - this.saveServer.loadProfile(profileId); - this.saveServer.saveProfile(profileId); + await this.saveServer.loadProfile(profileId); + await this.saveServer.saveProfile(profileId); return profileId; } diff --git a/project/src/controllers/ProfileController.ts b/project/src/controllers/ProfileController.ts index 8b6b539d..efd88399 100644 --- a/project/src/controllers/ProfileController.ts +++ b/project/src/controllers/ProfileController.ts @@ -99,8 +99,8 @@ export class ProfileController { * @param sessionID Player id * @returns Profiles _id value */ - public createProfile(info: IProfileCreateRequestData, sessionID: string): string { - return this.createProfileService.createProfile(sessionID, info); + public async createProfile(info: IProfileCreateRequestData, sessionID: string): Promise { + return await this.createProfileService.createProfile(sessionID, info); } /** diff --git a/project/src/di/Router.ts b/project/src/di/Router.ts index 573d92b1..b2e43b5e 100644 --- a/project/src/di/Router.ts +++ b/project/src/di/Router.ts @@ -75,7 +75,7 @@ export class ItemEventRouterDefinition extends Router { } export class SaveLoadRouter extends Router { - public handleLoad(profile: ISptProfile): ISptProfile { + public async handleLoad(profile: ISptProfile): Promise { throw new Error("This method needs to be overrode by the router classes"); } } diff --git a/project/src/routers/save_load/HealthSaveLoadRouter.ts b/project/src/routers/save_load/HealthSaveLoadRouter.ts index 5a3562d1..7c136b35 100644 --- a/project/src/routers/save_load/HealthSaveLoadRouter.ts +++ b/project/src/routers/save_load/HealthSaveLoadRouter.ts @@ -8,7 +8,7 @@ export class HealthSaveLoadRouter extends SaveLoadRouter { return [new HandledRoute("spt-health", false)]; } - public override handleLoad(profile: ISptProfile): ISptProfile { + public override async handleLoad(profile: ISptProfile): Promise { if (!profile.vitality) { // Occurs on newly created profiles profile.vitality = { health: undefined, effects: undefined }; diff --git a/project/src/routers/save_load/InraidSaveLoadRouter.ts b/project/src/routers/save_load/InraidSaveLoadRouter.ts index fb50166f..1f144990 100644 --- a/project/src/routers/save_load/InraidSaveLoadRouter.ts +++ b/project/src/routers/save_load/InraidSaveLoadRouter.ts @@ -8,7 +8,7 @@ export class InraidSaveLoadRouter extends SaveLoadRouter { return [new HandledRoute("spt-inraid", false)]; } - public override handleLoad(profile: ISptProfile): ISptProfile { + public override async handleLoad(profile: ISptProfile): Promise { if (profile.inraid === undefined) { profile.inraid = { location: "none", character: "none" }; } diff --git a/project/src/routers/save_load/InsuranceSaveLoadRouter.ts b/project/src/routers/save_load/InsuranceSaveLoadRouter.ts index 3917e4fc..63463665 100644 --- a/project/src/routers/save_load/InsuranceSaveLoadRouter.ts +++ b/project/src/routers/save_load/InsuranceSaveLoadRouter.ts @@ -8,7 +8,7 @@ export class InsuranceSaveLoadRouter extends SaveLoadRouter { return [new HandledRoute("spt-insurance", false)]; } - public override handleLoad(profile: ISptProfile): ISptProfile { + public override async handleLoad(profile: ISptProfile): Promise { if (profile.insurance === undefined) { profile.insurance = []; } diff --git a/project/src/routers/save_load/ProfileSaveLoadRouter.ts b/project/src/routers/save_load/ProfileSaveLoadRouter.ts index 074d082b..823ff8c6 100644 --- a/project/src/routers/save_load/ProfileSaveLoadRouter.ts +++ b/project/src/routers/save_load/ProfileSaveLoadRouter.ts @@ -9,7 +9,7 @@ export class ProfileSaveLoadRouter extends SaveLoadRouter { return [new HandledRoute("spt-profile", false)]; } - public override handleLoad(profile: ISptProfile): ISptProfile { + public override async handleLoad(profile: ISptProfile): Promise { if (!profile.characters) { profile.characters = { pmc: {} as IPmcData, scav: {} as IPmcData }; } diff --git a/project/src/routers/static/GameStaticRouter.ts b/project/src/routers/static/GameStaticRouter.ts index 39757275..16a97fb9 100644 --- a/project/src/routers/static/GameStaticRouter.ts +++ b/project/src/routers/static/GameStaticRouter.ts @@ -92,7 +92,7 @@ export class GameStaticRouter extends StaticRouter { sessionID: string, output: string, ): Promise> => { - return this.gameCallbacks.gameLogout(url, info, sessionID); + return await this.gameCallbacks.gameLogout(url, info, sessionID); }, ), new RouteAction( diff --git a/project/src/routers/static/LauncherStaticRouter.ts b/project/src/routers/static/LauncherStaticRouter.ts index 5c9997ee..40261881 100644 --- a/project/src/routers/static/LauncherStaticRouter.ts +++ b/project/src/routers/static/LauncherStaticRouter.ts @@ -27,7 +27,7 @@ export class LauncherStaticRouter extends StaticRouter { new RouteAction( "/launcher/profile/register", async (url: string, info: any, sessionID: string, output: string): Promise => { - return this.launcherCallbacks.register(url, info, sessionID); + return await this.launcherCallbacks.register(url, info, sessionID); }, ), new RouteAction( @@ -57,7 +57,7 @@ export class LauncherStaticRouter extends StaticRouter { new RouteAction( "/launcher/profile/remove", async (url: string, info: any, sessionID: string, output: string): Promise => { - return this.launcherCallbacks.removeProfile(url, info, sessionID); + return await this.launcherCallbacks.removeProfile(url, info, sessionID); }, ), new RouteAction( diff --git a/project/src/servers/SaveServer.ts b/project/src/servers/SaveServer.ts index 6915646b..b6364bad 100644 --- a/project/src/servers/SaveServer.ts +++ b/project/src/servers/SaveServer.ts @@ -5,7 +5,7 @@ import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig"; import type { ILogger } from "@spt/models/spt/utils/ILogger"; import { ConfigServer } from "@spt/servers/ConfigServer"; import { LocalisationService } from "@spt/services/LocalisationService"; -import { FileSystemSync } from "@spt/utils/FileSystemSync"; +import { FileSystem } from "@spt/utils/FileSystem"; import { HashUtil } from "@spt/utils/HashUtil"; import { JsonUtil } from "@spt/utils/JsonUtil"; import { Timer } from "@spt/utils/Timer"; @@ -14,13 +14,12 @@ import { inject, injectAll, injectable } from "tsyringe"; @injectable() export class SaveServer { protected profileFilepath = "user/profiles/"; - protected profiles = {}; - // onLoad = require("../bindings/SaveLoad"); - protected onBeforeSaveCallbacks = {}; - protected saveMd5 = {}; + protected profiles: Map = new Map(); + protected onBeforeSaveCallbacks: Map Promise> = new Map(); + protected saveSHA1: { [key: string]: string } = {}; constructor( - @inject("FileSystemSync") protected fileSystemSync: FileSystemSync, + @inject("FileSystem") protected fileSystem: FileSystem, @injectAll("SaveLoadRouter") protected saveLoadRouters: SaveLoadRouter[], @inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("HashUtil") protected hashUtil: HashUtil, @@ -34,8 +33,8 @@ export class SaveServer { * @param id Id for save callback * @param callback Callback to execute prior to running SaveServer.saveProfile() */ - public addBeforeSaveCallback(id: string, callback: (profile: Partial) => Partial): void { - this.onBeforeSaveCallbacks[id] = callback; + public addBeforeSaveCallback(id: string, callback: (profile: ISptProfile) => Promise): void { + this.onBeforeSaveCallbacks.set(id, callback); } /** @@ -43,22 +42,23 @@ export class SaveServer { * @param id Id of callback to remove */ public removeBeforeSaveCallback(id: string): void { - this.onBeforeSaveCallbacks[id] = undefined; + this.onBeforeSaveCallbacks.delete(id); } /** * Load all profiles in /user/profiles folder into memory (this.profiles) + * @returns A promise that resolves when loading all profiles is completed. */ - public load(): void { - this.fileSystemSync.ensureDir(this.profileFilepath); + public async load(): Promise { + await this.fileSystem.ensureDir(this.profileFilepath); // get files to load - const files = this.fileSystemSync.getFiles(this.profileFilepath, false, ["json"]); + const files = await this.fileSystem.getFiles(this.profileFilepath, false, ["json"]); // load profiles const timer = new Timer(); for (const file of files) { - this.loadProfile(FileSystemSync.getFileName(file)); + await this.loadProfile(FileSystem.getFileName(file)); } this.logger.debug( `Loading ${files.length} profile${files.length > 1 ? "s" : ""} took ${timer.getTime("ms")}ms`, @@ -67,13 +67,14 @@ export class SaveServer { /** * Save changes for each profile from memory into user/profiles json + * @returns A promise that resolves when saving all profiles is completed. */ - public save(): void { + public async save(): Promise { const timer = new Timer(); for (const sessionID in this.profiles) { - this.saveProfile(sessionID); + await this.saveProfile(sessionID); } - const profileCount = Object.keys(this.profiles).length; + const profileCount = this.profiles.size; this.logger.debug( `Saving ${profileCount} profile${profileCount > 1 ? "s" : ""} took ${timer.getTime("ms")}ms`, false, @@ -94,33 +95,35 @@ export class SaveServer { throw new Error(`no profiles found in saveServer with id: ${sessionId}`); } - if (!this.profiles[sessionId]) { + const profile = this.profiles.get(sessionId); + + if (!profile) { throw new Error(`no profile found for sessionId: ${sessionId}`); } - return this.profiles[sessionId]; + return profile; } public profileExists(id: string): boolean { - return !!this.profiles[id]; + return !!this.profiles.get(id); } /** - * Get all profiles from memory + * Gets all profiles from memory * @returns Dictionary of ISptProfile */ public getProfiles(): Record { - return this.profiles; + return Object.fromEntries(this.profiles); } /** - * Delete a profile by id + * Delete a profile by id (Does not remove the profile file!) * @param sessionID Id of profile to remove * @returns true when deleted, false when profile not found */ public deleteProfileById(sessionID: string): boolean { - if (this.profiles[sessionID]) { - delete this.profiles[sessionID]; + if (this.profiles.get(sessionID)) { + this.profiles.delete(sessionID); return true; } @@ -132,11 +135,14 @@ export class SaveServer { * @param profileInfo Basic profile data */ public createProfile(profileInfo: Info): void { - if (this.profiles[profileInfo.id]) { + if (this.profiles.get(profileInfo.id)) { throw new Error(`profile already exists for sessionId: ${profileInfo.id}`); } - this.profiles[profileInfo.id] = { info: profileInfo, characters: { pmc: {}, scav: {} } }; + this.profiles.set(profileInfo.id, { + info: profileInfo, + characters: { pmc: {}, scav: {} }, + } as ISptProfile); // Cast to ISptProfile so the errors of having empty pmc and scav data disappear } /** @@ -144,25 +150,26 @@ export class SaveServer { * @param profileDetails Profile to save */ public addProfile(profileDetails: ISptProfile): void { - this.profiles[profileDetails.info.id] = profileDetails; + this.profiles.set(profileDetails.info.id, profileDetails); } /** * Look up profile json in user/profiles by id and store in memory * Execute saveLoadRouters callbacks after being loaded into memory * @param sessionID Id of profile to store in memory + * @returns A promise that resolves when loading is completed. */ - public loadProfile(sessionID: string): void { + public async loadProfile(sessionID: string): Promise { const filename = `${sessionID}.json`; const filePath = `${this.profileFilepath}${filename}`; - if (this.fileSystemSync.exists(filePath)) { + if (await this.fileSystem.exists(filePath)) { // File found, store in profiles[] - this.profiles[sessionID] = this.fileSystemSync.readJson(filePath); + this.profiles.set(sessionID, await this.fileSystem.readJson(filePath)); } // Run callbacks for (const callback of this.saveLoadRouters) { - this.profiles[sessionID] = callback.handleLoad(this.getProfile(sessionID)); + this.profiles.set(sessionID, await callback.handleLoad(this.getProfile(sessionID))); } } @@ -170,46 +177,50 @@ export class SaveServer { * Save changes from in-memory profile to user/profiles json * Execute onBeforeSaveCallbacks callbacks prior to being saved to json * @param sessionID profile id (user/profiles/id.json) - * @returns void + * @returns A promise that resolves when saving is completed. */ - public saveProfile(sessionID: string): void { + public async saveProfile(sessionID: string): Promise { + if (!this.profiles.get(sessionID)) { + throw new Error(`Profile ${sessionID} does not exist! Unable to save this profile!`); + } + const filePath = `${this.profileFilepath}${sessionID}.json`; // Run pre-save callbacks before we save into json - for (const callback in this.onBeforeSaveCallbacks) { - const previous = this.profiles[sessionID]; + for (const [id, callback] of this.onBeforeSaveCallbacks) { + const previous = this.profiles.get(sessionID) as ISptProfile; // Cast as ISptProfile here since there should be no reason we're getting an undefined profile try { - this.profiles[sessionID] = this.onBeforeSaveCallbacks[callback](this.profiles[sessionID]); + this.profiles.set(sessionID, await callback(this.profiles.get(sessionID) as ISptProfile)); // Cast as ISptProfile here since there should be no reason we're getting an undefined profile } catch (error) { this.logger.error(this.localisationService.getText("profile_save_callback_error", { callback, error })); - this.profiles[sessionID] = previous; + this.profiles.set(sessionID, previous); } } const jsonProfile = this.jsonUtil.serialize( - this.profiles[sessionID], + this.profiles.get(sessionID), !this.configServer.getConfig(ConfigTypes.CORE).features.compressProfile, ); - const fmd5 = this.hashUtil.generateMd5ForData(jsonProfile); - if (typeof this.saveMd5[sessionID] !== "string" || this.saveMd5[sessionID] !== fmd5) { - this.saveMd5[sessionID] = String(fmd5); + const sha1 = await this.hashUtil.generateSha1ForDataAsync(jsonProfile); + if (typeof this.saveSHA1[sessionID] !== "string" || this.saveSHA1[sessionID] !== sha1) { + this.saveSHA1[sessionID] = sha1; // save profile to disk - this.fileSystemSync.write(filePath, jsonProfile); + await this.fileSystem.write(filePath, jsonProfile); } } /** * Remove a physical profile json from user/profiles * @param sessionID Profile id to remove - * @returns true if file no longer exists + * @returns A promise that is true if the file no longer exists */ - public removeProfile(sessionID: string): boolean { + public async removeProfile(sessionID: string): Promise { const file = `${this.profileFilepath}${sessionID}.json`; - delete this.profiles[sessionID]; + this.profiles.delete(sessionID); - this.fileSystemSync.remove(file); + await this.fileSystem.remove(file); - return !this.fileSystemSync.exists(file); + return !this.fileSystem.exists(file); } } diff --git a/project/src/services/CreateProfileService.ts b/project/src/services/CreateProfileService.ts index 2d5d499e..73d0bf9a 100644 --- a/project/src/services/CreateProfileService.ts +++ b/project/src/services/CreateProfileService.ts @@ -47,7 +47,7 @@ export class CreateProfileService { @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, ) {} - public createProfile(sessionID: string, info: IProfileCreateRequestData): string { + public async createProfile(sessionID: string, info: IProfileCreateRequestData): Promise { const account = this.saveServer.getProfile(sessionID).info; const profileTemplateClone: ITemplateSide = this.cloner.clone( this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()], @@ -145,12 +145,12 @@ export class CreateProfileService { this.saveServer.getProfile(sessionID).characters.scav = this.playerScavGenerator.generate(sessionID); // Store minimal profile and reload it - this.saveServer.saveProfile(sessionID); - this.saveServer.loadProfile(sessionID); + await this.saveServer.saveProfile(sessionID); + await this.saveServer.loadProfile(sessionID); // Completed account creation this.saveServer.getProfile(sessionID).info.wipe = false; - this.saveServer.saveProfile(sessionID); + await this.saveServer.saveProfile(sessionID); return pmcData._id; }