0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00
server/project/src/services/PostDbLoadService.ts
Dev 9f6ec573d6 Removed fixEmptyBotWavesSettings - Doesnt seem necessary for 3.10
Disabled `rogueLighthouseSpawnTimeSettings` Same as above
2024-10-22 16:38:32 +01:00

470 lines
19 KiB
TypeScript

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.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);
}
}
/**
* 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;
}
}
}
}