From 9eba62d5e251eecabef2526f83462b760e93b03d Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 22 Oct 2024 13:33:07 +0100 Subject: [PATCH] Moved code from `gameStart()` into server start via new class `PostDbLoadService` Fixed player adding their name multiple times to PMCs inside `addPlayerToPMCNames()` Updated `enableSeasonalEvents()` to not require a session id, moved player-specific code into new function `givePlayerSeasonalGifts()` --- project/src/controllers/GameController.ts | 468 +---------------- project/src/controllers/ProfileController.ts | 5 - project/src/di/Container.ts | 4 + project/src/services/PostDbLoadService.ts | 504 +++++++++++++++++++ project/src/services/SeasonalEventService.ts | 26 +- 5 files changed, 536 insertions(+), 471 deletions(-) create mode 100644 project/src/services/PostDbLoadService.ts diff --git a/project/src/controllers/GameController.ts b/project/src/controllers/GameController.ts index 5411730f..6df1bd8e 100644 --- a/project/src/controllers/GameController.ts +++ b/project/src/controllers/GameController.ts @@ -6,7 +6,6 @@ import { InventoryHelper } from "@spt/helpers/InventoryHelper"; import { ProfileHelper } from "@spt/helpers/ProfileHelper"; import { PreSptModLoader } from "@spt/loaders/PreSptModLoader"; import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData"; -import { ILocation } from "@spt/models/eft/common/ILocation"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; import { IBodyPartHealth } from "@spt/models/eft/common/tables/IBotBase"; import { ICheckVersionResponse } from "@spt/models/eft/game/ICheckVersionResponse"; @@ -23,15 +22,10 @@ import { BonusType } from "@spt/models/enums/BonusType"; import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { HideoutAreas } from "@spt/models/enums/HideoutAreas"; import { SkillTypes } from "@spt/models/enums/SkillTypes"; -import { Traders } from "@spt/models/enums/Traders"; -import { Weapons } from "@spt/models/enums/Weapons"; import { IBotConfig } from "@spt/models/spt/config/IBotConfig"; import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig"; import { IHideoutConfig } from "@spt/models/spt/config/IHideoutConfig"; import { IHttpConfig } from "@spt/models/spt/config/IHttpConfig"; -import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig"; -import { ILootConfig } from "@spt/models/spt/config/ILootConfig"; -import { IPmcConfig } from "@spt/models/spt/config/IPmcConfig"; import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig"; import { ILogger } from "@spt/models/spt/utils/ILogger"; import { ConfigServer } from "@spt/servers/ConfigServer"; @@ -41,6 +35,7 @@ import { GiftService } from "@spt/services/GiftService"; import { ItemBaseClassService } from "@spt/services/ItemBaseClassService"; import { LocalisationService } from "@spt/services/LocalisationService"; import { OpenZoneService } from "@spt/services/OpenZoneService"; +import { PostDbLoadService } from "@spt/services/PostDbLoadService"; import { ProfileActivityService } from "@spt/services/ProfileActivityService"; import { ProfileFixerService } from "@spt/services/ProfileFixerService"; import { RaidTimeAdjustmentService } from "@spt/services/RaidTimeAdjustmentService"; @@ -55,11 +50,8 @@ import { inject, injectable } from "tsyringe"; export class GameController { protected httpConfig: IHttpConfig; protected coreConfig: ICoreConfig; - protected locationConfig: ILocationConfig; protected ragfairConfig: IRagfairConfig; protected hideoutConfig: IHideoutConfig; - protected pmcConfig: IPmcConfig; - protected lootConfig: ILootConfig; protected botConfig: IBotConfig; constructor( @@ -75,6 +67,7 @@ export class GameController { @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("ProfileFixerService") protected profileFixerService: ProfileFixerService, @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("PostDbLoadService") protected postDbLoadService: PostDbLoadService, @inject("CustomLocationWaveService") protected customLocationWaveService: CustomLocationWaveService, @inject("OpenZoneService") protected openZoneService: OpenZoneService, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, @@ -88,20 +81,13 @@ export class GameController { ) { this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP); this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE); - this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION); this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT); - this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC); - this.lootConfig = this.configServer.getConfig(ConfigTypes.LOOT); this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); } public load(): void { - // Regenerate base cache now mods are loaded and game is starting - // Mods that add items and use the baseClass service generate the cache including their items, the next mod that - // add items gets left out,causing warnings - this.itemBaseClassService.hydrateItemBaseClassCache(); - this.addCustomLooseLootPositions(); + this.postDbLoadService.performPostDbLoadActions(); } /** @@ -113,28 +99,6 @@ export class GameController { this.profileActivityService.setActivityTimestamp(sessionID); - if (this.coreConfig.fixes.fixShotgunDispersion) { - this.fixShotgunDispersions(); - } - - if (this.locationConfig.addOpenZonesToAllMaps) { - this.openZoneService.applyZoneChangesToAllMaps(); - } - - if (this.locationConfig.addCustomBotWavesToMaps) { - this.customLocationWaveService.applyWaveChangesToAllMaps(); - } - - if (this.locationConfig.enableBotTypeLimits) { - this.adjustMapBotLimits(); - } - - this.adjustLooseLootSpawnProbabilities(); - - this.checkTraderRepairValuesExist(); - - this.adjustLocationBotValues(); - // repeatableQuests are stored by in profile.Quests due to the responses of the client (e.g. Quests in // offraidData). Since we don't want to clutter the Quests list, we need to remove all completed (failed or // successful) repeatable quests. We also have to remove the Counters from the repeatableQuests @@ -182,18 +146,6 @@ export class GameController { this.updateProfileHealthValues(pmcProfile); } - if (this.locationConfig.fixEmptyBotWavesSettings.enabled) { - this.fixBrokenOfflineMapWaves(); - } - - if (this.locationConfig.rogueLighthouseSpawnTimeSettings.enabled) { - this.fixRoguesSpawningInstantlyOnLighthouse(); - } - - if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.enabled) { - this.splitBotWavesIntoSingleWaves(); - } - if (pmcProfile.Inventory) { this.sendPraporGiftsToNewProfiles(pmcProfile); @@ -210,35 +162,19 @@ export class GameController { this.logProfileDetails(fullProfile); - this.adjustLabsRaiderSpawnRate(); - - this.adjustHideoutCraftTimes(this.hideoutConfig.overrideCraftTimeSeconds); - this.adjustHideoutBuildTimes(this.hideoutConfig.overrideBuildTimeSeconds); - - this.removePraporTestMessage(); - this.saveActiveModsToProfile(fullProfile); - this.validateQuestAssortUnlocksExist(); - if (pmcProfile.Info) { this.addPlayerToPMCNames(pmcProfile); this.checkForAndRemoveUndefinedDialogs(fullProfile); } - if (this.seasonalEventService.isAutomaticEventDetectionEnabled()) { - this.seasonalEventService.enableSeasonalEvents(sessionID); - } - if (pmcProfile?.Skills?.Common) { this.warnOnActiveBotReloadSkill(pmcProfile); } - // Flea bsg blacklist is off - if (!this.ragfairConfig.dynamic.blacklist.enableBsgList) { - this.setAllDbItemsAsSellableOnFlea(); - } + this.seasonalEventService.givePlayerSeasonalGifts(sessionID); } } @@ -303,189 +239,6 @@ export class GameController { } } - protected adjustHideoutCraftTimes(overrideSeconds: number): void { - if (overrideSeconds === -1) { - return; - } - - for (const craft of this.databaseService.getHideout().production.recipes) { - // Only adjust crafts ABOVE the override - craft.productionTime = Math.min(craft.productionTime, overrideSeconds); - } - } - - /** - * Adjust all hideout craft times to be no higher than the override - */ - protected adjustHideoutBuildTimes(overrideSeconds: number): void { - if (overrideSeconds === -1) { - return; - } - - for (const area of this.databaseService.getHideout().areas) { - for (const stage of Object.values(area.stages)) { - // Only adjust crafts ABOVE the override - stage.constructionTime = Math.min(stage.constructionTime, overrideSeconds); - } - } - } - - protected adjustLocationBotValues(): void { - const mapsDb = this.databaseService.getLocations(); - - for (const locationKey in this.botConfig.maxBotCap) { - const map: ILocation = mapsDb[locationKey]; - if (!map) { - continue; - } - - map.base.BotMaxPvE = this.botConfig.maxBotCap[locationKey]; - - // make values no larger than 30 secs - map.base.BotStart = Math.min(map.base.BotStart, 30); - } - } - - /** - * Out of date/incorrectly made trader mods forget this data - */ - protected checkTraderRepairValuesExist(): void { - const traders = this.databaseService.getTraders(); - for (const trader of Object.values(traders)) { - if (!trader?.base?.repair) { - this.logger.warning( - this.localisationService.getText("trader-missing_repair_property_using_default", { - traderId: trader.base._id, - nickname: trader.base.nickname, - }), - ); - - // use ragfair trader as a default - trader.base.repair = this.cloner.clone(traders.ragfair.base.repair); - - return; - } - - if (trader.base.repair?.quality === undefined) { - this.logger.warning( - this.localisationService.getText("trader-missing_repair_quality_property_using_default", { - traderId: trader.base._id, - nickname: trader.base.nickname, - }), - ); - - // use ragfair trader as a default - trader.base.repair.quality = this.cloner.clone(traders.ragfair.base.repair.quality); - trader.base.repair.quality = traders.ragfair.base.repair.quality; - } - } - } - - protected addCustomLooseLootPositions(): void { - const looseLootPositionsToAdd = this.lootConfig.looseLoot; - for (const [mapId, positionsToAdd] of Object.entries(looseLootPositionsToAdd)) { - if (!mapId) { - this.logger.warning( - this.localisationService.getText("location-unable_to_add_custom_loot_position", mapId), - ); - - continue; - } - - const mapLooseLoot = this.databaseService.getLocation(mapId).looseLoot; - if (!mapLooseLoot) { - this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId)); - - continue; - } - - for (const positionToAdd of positionsToAdd) { - // Exists already, add new items to existing positions pool - const existingLootPosition = mapLooseLoot.spawnpoints.find( - (x) => x.template.Id === positionToAdd.template.Id, - ); - - if (existingLootPosition) { - existingLootPosition.template.Items.push(...positionToAdd.template.Items); - existingLootPosition.itemDistribution.push(...positionToAdd.itemDistribution); - - continue; - } - - // New position, add entire object - mapLooseLoot.spawnpoints.push(positionToAdd); - } - } - } - - protected adjustLooseLootSpawnProbabilities(): void { - if (!this.lootConfig.looseLootSpawnPointAdjustments) { - return; - } - - for (const [mapId, mapAdjustments] of Object.entries(this.lootConfig.looseLootSpawnPointAdjustments)) { - const mapLooseLootData = this.databaseService.getLocation(mapId).looseLoot; - if (!mapLooseLootData) { - this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId)); - - continue; - } - - for (const [lootKey, newChanceValue] of Object.entries(mapAdjustments)) { - const lootPostionToAdjust = mapLooseLootData.spawnpoints.find( - (spawnPoint) => spawnPoint.template.Id === lootKey, - ); - if (!lootPostionToAdjust) { - this.logger.warning( - this.localisationService.getText("location-unable_to_adjust_loot_position_on_map", { - lootKey: lootKey, - mapId: mapId, - }), - ); - - continue; - } - - lootPostionToAdjust.probability = newChanceValue; - } - } - } - - /** Apply custom limits on bot types as defined in configs/location.json/botTypeLimits */ - protected adjustMapBotLimits(): void { - const mapsDb = this.databaseService.getLocations(); - if (!this.locationConfig.botTypeLimits) { - return; - } - - for (const mapId in this.locationConfig.botTypeLimits) { - const map: ILocation = mapsDb[mapId]; - if (!map) { - this.logger.warning( - this.localisationService.getText("bot-unable_to_edit_limits_of_unknown_map", mapId), - ); - } - - for (const botToLimit of this.locationConfig.botTypeLimits[mapId]) { - const index = map.base.MinMaxBots.findIndex((x) => x.WildSpawnType === botToLimit.type); - if (index !== -1) { - // Existing bot type found in MinMaxBots array, edit - const limitObjectToUpdate = map.base.MinMaxBots[index]; - limitObjectToUpdate.min = botToLimit.min; - limitObjectToUpdate.max = botToLimit.max; - } else { - // Bot type not found, add new object - map.base.MinMaxBots.push({ - // Bot type not found, add new object - WildSpawnType: botToLimit.type, - min: botToLimit.min, - max: botToLimit.max, - }); - } - } - } - } - /** * Handle client/game/config */ @@ -568,20 +321,6 @@ export class GameController { return this.raidTimeAdjustmentService.getRaidAdjustments(sessionId, request); } - /** - * BSG have two values for shotgun dispersion, we make sure both have the same value - */ - protected fixShotgunDispersions(): void { - const itemDb = this.databaseService.getItems(); - - const shotguns = [Weapons.SHOTGUN_12G_SAIGA_12K, Weapons.SHOTGUN_20G_TOZ_106, Weapons.SHOTGUN_12G_M870]; - for (const shotgunId of shotguns) { - if (itemDb[shotgunId]._props.ShotgunDispersion) { - itemDb[shotgunId]._props.shotgunDispersion = itemDb[shotgunId]._props.ShotgunDispersion; - } - } - } - /** * Players set botReload to a high value and don't expect the crazy fast reload speeds, give them a warn about it * @param pmcProfile Player profile @@ -593,19 +332,6 @@ export class GameController { } } - protected setAllDbItemsAsSellableOnFlea(): void { - const dbItems = Object.values(this.databaseService.getItems()); - for (const item of dbItems) { - if ( - item._type === "Item" && - !item._props?.CanSellOnRagfair && - !this.ragfairConfig.dynamic.blacklist.custom.includes(item._id) - ) { - item._props.CanSellOnRagfair = true; - } - } - } - /** * When player logs in, iterate over all active effects and reduce timer * @param pmcProfile Profile to adjust values for @@ -691,56 +417,6 @@ export class GameController { } } - /** - * Waves with an identical min/max values spawn nothing, the number of bots that spawn is the difference between min and max - */ - protected fixBrokenOfflineMapWaves(): void { - const locations = this.databaseService.getLocations(); - for (const locationKey in locations) { - // Skip ignored maps - if (this.locationConfig.fixEmptyBotWavesSettings.ignoreMaps.includes(locationKey)) { - continue; - } - - // Loop over all of the locations waves and look for waves with identical min and max slots - const location: ILocation = locations[locationKey]; - if (!location.base) { - this.logger.warning( - this.localisationService.getText("location-unable_to_fix_broken_waves_missing_base", locationKey), - ); - continue; - } - - for (const wave of location.base.waves ?? []) { - if (wave.slots_max - wave.slots_min === 0) { - this.logger.debug( - `Fixed ${wave.WildSpawnType} Spawn: ${locationKey} wave: ${wave.number} of type: ${wave.WildSpawnType} in zone: ${wave.SpawnPoints} with Max Slots of ${wave.slots_max}`, - ); - wave.slots_max++; - } - } - } - } - - /** - * Make Rogues spawn later to allow for scavs to spawn first instead of rogues filling up all spawn positions - */ - protected fixRoguesSpawningInstantlyOnLighthouse(): void { - const rogueSpawnDelaySeconds = this.locationConfig.rogueLighthouseSpawnTimeSettings.waitTimeSeconds; - const lighthouse = this.databaseService.getLocations().lighthouse?.base; - if (!lighthouse) { - return; - } - - // Find Rogues that spawn instantly - const instantRogueBossSpawns = lighthouse.BossLocationSpawn.filter( - (spawn) => spawn.BossName === "exUsec" && spawn.Time === -1, - ); - for (const wave of instantRogueBossSpawns) { - wave.Time = rogueSpawnDelaySeconds; - } - } - /** * Send starting gifts to profile after x days * @param pmcProfile Profile to add gifts to @@ -761,71 +437,6 @@ export class GameController { } } - /** - * Find and split waves with large numbers of bots into smaller waves - BSG appears to reduce the size of these - * waves to one bot when they're waiting to spawn for too long - */ - protected splitBotWavesIntoSingleWaves(): void { - const locations = this.databaseService.getLocations(); - for (const locationKey in locations) { - if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.ignoreMaps.includes(locationKey)) { - continue; - } - - // Iterate over all maps - const location: ILocation = locations[locationKey]; - for (const wave of location.base.waves) { - // Wave has size that makes it candidate for splitting - if ( - wave.slots_max - wave.slots_min >= - this.locationConfig.splitWaveIntoSingleSpawnsSettings.waveSizeThreshold - ) { - // Get count of bots to be spawned in wave - const waveSize = wave.slots_max - wave.slots_min; - - // Update wave to spawn single bot - wave.slots_min = 1; - wave.slots_max = 2; - - // Get index of wave - const indexOfWaveToSplit = location.base.waves.indexOf(wave); - this.logger.debug( - `Splitting map: ${location.base.Id} wave: ${indexOfWaveToSplit} with ${waveSize} bots`, - ); - - // Add new waves to fill gap from bots we removed in above wave - let wavesAddedCount = 0; - for (let index = indexOfWaveToSplit + 1; index < indexOfWaveToSplit + waveSize; index++) { - // Clone wave ready to insert into array - const waveToAddClone = this.cloner.clone(wave); - - // Some waves have value of 0 for some reason, preserve - if (waveToAddClone.number !== 0) { - // Update wave number to new location in array - waveToAddClone.number = index; - } - - // Place wave into array in just-edited position + 1 - location.base.waves.splice(index, 0, waveToAddClone); - wavesAddedCount++; - } - - // Update subsequent wave number property to accommodate the new waves - for ( - let index = indexOfWaveToSplit + wavesAddedCount + 1; - index < location.base.waves.length; - index++ - ) { - // Some waves have value of 0, leave them as-is - if (location.base.waves[index].number !== 0) { - location.base.waves[index].number += wavesAddedCount; - } - } - } - } - } - } - /** * Get a list of installed mods and save their details to the profile being used * @param fullProfile Profile to add mod details to @@ -862,44 +473,6 @@ export class GameController { } } - /** - * Check for any missing assorts inside each traders assort.json data, checking against traders questassort.json - */ - protected validateQuestAssortUnlocksExist(): void { - const db = this.databaseService.getTables(); - const traders = db.traders; - const quests = db.templates.quests; - for (const traderId of Object.values(Traders)) { - const traderData = traders[traderId]; - const traderAssorts = traderData?.assort; - if (!traderAssorts) { - continue; - } - - // Merge started/success/fail quest assorts into one dictionary - const mergedQuestAssorts = { - ...traderData.questassort?.started, - ...traderData.questassort?.success, - ...traderData.questassort?.fail, - }; - - // Loop over all assorts for trader - for (const [assortKey, questKey] of Object.entries(mergedQuestAssorts)) { - // Does assort key exist in trader assort file - if (!traderAssorts.loyal_level_items[assortKey]) { - // Reverse lookup of enum key by value - const messageValues = { - traderName: Object.keys(Traders)[Object.values(Traders).indexOf(traderId)], - questName: quests[questKey]?.QuestName ?? "UNKNOWN", - }; - this.logger.warning( - this.localisationService.getText("assort-missing_quest_assort_unlock", messageValues), - ); - } - } - } - } - /** * Add the logged in players name to PMC name pool * @param pmcProfile Profile of player to get name from @@ -914,6 +487,11 @@ export class GameController { return; } + // Skip if player name exists already + if (bots.bear?.firstName.some((x) => x === playerName)) { + return; + } + if (bots.bear) { bots.bear.firstName.push(playerName); } @@ -935,34 +513,6 @@ export class GameController { } } - /** - * Blank out the "test" mail message from prapor - */ - protected removePraporTestMessage(): void { - // Iterate over all languages (e.g. "en", "fr") - const locales = this.databaseService.getLocales(); - for (const localeKey in locales.global) { - locales.global[localeKey]["61687e2c3e526901fa76baf9"] = ""; - } - } - - /** - * Make non-trigger-spawned raiders spawn earlier + always - */ - protected adjustLabsRaiderSpawnRate(): void { - const labsBase = this.databaseService.getLocations().laboratory.base; - - // Find spawns with empty string for triggerId/TriggerName - const nonTriggerLabsBossSpawns = labsBase.BossLocationSpawn.filter( - (bossSpawn) => !bossSpawn.TriggerId && !bossSpawn.TriggerName, - ); - - for (const boss of nonTriggerLabsBossSpawns) { - boss.BossChance = 100; - boss.Time /= 10; - } - } - protected logProfileDetails(fullProfile: ISptProfile): void { this.logger.debug(`Profile made with: ${fullProfile.spt.version}`); this.logger.debug( diff --git a/project/src/controllers/ProfileController.ts b/project/src/controllers/ProfileController.ts index ce059556..d04a6054 100644 --- a/project/src/controllers/ProfileController.ts +++ b/project/src/controllers/ProfileController.ts @@ -219,11 +219,6 @@ export class ProfileController { this.saveServer.getProfile(sessionID).info.wipe = false; this.saveServer.saveProfile(sessionID); - // Requires to enable seasonal changes after creating fresh profile - if (this.seasonalEventService.isAutomaticEventDetectionEnabled()) { - this.seasonalEventService.enableSeasonalEvents(sessionID); - } - return pmcData._id; } diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index e0b75e4f..4433426c 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -226,6 +226,7 @@ import { OpenZoneService } from "@spt/services/OpenZoneService"; import { PaymentService } from "@spt/services/PaymentService"; import { PlayerService } from "@spt/services/PlayerService"; import { PmcChatResponseService } from "@spt/services/PmcChatResponseService"; +import { PostDbLoadService } from "@spt/services/PostDbLoadService"; import { ProfileActivityService } from "@spt/services/ProfileActivityService"; import { ProfileFixerService } from "@spt/services/ProfileFixerService"; import { RagfairCategoriesService } from "@spt/services/RagfairCategoriesService"; @@ -807,6 +808,9 @@ export class Container { depContainer.register("RaidWeatherService", RaidWeatherService, { lifecycle: Lifecycle.Singleton, }); + depContainer.register("PostDbLoadService", PostDbLoadService, { + lifecycle: Lifecycle.Singleton, + }); } private static registerServers(depContainer: DependencyContainer): void { diff --git a/project/src/services/PostDbLoadService.ts b/project/src/services/PostDbLoadService.ts new file mode 100644 index 00000000..fc7449f1 --- /dev/null +++ b/project/src/services/PostDbLoadService.ts @@ -0,0 +1,504 @@ +import { ILocation } from "@spt/models/eft/common/ILocation"; +import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; +import { Traders } from "@spt/models/enums/Traders"; +import { Weapons } from "@spt/models/enums/Weapons"; +import { IBotConfig } from "@spt/models/spt/config/IBotConfig"; +import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig"; +import { IHideoutConfig } from "@spt/models/spt/config/IHideoutConfig"; +import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig"; +import { ILootConfig } from "@spt/models/spt/config/ILootConfig"; +import { IPmcConfig } from "@spt/models/spt/config/IPmcConfig"; +import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig"; +import { ILogger } from "@spt/models/spt/utils/ILogger"; +import { ConfigServer } from "@spt/servers/ConfigServer"; +import { CustomLocationWaveService } from "@spt/services/CustomLocationWaveService"; +import { DatabaseService } from "@spt/services/DatabaseService"; +import { ItemBaseClassService } from "@spt/services/ItemBaseClassService"; +import { LocalisationService } from "@spt/services/LocalisationService"; +import { OpenZoneService } from "@spt/services/OpenZoneService"; +import { SeasonalEventService } from "@spt/services/SeasonalEventService"; +import { ICloner } from "@spt/utils/cloners/ICloner"; +import { inject, injectable } from "tsyringe"; + +@injectable() +export class PostDbLoadService { + protected coreConfig: ICoreConfig; + protected locationConfig: ILocationConfig; + protected ragfairConfig: IRagfairConfig; + protected hideoutConfig: IHideoutConfig; + protected pmcConfig: IPmcConfig; + protected lootConfig: ILootConfig; + protected botConfig: IBotConfig; + + constructor( + @inject("PrimaryLogger") protected logger: ILogger, + @inject("DatabaseService") protected databaseService: DatabaseService, + @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("CustomLocationWaveService") protected customLocationWaveService: CustomLocationWaveService, + @inject("OpenZoneService") protected openZoneService: OpenZoneService, + @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, + @inject("ItemBaseClassService") protected itemBaseClassService: ItemBaseClassService, + @inject("ConfigServer") protected configServer: ConfigServer, + @inject("PrimaryCloner") protected cloner: ICloner, + ) { + this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE); + this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION); + this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); + this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT); + this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC); + this.lootConfig = this.configServer.getConfig(ConfigTypes.LOOT); + this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); + } + + public performPostDbLoadActions(): void { + // Regenerate base cache now mods are loaded and game is starting + // Mods that add items and use the baseClass service generate the cache including their items, the next mod that + // add items gets left out,causing warnings + this.itemBaseClassService.hydrateItemBaseClassCache(); + + this.addCustomLooseLootPositions(); + + if (this.coreConfig.fixes.fixShotgunDispersion) { + this.fixShotgunDispersions(); + } + + if (this.locationConfig.addOpenZonesToAllMaps) { + this.openZoneService.applyZoneChangesToAllMaps(); + } + + if (this.locationConfig.addCustomBotWavesToMaps) { + this.customLocationWaveService.applyWaveChangesToAllMaps(); + } + + if (this.locationConfig.enableBotTypeLimits) { + this.adjustMapBotLimits(); + } + + this.adjustLooseLootSpawnProbabilities(); + + this.checkTraderRepairValuesExist(); + + this.adjustLocationBotValues(); + + if (this.locationConfig.fixEmptyBotWavesSettings.enabled) { + this.fixBrokenOfflineMapWaves(); + } + + if (this.locationConfig.rogueLighthouseSpawnTimeSettings.enabled) { + this.fixRoguesSpawningInstantlyOnLighthouse(); + } + + if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.enabled) { + this.splitBotWavesIntoSingleWaves(); + } + + this.adjustLabsRaiderSpawnRate(); + + this.adjustHideoutCraftTimes(this.hideoutConfig.overrideCraftTimeSeconds); + this.adjustHideoutBuildTimes(this.hideoutConfig.overrideBuildTimeSeconds); + + this.removePraporTestMessage(); + + this.validateQuestAssortUnlocksExist(); + + if (this.seasonalEventService.isAutomaticEventDetectionEnabled()) { + this.seasonalEventService.enableSeasonalEvents(); + } + + // Flea bsg blacklist is off + if (!this.ragfairConfig.dynamic.blacklist.enableBsgList) { + this.setAllDbItemsAsSellableOnFlea(); + } + } + + protected addCustomLooseLootPositions(): void { + const looseLootPositionsToAdd = this.lootConfig.looseLoot; + for (const [mapId, positionsToAdd] of Object.entries(looseLootPositionsToAdd)) { + if (!mapId) { + this.logger.warning( + this.localisationService.getText("location-unable_to_add_custom_loot_position", mapId), + ); + + continue; + } + + const mapLooseLoot = this.databaseService.getLocation(mapId).looseLoot; + if (!mapLooseLoot) { + this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId)); + + continue; + } + + for (const positionToAdd of positionsToAdd) { + // Exists already, add new items to existing positions pool + const existingLootPosition = mapLooseLoot.spawnpoints.find( + (x) => x.template.Id === positionToAdd.template.Id, + ); + + if (existingLootPosition) { + existingLootPosition.template.Items.push(...positionToAdd.template.Items); + existingLootPosition.itemDistribution.push(...positionToAdd.itemDistribution); + + continue; + } + + // New position, add entire object + mapLooseLoot.spawnpoints.push(positionToAdd); + } + } + } + + /** + * BSG have two values for shotgun dispersion, we make sure both have the same value + */ + protected fixShotgunDispersions(): void { + const itemDb = this.databaseService.getItems(); + + const shotguns = [Weapons.SHOTGUN_12G_SAIGA_12K, Weapons.SHOTGUN_20G_TOZ_106, Weapons.SHOTGUN_12G_M870]; + for (const shotgunId of shotguns) { + if (itemDb[shotgunId]._props.ShotgunDispersion) { + itemDb[shotgunId]._props.shotgunDispersion = itemDb[shotgunId]._props.ShotgunDispersion; + } + } + } + + /** Apply custom limits on bot types as defined in configs/location.json/botTypeLimits */ + protected adjustMapBotLimits(): void { + const mapsDb = this.databaseService.getLocations(); + if (!this.locationConfig.botTypeLimits) { + return; + } + + for (const mapId in this.locationConfig.botTypeLimits) { + const map: ILocation = mapsDb[mapId]; + if (!map) { + this.logger.warning( + this.localisationService.getText("bot-unable_to_edit_limits_of_unknown_map", mapId), + ); + } + + for (const botToLimit of this.locationConfig.botTypeLimits[mapId]) { + const index = map.base.MinMaxBots.findIndex((x) => x.WildSpawnType === botToLimit.type); + if (index !== -1) { + // Existing bot type found in MinMaxBots array, edit + const limitObjectToUpdate = map.base.MinMaxBots[index]; + limitObjectToUpdate.min = botToLimit.min; + limitObjectToUpdate.max = botToLimit.max; + } else { + // Bot type not found, add new object + map.base.MinMaxBots.push({ + // Bot type not found, add new object + WildSpawnType: botToLimit.type, + min: botToLimit.min, + max: botToLimit.max, + }); + } + } + } + } + + protected adjustLooseLootSpawnProbabilities(): void { + if (!this.lootConfig.looseLootSpawnPointAdjustments) { + return; + } + + for (const [mapId, mapAdjustments] of Object.entries(this.lootConfig.looseLootSpawnPointAdjustments)) { + const mapLooseLootData = this.databaseService.getLocation(mapId).looseLoot; + if (!mapLooseLootData) { + this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId)); + + continue; + } + + for (const [lootKey, newChanceValue] of Object.entries(mapAdjustments)) { + const lootPostionToAdjust = mapLooseLootData.spawnpoints.find( + (spawnPoint) => spawnPoint.template.Id === lootKey, + ); + if (!lootPostionToAdjust) { + this.logger.warning( + this.localisationService.getText("location-unable_to_adjust_loot_position_on_map", { + lootKey: lootKey, + mapId: mapId, + }), + ); + + continue; + } + + lootPostionToAdjust.probability = newChanceValue; + } + } + } + + /** + * Out of date/incorrectly made trader mods forget this data + */ + protected checkTraderRepairValuesExist(): void { + const traders = this.databaseService.getTraders(); + for (const trader of Object.values(traders)) { + if (!trader?.base?.repair) { + this.logger.warning( + this.localisationService.getText("trader-missing_repair_property_using_default", { + traderId: trader.base._id, + nickname: trader.base.nickname, + }), + ); + + // use ragfair trader as a default + trader.base.repair = this.cloner.clone(traders.ragfair.base.repair); + + return; + } + + if (trader.base.repair?.quality === undefined) { + this.logger.warning( + this.localisationService.getText("trader-missing_repair_quality_property_using_default", { + traderId: trader.base._id, + nickname: trader.base.nickname, + }), + ); + + // use ragfair trader as a default + trader.base.repair.quality = this.cloner.clone(traders.ragfair.base.repair.quality); + trader.base.repair.quality = traders.ragfair.base.repair.quality; + } + } + } + + protected adjustLocationBotValues(): void { + const mapsDb = this.databaseService.getLocations(); + + for (const locationKey in this.botConfig.maxBotCap) { + const map: ILocation = mapsDb[locationKey]; + if (!map) { + continue; + } + + map.base.BotMaxPvE = this.botConfig.maxBotCap[locationKey]; + + // make values no larger than 30 secs + map.base.BotStart = Math.min(map.base.BotStart, 30); + } + } + + /** + * Waves with an identical min/max values spawn nothing, the number of bots that spawn is the difference between min and max + */ + protected fixBrokenOfflineMapWaves(): void { + const locations = this.databaseService.getLocations(); + for (const locationKey in locations) { + // Skip ignored maps + if (this.locationConfig.fixEmptyBotWavesSettings.ignoreMaps.includes(locationKey)) { + continue; + } + + // Loop over all of the locations waves and look for waves with identical min and max slots + const location: ILocation = locations[locationKey]; + if (!location.base) { + this.logger.warning( + this.localisationService.getText("location-unable_to_fix_broken_waves_missing_base", locationKey), + ); + continue; + } + + for (const wave of location.base.waves ?? []) { + if (wave.slots_max - wave.slots_min === 0) { + this.logger.debug( + `Fixed ${wave.WildSpawnType} Spawn: ${locationKey} wave: ${wave.number} of type: ${wave.WildSpawnType} in zone: ${wave.SpawnPoints} with Max Slots of ${wave.slots_max}`, + ); + wave.slots_max++; + } + } + } + } + + /** + * Make Rogues spawn later to allow for scavs to spawn first instead of rogues filling up all spawn positions + */ + protected fixRoguesSpawningInstantlyOnLighthouse(): void { + const rogueSpawnDelaySeconds = this.locationConfig.rogueLighthouseSpawnTimeSettings.waitTimeSeconds; + const lighthouse = this.databaseService.getLocations().lighthouse?.base; + if (!lighthouse) { + return; + } + + // Find Rogues that spawn instantly + const instantRogueBossSpawns = lighthouse.BossLocationSpawn.filter( + (spawn) => spawn.BossName === "exUsec" && spawn.Time === -1, + ); + for (const wave of instantRogueBossSpawns) { + wave.Time = rogueSpawnDelaySeconds; + } + } + + /** + * Find and split waves with large numbers of bots into smaller waves - BSG appears to reduce the size of these + * waves to one bot when they're waiting to spawn for too long + */ + protected splitBotWavesIntoSingleWaves(): void { + const locations = this.databaseService.getLocations(); + for (const locationKey in locations) { + if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.ignoreMaps.includes(locationKey)) { + continue; + } + + // Iterate over all maps + const location: ILocation = locations[locationKey]; + for (const wave of location.base.waves) { + // Wave has size that makes it candidate for splitting + if ( + wave.slots_max - wave.slots_min >= + this.locationConfig.splitWaveIntoSingleSpawnsSettings.waveSizeThreshold + ) { + // Get count of bots to be spawned in wave + const waveSize = wave.slots_max - wave.slots_min; + + // Update wave to spawn single bot + wave.slots_min = 1; + wave.slots_max = 2; + + // Get index of wave + const indexOfWaveToSplit = location.base.waves.indexOf(wave); + this.logger.debug( + `Splitting map: ${location.base.Id} wave: ${indexOfWaveToSplit} with ${waveSize} bots`, + ); + + // Add new waves to fill gap from bots we removed in above wave + let wavesAddedCount = 0; + for (let index = indexOfWaveToSplit + 1; index < indexOfWaveToSplit + waveSize; index++) { + // Clone wave ready to insert into array + const waveToAddClone = this.cloner.clone(wave); + + // Some waves have value of 0 for some reason, preserve + if (waveToAddClone.number !== 0) { + // Update wave number to new location in array + waveToAddClone.number = index; + } + + // Place wave into array in just-edited position + 1 + location.base.waves.splice(index, 0, waveToAddClone); + wavesAddedCount++; + } + + // Update subsequent wave number property to accommodate the new waves + for ( + let index = indexOfWaveToSplit + wavesAddedCount + 1; + index < location.base.waves.length; + index++ + ) { + // Some waves have value of 0, leave them as-is + if (location.base.waves[index].number !== 0) { + location.base.waves[index].number += wavesAddedCount; + } + } + } + } + } + } + + /** + * Make non-trigger-spawned raiders spawn earlier + always + */ + protected adjustLabsRaiderSpawnRate(): void { + const labsBase = this.databaseService.getLocations().laboratory.base; + + // Find spawns with empty string for triggerId/TriggerName + const nonTriggerLabsBossSpawns = labsBase.BossLocationSpawn.filter( + (bossSpawn) => !bossSpawn.TriggerId && !bossSpawn.TriggerName, + ); + + for (const boss of nonTriggerLabsBossSpawns) { + boss.BossChance = 100; + boss.Time /= 10; + } + } + + protected adjustHideoutCraftTimes(overrideSeconds: number): void { + if (overrideSeconds === -1) { + return; + } + + for (const craft of this.databaseService.getHideout().production.recipes) { + // Only adjust crafts ABOVE the override + craft.productionTime = Math.min(craft.productionTime, overrideSeconds); + } + } + + /** + * Adjust all hideout craft times to be no higher than the override + */ + protected adjustHideoutBuildTimes(overrideSeconds: number): void { + if (overrideSeconds === -1) { + return; + } + + for (const area of this.databaseService.getHideout().areas) { + for (const stage of Object.values(area.stages)) { + // Only adjust crafts ABOVE the override + stage.constructionTime = Math.min(stage.constructionTime, overrideSeconds); + } + } + } + + /** + * Blank out the "test" mail message from prapor + */ + protected removePraporTestMessage(): void { + // Iterate over all languages (e.g. "en", "fr") + const locales = this.databaseService.getLocales(); + for (const localeKey in locales.global) { + locales.global[localeKey]["61687e2c3e526901fa76baf9"] = ""; + } + } + + /** + * Check for any missing assorts inside each traders assort.json data, checking against traders questassort.json + */ + protected validateQuestAssortUnlocksExist(): void { + const db = this.databaseService.getTables(); + const traders = db.traders; + const quests = db.templates.quests; + for (const traderId of Object.values(Traders)) { + const traderData = traders[traderId]; + const traderAssorts = traderData?.assort; + if (!traderAssorts) { + continue; + } + + // Merge started/success/fail quest assorts into one dictionary + const mergedQuestAssorts = { + ...traderData.questassort?.started, + ...traderData.questassort?.success, + ...traderData.questassort?.fail, + }; + + // Loop over all assorts for trader + for (const [assortKey, questKey] of Object.entries(mergedQuestAssorts)) { + // Does assort key exist in trader assort file + if (!traderAssorts.loyal_level_items[assortKey]) { + // Reverse lookup of enum key by value + const messageValues = { + traderName: Object.keys(Traders)[Object.values(Traders).indexOf(traderId)], + questName: quests[questKey]?.QuestName ?? "UNKNOWN", + }; + this.logger.warning( + this.localisationService.getText("assort-missing_quest_assort_unlock", messageValues), + ); + } + } + } + } + + protected setAllDbItemsAsSellableOnFlea(): void { + const dbItems = Object.values(this.databaseService.getItems()); + for (const item of dbItems) { + if ( + item._type === "Item" && + !item._props?.CanSellOnRagfair && + !this.ragfairConfig.dynamic.blacklist.custom.includes(item._id) + ) { + item._props.CanSellOnRagfair = true; + } + } + } +} diff --git a/project/src/services/SeasonalEventService.ts b/project/src/services/SeasonalEventService.ts index e6bebb45..34395a9e 100644 --- a/project/src/services/SeasonalEventService.ts +++ b/project/src/services/SeasonalEventService.ts @@ -197,13 +197,12 @@ export class SeasonalEventService { /** * Handle seasonal events - * @param sessionId Players id */ - public enableSeasonalEvents(sessionId: string): void { + public enableSeasonalEvents(): void { if (this.currentlyActiveEvents) { const globalConfig = this.databaseService.getGlobals().config; for (const event of this.currentlyActiveEvents) { - this.updateGlobalEvents(sessionId, globalConfig, event); + this.updateGlobalEvents(globalConfig, event); } } } @@ -325,11 +324,10 @@ export class SeasonalEventService { /** * Make adjusted to server code based on the name of the event passed in - * @param sessionId Player id * @param globalConfig globals.json * @param eventName Name of the event to enable. e.g. Christmas */ - protected updateGlobalEvents(sessionId: string, globalConfig: IConfig, eventType: SeasonalEventType): void { + protected updateGlobalEvents(globalConfig: IConfig, eventType: SeasonalEventType): void { this.logger.success(`${eventType} event is active`); switch (eventType.toLowerCase()) { @@ -352,11 +350,9 @@ export class SeasonalEventService { this.addGifterBotToMaps(); this.addLootItemsToGifterDropItemsList(); this.enableDancingTree(); - this.giveGift(sessionId, "Christmas2022"); this.enableSnow(); break; case SeasonalEventType.NEW_YEARS.toLowerCase(): - this.giveGift(sessionId, "NewYear2023"); this.enableSnow(); break; case SeasonalEventType.SNOW.toLowerCase(): @@ -369,6 +365,22 @@ export class SeasonalEventService { } } + public givePlayerSeasonalGifts(sessionId: string): void { + if (this.currentlyActiveEvents) { + const globalConfig = this.databaseService.getGlobals().config; + for (const event of this.currentlyActiveEvents) { + switch (event.toLowerCase()) { + case SeasonalEventType.CHRISTMAS.toLowerCase(): + this.giveGift(sessionId, "Christmas2022"); + break; + case SeasonalEventType.NEW_YEARS.toLowerCase(): + this.giveGift(sessionId, "NewYear2023"); + break; + } + } + } + } + /** * Force zryachiy to always have a melee weapon */