diff --git a/project/assets/database/locales/server/en.json b/project/assets/database/locales/server/en.json index 897cc2a0..d13ad243 100644 --- a/project/assets/database/locales/server/en.json +++ b/project/assets/database/locales/server/en.json @@ -66,6 +66,7 @@ "customisation-unable_to_find_suit_with_id": "Unable to find suit with offer id: %s", "customisation-unable_to_get_trader_suits": "Unable to get suits from trader: %s", "database-data_at_path_missing": "The database was unable to retrieve data from: [%s] Please ensure your configs are valid and the data at the location exists", + "database-invalid_data": "Invalid data detected in database, please remove any outdated mods. See previous error for invalid data. Server stopped.", "database-no_location_found_with_id": "No location found with an Id of: %s in database", "database-no_trader_found_with_id": "Unable to find trader: %s in database", "dialog-chatbot_id_already_exists": "Chat bot: %s being registered already exists, unable to register bot", diff --git a/project/src/servers/HttpServer.ts b/project/src/servers/HttpServer.ts index 6f461af4..80a61fad 100644 --- a/project/src/servers/HttpServer.ts +++ b/project/src/servers/HttpServer.ts @@ -6,11 +6,11 @@ import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { IHttpConfig } from "@spt/models/spt/config/IHttpConfig"; import { ILogger } from "@spt/models/spt/utils/ILogger"; import { ConfigServer } from "@spt/servers/ConfigServer"; -import { DatabaseServer } from "@spt/servers/DatabaseServer"; import { WebSocketServer } from "@spt/servers/WebSocketServer"; import { IHttpListener } from "@spt/servers/http/IHttpListener"; import { LocalisationService } from "@spt/services/LocalisationService"; import { inject, injectAll, injectable } from "tsyringe"; +import { DatabaseService } from "@spt/services/DatabaseService"; @injectable() export class HttpServer { @@ -19,7 +19,7 @@ export class HttpServer { constructor( @inject("PrimaryLogger") protected logger: ILogger, - @inject("DatabaseServer") protected databaseServer: DatabaseServer, + @inject("DatabaseService") protected databaseService: DatabaseService, @inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper, @inject("LocalisationService") protected localisationService: LocalisationService, @injectAll("HttpListener") protected httpListeners: IHttpListener[], @@ -36,6 +36,12 @@ export class HttpServer { public load(): void { this.started = false; + // If the database couldn't be validated, don't start the server + if (!this.databaseService.isDatabaseValid()) + { + return; + } + /* create server */ const httpServer: Server = http.createServer(); diff --git a/project/src/services/DatabaseService.ts b/project/src/services/DatabaseService.ts index fec5ebd0..11908da3 100644 --- a/project/src/services/DatabaseService.ts +++ b/project/src/services/DatabaseService.ts @@ -21,16 +21,19 @@ import { ITemplates } from "@spt/models/spt/templates/ITemplates"; import { ILogger } from "@spt/models/spt/utils/ILogger"; import { DatabaseServer } from "@spt/servers/DatabaseServer"; import { LocalisationService } from "@spt/services/LocalisationService"; +import { HashUtil } from "@spt/utils/HashUtil"; import { inject, injectable } from "tsyringe"; @injectable() export class DatabaseService { protected locationConfig: ILocationConfig; + protected isDataValid: boolean; constructor( @inject("PrimaryLogger") protected logger: ILogger, @inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("HashUtil") protected hashUtil: HashUtil, ) {} /** @@ -322,4 +325,53 @@ export class DatabaseService { return this.databaseServer.getTables().templates.locationServices; } + + /** + * Validates that the database doesn't contain invalid ID data + */ + public validateDatabase(): void { + const start = performance.now(); + + this.isDataValid = + this.validateTable(this.getQuests(), 'quest') && + this.validateTable(this.getTraders(), 'trader') && + this.validateTable(this.getItems(), 'item') && + this.validateTable(this.getCustomization(), 'customization'); + + if (!this.isDataValid) + { + this.logger.error(this.localisationService.getText("database-invalid_data")); + } + + const validateTime = performance.now() - start + this.logger.debug(`ID validation took: ${validateTime.toFixed(2)}ms`); + } + + /** + * Validate that the given table only contains valid MongoIDs + * @param table Table to validate for MongoIDs + * @param tableType The type of table, used in output message + * @returns True if the table only contains valid data + */ + private validateTable(table: Record, tableType: string): boolean + { + for (const tableId in table) + { + if (!this.hashUtil.isValidMongoId(tableId)) + { + this.logger.error(`Invalid ${tableType} ID: '${tableId}'`); + return false; + } + } + + return true; + } + + /** + * Check if the database is valid + * @returns True if the database contains valid data, false otherwise + */ + public isDatabaseValid(): boolean { + return this.isDataValid; + } } diff --git a/project/src/services/PostDbLoadService.ts b/project/src/services/PostDbLoadService.ts index 15b5b038..ef041612 100644 --- a/project/src/services/PostDbLoadService.ts +++ b/project/src/services/PostDbLoadService.ts @@ -57,6 +57,15 @@ export class PostDbLoadService { // add items gets left out,causing warnings this.itemBaseClassService.hydrateItemBaseClassCache(); + // Validate that only mongoIds exist in items, quests, and traders + // Kill the startup if not. + // TODO: We can probably remove this in a couple versions + this.databaseService.validateDatabase(); + if (!this.databaseService.isDatabaseValid()) + { + return; + } + this.addCustomLooseLootPositions(); this.adjustMinReserveRaiderSpawnChance(); diff --git a/project/src/utils/App.ts b/project/src/utils/App.ts index 27f563e4..7c9f9082 100644 --- a/project/src/utils/App.ts +++ b/project/src/utils/App.ts @@ -10,6 +10,7 @@ import { LocalisationService } from "@spt/services/LocalisationService"; import { EncodingUtil } from "@spt/utils/EncodingUtil"; import { TimeUtil } from "@spt/utils/TimeUtil"; import { inject, injectAll, injectable } from "tsyringe"; +import { DatabaseService } from "@spt/services/DatabaseService"; @injectable() export class App { @@ -23,6 +24,7 @@ export class App { @inject("ConfigServer") protected configServer: ConfigServer, @inject("EncodingUtil") protected encodingUtil: EncodingUtil, @inject("HttpServer") protected httpServer: HttpServer, + @inject("DatabaseService") protected databaseService: DatabaseService, @injectAll("OnLoad") protected onLoadComponents: OnLoad[], @injectAll("OnUpdate") protected onUpdateComponents: OnUpdate[], ) { @@ -58,7 +60,7 @@ export class App { protected async update(onUpdateComponents: OnUpdate[]): Promise { // If the server has failed to start, skip any update calls - if (!this.httpServer.isStarted()) { + if (!this.httpServer.isStarted() || !this.databaseService.isDatabaseValid()) { return; }