0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00
server/project/src/controllers/GameController.ts

1000 lines
38 KiB
TypeScript
Raw Normal View History

2023-03-03 15:23:46 +00:00
import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { HideoutHelper } from "@spt/helpers/HideoutHelper";
import { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
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 { ILooseLoot } from "@spt/models/eft/common/ILooseLoot";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { BodyPartHealth } from "@spt/models/eft/common/tables/IBotBase";
import { ICheckVersionResponse } from "@spt/models/eft/game/ICheckVersionResponse";
import { ICurrentGroupResponse } from "@spt/models/eft/game/ICurrentGroupResponse";
import { IGameConfigResponse } from "@spt/models/eft/game/IGameConfigResponse";
import { IGameKeepAliveResponse } from "@spt/models/eft/game/IGameKeepAliveResponse";
import { IGameModeRequestData } from "@spt/models/eft/game/IGameModeRequestData";
import { ESessionMode } from "@spt/models/eft/game/IGameModeResponse";
import { IGetRaidTimeRequest } from "@spt/models/eft/game/IGetRaidTimeRequest";
import { IGetRaidTimeResponse } from "@spt/models/eft/game/IGetRaidTimeResponse";
import { IServerDetails } from "@spt/models/eft/game/IServerDetails";
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile";
import { BonusType } from "@spt/models/enums/BonusType";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { SkillTypes } from "@spt/models/enums/SkillTypes";
import { Traders } from "@spt/models/enums/Traders";
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";
import { DatabaseServer } from "@spt/servers/DatabaseServer";
import { CustomLocationWaveService } from "@spt/services/CustomLocationWaveService";
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 { ProfileActivityService } from "@spt/services/ProfileActivityService";
import { ProfileFixerService } from "@spt/services/ProfileFixerService";
import { RaidTimeAdjustmentService } from "@spt/services/RaidTimeAdjustmentService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
2023-03-03 15:23:46 +00:00
@injectable()
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;
2023-03-03 15:23:46 +00:00
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("PreSptModLoader") protected preSptModLoader: PreSptModLoader,
2023-03-03 15:23:46 +00:00
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("RandomUtil") protected randomUtil: RandomUtil,
2023-03-03 15:23:46 +00:00
@inject("HideoutHelper") protected hideoutHelper: HideoutHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("ProfileFixerService") protected profileFixerService: ProfileFixerService,
@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("GiftService") protected giftService: GiftService,
@inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
2023-03-03 15:23:46 +00:00
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
2023-11-15 20:35:05 -05:00
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("RecursiveCloner") protected cloner: ICloner,
2023-03-03 15:23:46 +00:00
)
{
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
{
2023-11-10 16:49:29 -05:00
// 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();
2023-03-03 15:23:46 +00:00
}
/**
* Handle client/game/start
*/
2023-03-03 15:23:46 +00:00
public gameStart(_url: string, _info: IEmptyRequestData, sessionID: string, startTimeStampMS: number): void
{
// Store client start time in app context
this.applicationContext.addValue(ContextVariableType.CLIENT_START_TIMESTAMP, startTimeStampMS);
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();
}
2023-03-03 15:23:46 +00:00
if (this.locationConfig.enableBotTypeLimits)
{
this.adjustMapBotLimits();
}
this.adjustLooseLootSpawnProbabilities();
2023-11-07 20:00:02 +00:00
this.checkTraderRepairValuesExist();
this.adjustLocationBotValues();
2023-11-10 16:49:29 -05:00
// 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
2023-03-03 15:23:46 +00:00
if (sessionID)
{
const fullProfile = this.profileHelper.getFullProfile(sessionID);
if (fullProfile.info.wipe)
{
// Don't bother doing any fixes, we're resetting profile
return;
}
2023-03-03 15:23:46 +00:00
const pmcProfile = fullProfile.characters.pmc;
this.logger.debug(`Started game with sessionId: ${sessionID} ${pmcProfile.Info?.Nickname}`);
2023-05-30 13:58:02 +01:00
// Migrate aki object data into spt for 3.9.0 release
if ((fullProfile as any).aki)
{
fullProfile.spt = this.cloner.clone((fullProfile as any).aki);
delete (fullProfile as any).aki;
}
if (this.coreConfig.fixes.fixProfileBreakingInventoryItemIssues)
{
this.profileFixerService.fixProfileBreakingInventoryItemIssues(pmcProfile);
}
2023-03-03 15:23:46 +00:00
if (pmcProfile.Health)
{
this.updateProfileHealthValues(pmcProfile);
}
2023-03-03 15:23:46 +00:00
if (this.locationConfig.fixEmptyBotWavesSettings.enabled)
{
this.fixBrokenOfflineMapWaves();
}
if (this.locationConfig.rogueLighthouseSpawnTimeSettings.enabled)
{
this.fixRoguesSpawningInstantlyOnLighthouse();
}
if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.enabled)
{
this.splitBotWavesIntoSingleWaves();
}
this.profileFixerService.removeLegacyScavCaseProductionCrafts(pmcProfile);
this.profileFixerService.addMissingHideoutAreasToProfile(fullProfile);
if (pmcProfile.Inventory)
{
// MUST occur prior to `profileFixerService.checkForAndFixPmcProfileIssues()`
this.profileFixerService.fixIncorrectAidValue(fullProfile);
this.profileFixerService.migrateStatsToNewStructure(fullProfile);
2023-11-15 20:35:05 -05:00
this.sendPraporGiftsToNewProfiles(pmcProfile);
this.profileFixerService.checkForOrphanedModdedItems(sessionID, fullProfile);
}
2023-03-03 15:23:46 +00:00
this.profileFixerService.checkForAndFixPmcProfileIssues(pmcProfile);
this.profileFixerService.addMissingSptVersionTagToProfile(fullProfile);
2023-03-03 15:23:46 +00:00
if (pmcProfile.Hideout)
{
this.profileFixerService.addMissingHideoutBonusesToProfile(pmcProfile);
this.profileFixerService.addMissingUpgradesPropertyToHideout(pmcProfile);
this.hideoutHelper.setHideoutImprovementsToCompleted(pmcProfile);
this.hideoutHelper.unlockHideoutWallInProfile(pmcProfile);
this.profileFixerService.addMissingIdsToBonuses(pmcProfile);
this.profileFixerService.fixBitcoinProductionTime(pmcProfile);
2023-03-03 15:23:46 +00:00
}
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
this.logProfileDetails(fullProfile);
this.adjustLabsRaiderSpawnRate();
this.adjustHideoutCraftTimes();
this.adjustHideoutBuildTimes();
2023-03-03 15:23:46 +00:00
this.removePraporTestMessage();
this.saveActiveModsToProfile(fullProfile);
this.validateQuestAssortUnlocksExist();
2023-03-03 15:23:46 +00:00
if (pmcProfile.Info)
{
this.addPlayerToPMCNames(pmcProfile);
this.checkForAndRemoveUndefinedDialogs(fullProfile);
2023-03-03 15:23:46 +00:00
}
if (this.seasonalEventService.isAutomaticEventDetectionEnabled())
{
this.seasonalEventService.enableSeasonalEvents(sessionID);
2023-03-03 15:23:46 +00:00
}
if (pmcProfile?.Skills?.Common)
{
this.warnOnActiveBotReloadSkill(pmcProfile);
}
// Flea bsg blacklist is off
if (!this.ragfairConfig.dynamic.blacklist.enableBsgList)
{
this.flagAllItemsInDbAsSellableOnFlea();
}
}
}
protected adjustHideoutCraftTimes(): void
{
const craftTimeOverrideSeconds = this.hideoutConfig.overrideCraftTimeSeconds;
if (craftTimeOverrideSeconds === -1)
{
return;
}
for (const craft of this.databaseServer.getTables().hideout!.production)
{
// Only adjust crafts ABOVE the override
if (craft.productionTime > craftTimeOverrideSeconds)
{
craft.productionTime = craftTimeOverrideSeconds;
}
}
}
protected adjustHideoutBuildTimes(): void
{
const craftTimeOverrideSeconds = this.hideoutConfig.overrideBuildTimeSeconds;
if (craftTimeOverrideSeconds === -1)
{
return;
}
for (const area of this.databaseServer.getTables().hideout!.areas)
{
for (const stageKey of Object.keys(area.stages))
{
const stage = area.stages[stageKey];
// Only adjust crafts ABOVE the override
if (stage.constructionTime > craftTimeOverrideSeconds)
{
stage.constructionTime = craftTimeOverrideSeconds;
}
}
}
}
protected adjustLocationBotValues(): void
{
const mapsDb = this.databaseServer.getTables().locations!;
for (const locationKey in this.botConfig.maxBotCap)
{
const map: ILocation = mapsDb[locationKey];
if (!map)
{
continue;
}
map.base.BotMax = this.botConfig.maxBotCap[locationKey];
// make values no larger than 30 secs
map.base.BotStart = Math.min(map.base.BotStart, 30);
}
}
2023-11-07 20:00:02 +00:00
/**
* Out of date/incorrectly made trader mods forget this data
*/
protected checkTraderRepairValuesExist(): void
{
for (const traderKey in this.databaseServer.getTables().traders)
{
const trader = this.databaseServer.getTables().traders![traderKey];
2023-11-07 20:00:02 +00:00
if (!trader?.base?.repair)
{
2024-05-21 14:28:52 +01:00
this.logger.warning(this.localisationService.getText("trader-missing_repair_property_using_default",
{ traderId: trader.base._id, nickname: trader.base.nickname }));
trader.base.repair = this.cloner.clone(this.databaseServer.getTables().traders!.ragfair.base.repair);
2023-11-07 20:00:02 +00:00
return;
}
2023-11-10 19:00:04 +00:00
if (trader.base.repair?.quality === undefined)
2023-11-07 20:00:02 +00:00
{
2024-05-21 14:28:52 +01:00
this.logger.warning(this.localisationService.getText("trader-missing_repair_quality_property_using_default",
{ traderId: trader.base._id, nickname: trader.base.nickname }));
trader.base.repair.quality = this.cloner.clone(
this.databaseServer.getTables().traders!.ragfair.base.repair.quality,
2023-11-10 16:49:29 -05:00
);
trader.base.repair.quality = this.databaseServer.getTables().traders!.ragfair.base.repair.quality;
2023-11-07 20:00:02 +00:00
}
}
}
protected addCustomLooseLootPositions(): void
{
const looseLootPositionsToAdd = this.lootConfig.looseLoot;
for (const mapId in looseLootPositionsToAdd)
{
if (!mapId)
{
2024-05-21 14:28:52 +01:00
this.logger.warning(this.localisationService.getText("location-unable_to_add_custom_loot_position", mapId));
continue;
}
2024-05-21 14:28:52 +01:00
const mapLooseLoot: ILooseLoot = this.databaseServer.getTables().locations![mapId]?.looseLoot;
if (!mapLooseLoot)
{
2024-05-21 14:28:52 +01:00
this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId));
continue;
}
2024-05-21 14:28:52 +01:00
const positionsToAdd = looseLootPositionsToAdd[mapId];
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,
2023-11-15 20:35:05 -05:00
);
2024-05-21 14:28:52 +01:00
if (existingLootPosition)
{
existingLootPosition.template.Items.push(...positionToAdd.template.Items);
existingLootPosition.itemDistribution.push(...positionToAdd.itemDistribution);
continue;
}
2023-11-10 16:49:29 -05:00
// New position, add entire object
mapLooseLoot.spawnpoints.push(positionToAdd);
}
}
}
protected adjustLooseLootSpawnProbabilities(): void
{
const adjustments = this.lootConfig.looseLootSpawnPointAdjustments;
for (const mapId in adjustments)
{
const mapLooseLootData: ILooseLoot = this.databaseServer.getTables().locations![mapId]?.looseLoot;
if (!mapLooseLootData)
{
2024-05-21 14:28:52 +01:00
this.logger.warning(this.localisationService.getText("location-map_has_no_loose_loot_data", mapId));
continue;
}
const mapLootAdjustmentsDict = adjustments[mapId];
for (const lootKey in mapLootAdjustmentsDict)
{
2024-05-21 14:28:52 +01:00
const lootPostionToAdjust = mapLooseLootData.spawnpoints
.find((spawnPoint) => spawnPoint.template.Id === lootKey);
if (!lootPostionToAdjust)
{
2024-05-21 14:28:52 +01:00
this.logger.warning(this.localisationService.getText("location-unable_to_adjust_loot_position_on_map", { lootKey: lootKey, mapId: mapId }));
continue;
}
lootPostionToAdjust.probability = mapLootAdjustmentsDict[lootKey];
}
}
}
2023-11-15 20:35:05 -05:00
/** Apply custom limits on bot types as defined in configs/location.json/botTypeLimits */
protected adjustMapBotLimits(): void
{
const mapsDb = this.databaseServer.getTables().locations!;
if (!this.locationConfig.botTypeLimits)
{
return;
}
for (const mapId in this.locationConfig.botTypeLimits)
{
const map: ILocation = mapsDb[mapId];
if (!map)
{
2023-11-15 20:35:05 -05:00
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
{
2023-11-10 16:49:29 -05:00
// Bot type not found, add new object
2023-11-15 20:35:05 -05:00
map.base.MinMaxBots.push({
// Bot type not found, add new object
WildSpawnType: botToLimit.type,
min: botToLimit.min,
max: botToLimit.max,
});
}
}
}
}
/**
* Handle client/game/config
*/
public getGameConfig(sessionID: string): IGameConfigResponse
{
2023-07-24 16:38:28 +01:00
const profile = this.profileHelper.getPmcProfile(sessionID);
const gameTime
= profile.Stats?.Eft.OverallCounters.Items?.find(
(counter) => counter.Key.includes("LifeTime") && counter.Key.includes("Pmc"),
)?.Value ?? 0;
2023-07-24 16:38:28 +01:00
const config: IGameConfigResponse = {
languages: this.databaseServer.getTables().locales!.languages,
ndaFree: false,
reportAvailable: false,
twitchEventMember: false,
lang: "en",
aid: profile.aid,
taxonomy: 6,
activeProfileId: sessionID,
backend: {
Lobby: this.httpServerHelper.getBackendUrl(),
Trading: this.httpServerHelper.getBackendUrl(),
Messaging: this.httpServerHelper.getBackendUrl(),
Main: this.httpServerHelper.getBackendUrl(),
2023-11-15 20:35:05 -05:00
RagFair: this.httpServerHelper.getBackendUrl(),
},
useProtobuf: false,
utc_time: new Date().getTime() / 1000,
2023-12-21 22:12:55 +00:00
totalInGame: gameTime,
};
return config;
}
/**
* Handle client/game/mode
*/
public getGameMode(sessionID: string, info: IGameModeRequestData): any
{
return { gameMode: ESessionMode.REGULAR, backendUrl: this.httpServerHelper.getBackendUrl() };
}
/**
* Handle client/server/list
*/
public getServer(sessionId: string): IServerDetails[]
{
return [{ ip: this.httpConfig.backendIp, port: Number.parseInt(this.httpConfig.backendPort) }];
}
/**
* Handle client/match/group/current
*/
public getCurrentGroup(sessionId: string): ICurrentGroupResponse
{
2023-11-15 20:35:05 -05:00
return { squad: [] };
}
/**
* Handle client/checkVersion
*/
public getValidGameVersion(sessionId: string): ICheckVersionResponse
{
2023-11-15 20:35:05 -05:00
return { isvalid: true, latestVersion: this.coreConfig.compatibleTarkovVersion };
}
/**
* Handle client/game/keepalive
*/
public getKeepAlive(sessionId: string): IGameKeepAliveResponse
{
this.profileActivityService.setActivityTimestamp(sessionId);
return { msg: "OK", utc_time: new Date().getTime() / 1000 };
}
/**
* Handle singleplayer/settings/getRaidTime
*/
public getRaidTime(sessionId: string, request: IGetRaidTimeRequest): IGetRaidTimeResponse
{
// Set interval times to in-raid value
this.ragfairConfig.runIntervalSeconds = this.ragfairConfig.runIntervalValues.inRaid;
this.hideoutConfig.runIntervalSeconds = this.hideoutConfig.runIntervalValues.inRaid;
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.databaseServer.getTables().templates!.items;
// Saiga 12ga
// Toz 106
// Remington 870
const shotguns = ["576165642459773c7a400233", "5a38e6bac4a2826c6e06d79b", "5a7828548dc32e5a9c28b516"];
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
*/
protected warnOnActiveBotReloadSkill(pmcProfile: IPmcData): void
{
const botReloadSkill = this.profileHelper.getSkillFromProfile(pmcProfile, SkillTypes.BOT_RELOAD);
if (botReloadSkill?.Progress > 0)
{
this.logger.warning(this.localisationService.getText("server_start_player_active_botreload_skill"));
2023-03-03 15:23:46 +00:00
}
}
protected flagAllItemsInDbAsSellableOnFlea(): void
{
const dbItems = Object.values(this.databaseServer.getTables().templates!.items);
for (const item of dbItems)
{
if (item._type === "Item" && !item._props?.CanSellOnRagfair)
{
item._props.CanSellOnRagfair = true;
}
}
}
2023-03-03 15:23:46 +00:00
/**
* When player logs in, iterate over all active effects and reduce timer
2024-02-03 19:47:39 +00:00
* @param pmcProfile Profile to adjust values for
2023-03-03 15:23:46 +00:00
*/
protected updateProfileHealthValues(pmcProfile: IPmcData): void
{
const healthLastUpdated = pmcProfile.Health.UpdateTime;
const currentTimeStamp = this.timeUtil.getTimestamp();
const diffSeconds = currentTimeStamp - healthLastUpdated;
2024-02-03 19:47:39 +00:00
// Last update is in past
2023-03-03 15:23:46 +00:00
if (healthLastUpdated < currentTimeStamp)
{
// Base values
let energyRegenPerHour = 60;
let hydrationRegenPerHour = 60;
let hpRegenPerHour = 456.6;
// Set new values, whatever is smallest
energyRegenPerHour += pmcProfile.Bonuses.filter(
(bonus) => bonus.type === BonusType.ENERGY_REGENERATION,
).reduce((sum, curr) => sum + (curr.value ?? 0), 0);
hydrationRegenPerHour += pmcProfile.Bonuses.filter(
(bonus) => bonus.type === BonusType.HYDRATION_REGENERATION,
).reduce((sum, curr) => sum + (curr.value ?? 0), 0);
hpRegenPerHour += pmcProfile.Bonuses.filter((bonus) => bonus.type === BonusType.HEALTH_REGENERATION).reduce(
(sum, curr) => sum + (curr.value ?? 0),
2023-11-15 20:35:05 -05:00
0,
);
2023-03-03 15:23:46 +00:00
// Player has energy deficit
2023-03-03 15:23:46 +00:00
if (pmcProfile.Health.Energy.Current !== pmcProfile.Health.Energy.Maximum)
{
// Set new value, whatever is smallest
2023-11-15 20:35:05 -05:00
pmcProfile.Health.Energy.Current += Math.round(energyRegenPerHour * (diffSeconds / 3600));
2023-03-03 15:23:46 +00:00
if (pmcProfile.Health.Energy.Current > pmcProfile.Health.Energy.Maximum)
{
pmcProfile.Health.Energy.Current = pmcProfile.Health.Energy.Maximum;
}
}
2024-02-03 19:47:39 +00:00
// Player has hydration deficit
2023-03-03 15:23:46 +00:00
if (pmcProfile.Health.Hydration.Current !== pmcProfile.Health.Hydration.Maximum)
{
2023-11-15 20:35:05 -05:00
pmcProfile.Health.Hydration.Current += Math.round(hydrationRegenPerHour * (diffSeconds / 3600));
2023-03-03 15:23:46 +00:00
if (pmcProfile.Health.Hydration.Current > pmcProfile.Health.Hydration.Maximum)
{
pmcProfile.Health.Hydration.Current = pmcProfile.Health.Hydration.Maximum;
}
}
// Check all body parts
for (const bodyPartKey in pmcProfile.Health.BodyParts)
{
const bodyPart = pmcProfile.Health.BodyParts[bodyPartKey] as BodyPartHealth;
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
// Check part hp
if (bodyPart.Health.Current < bodyPart.Health.Maximum)
{
2023-11-15 20:35:05 -05:00
bodyPart.Health.Current += Math.round(hpRegenPerHour * (diffSeconds / 3600));
2023-03-03 15:23:46 +00:00
}
if (bodyPart.Health.Current > bodyPart.Health.Maximum)
{
bodyPart.Health.Current = bodyPart.Health.Maximum;
}
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
// Look for effects
if (Object.keys(bodyPart.Effects ?? {}).length > 0)
{
// Decrement effect time value by difference between current time and time health was last updated
for (const effectKey in bodyPart.Effects)
{
2024-02-03 19:47:39 +00:00
// remove effects below 1, .e.g. bleeds at -1
2023-03-03 15:23:46 +00:00
if (bodyPart.Effects[effectKey].Time < 1)
{
// More than 30 mins has passed
if (diffSeconds > 1800)
{
delete bodyPart.Effects[effectKey];
}
2023-03-03 15:23:46 +00:00
continue;
}
bodyPart.Effects[effectKey].Time -= diffSeconds;
if (bodyPart.Effects[effectKey].Time < 1)
{
// effect time was sub 1, set floor it can be
bodyPart.Effects[effectKey].Time = 1;
}
}
}
}
pmcProfile.Health.UpdateTime = currentTimeStamp;
}
}
/**
* 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
{
for (const locationKey in this.databaseServer.getTables().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 = this.databaseServer.getTables().locations![locationKey];
if (!location.base)
{
2023-11-15 20:35:05 -05:00
this.logger.warning(
this.localisationService.getText("location-unable_to_fix_broken_waves_missing_base", locationKey),
);
continue;
}
for (const wave of location.base.waves ?? [])
2023-03-03 15:23:46 +00:00
{
if (wave.slots_max - wave.slots_min === 0)
2023-03-03 15:23:46 +00:00
{
2023-11-15 20:35:05 -05:00
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}`,
);
2023-03-03 15:23:46 +00:00
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 lighthouse = this.databaseServer.getTables().locations!.lighthouse!.base;
2023-03-03 15:23:46 +00:00
for (const wave of lighthouse.BossLocationSpawn)
{
// Find Rogues that spawn instantly
if (wave.BossName === "exUsec" && wave.Time === -1)
{
wave.Time = this.locationConfig.rogueLighthouseSpawnTimeSettings.waitTimeSeconds;
}
}
}
/**
* Send starting gifts to profile after x days
* @param pmcProfile Profile to add gifts to
*/
protected sendPraporGiftsToNewProfiles(pmcProfile: IPmcData): void
{
const timeStampProfileCreated = pmcProfile.Info.RegistrationDate;
const oneDaySeconds = this.timeUtil.getHoursAsSeconds(24);
const currentTimeStamp = this.timeUtil.getTimestamp();
// One day post-profile creation
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds)
{
this.giftService.sendPraporStartingGift(pmcProfile.sessionId, 1);
}
// Two day post-profile creation
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds * 2)
{
this.giftService.sendPraporStartingGift(pmcProfile.sessionId, 2);
}
}
2023-03-03 15:23:46 +00:00
/**
2023-11-10 16:49:29 -05:00
* 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
2023-03-03 15:23:46 +00:00
*/
protected splitBotWavesIntoSingleWaves(): void
{
for (const locationKey in this.databaseServer.getTables().locations)
{
if (this.locationConfig.splitWaveIntoSingleSpawnsSettings.ignoreMaps.includes(locationKey))
{
continue;
}
// Iterate over all maps
const location: ILocation = this.databaseServer.getTables().locations![locationKey];
2023-03-03 15:23:46 +00:00
for (const wave of location.base.waves)
{
// Wave has size that makes it candidate for splitting
2023-11-15 20:35:05 -05:00
if (
wave.slots_max - wave.slots_min
>= this.locationConfig.splitWaveIntoSingleSpawnsSettings.waveSizeThreshold
2023-11-15 20:35:05 -05:00
)
2023-03-03 15:23:46 +00:00
{
// Get count of bots to be spawned in wave
const waveSize = wave.slots_max - wave.slots_min;
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
// Update wave to spawn single bot
wave.slots_min = 1;
wave.slots_max = 2;
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
// Get index of wave
const indexOfWaveToSplit = location.base.waves.indexOf(wave);
2023-11-15 20:35:05 -05:00
this.logger.debug(
`Splitting map: ${location.base.Id} wave: ${indexOfWaveToSplit} with ${waveSize} bots`,
);
2023-03-03 15:23:46 +00:00
// 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);
2023-03-03 15:23:46 +00:00
// Some waves have value of 0 for some reason, preserve
if (waveToAddClone.number !== 0)
2023-03-03 15:23:46 +00:00
{
// Update wave number to new location in array
waveToAddClone.number = index;
2023-03-03 15:23:46 +00:00
}
2023-11-10 16:49:29 -05:00
// Place wave into array in just-edited position + 1
location.base.waves.splice(index, 0, waveToAddClone);
2023-03-03 15:23:46 +00:00
wavesAddedCount++;
}
2023-11-10 16:49:29 -05:00
// Update subsequent wave number property to accommodate the new waves
2023-11-15 20:35:05 -05:00
for (
let index = indexOfWaveToSplit + wavesAddedCount + 1;
index < location.base.waves.length;
index++
)
2023-03-03 15:23:46 +00:00
{
// 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
*/
protected saveActiveModsToProfile(fullProfile: ISptProfile): void
2023-03-03 15:23:46 +00:00
{
// Add empty mod array if undefined
if (!fullProfile.spt.mods)
2023-03-03 15:23:46 +00:00
{
fullProfile.spt.mods = [];
2023-03-03 15:23:46 +00:00
}
// Get active mods
const activeMods = this.preSptModLoader.getImportedModDetails();
2023-03-03 15:23:46 +00:00
for (const modKey in activeMods)
{
const modDetails = activeMods[modKey];
2023-11-15 20:35:05 -05:00
if (
fullProfile.spt.mods.some(
(x) =>
x.author === modDetails.author
&& x.name === modDetails.name
&& x.version === modDetails.version,
2023-11-15 20:35:05 -05:00
)
)
2023-03-03 15:23:46 +00:00
{
// Exists already, skip
continue;
}
fullProfile.spt.mods.push({
2023-03-03 15:23:46 +00:00
author: modDetails.author,
dateAdded: Date.now(),
name: modDetails.name,
2023-11-15 20:35:05 -05:00
version: modDetails.version,
url: modDetails.url,
2023-03-03 15:23:46 +00:00
});
}
}
/**
2023-11-10 16:49:29 -05:00
* Check for any missing assorts inside each traders assort.json data, checking against traders questassort.json
*/
protected validateQuestAssortUnlocksExist(): void
{
const db = this.databaseServer.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
2023-11-15 20:35:05 -05:00
const mergedQuestAssorts = {
...traderData.questassort?.started,
...traderData.questassort?.success,
...traderData.questassort?.fail,
2023-11-15 20:35:05 -05:00
};
2023-11-10 16:49:29 -05:00
// 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])
{
2023-11-10 16:49:29 -05:00
// Reverse lookup of enum key by value
const messageValues = {
traderName: Object.keys(Traders)[Object.values(Traders).indexOf(traderId)],
2023-11-15 20:35:05 -05:00
questName: quests[questKey]?.QuestName ?? "UNKNOWN",
};
this.logger.warning(
2023-11-15 20:35:05 -05:00
this.localisationService.getText("assort-missing_quest_assort_unlock", messageValues),
);
}
}
}
}
2023-03-03 15:23:46 +00:00
/**
* Add the logged in players name to PMC name pool
* @param pmcProfile Profile of player to get name from
2023-03-03 15:23:46 +00:00
*/
protected addPlayerToPMCNames(pmcProfile: IPmcData): void
{
const playerName = pmcProfile.Info.Nickname;
if (playerName)
{
const bots = this.databaseServer.getTables().bots!.types;
2023-03-03 15:23:46 +00:00
if (bots.bear)
2023-03-03 15:23:46 +00:00
{
bots.bear.firstName.push(playerName);
2023-03-03 15:23:46 +00:00
}
2023-11-15 20:35:05 -05:00
if (bots.usec)
2023-03-03 15:23:46 +00:00
{
bots.usec.firstName.push(playerName);
2023-11-15 20:35:05 -05:00
}
2023-03-03 15:23:46 +00:00
}
}
/**
* Check for a dialog with the key 'undefined', and remove it
* @param fullProfile Profile to check for dialog in
*/
protected checkForAndRemoveUndefinedDialogs(fullProfile: ISptProfile): void
{
const undefinedDialog = fullProfile.dialogues.undefined;
if (undefinedDialog)
{
delete fullProfile.dialogues.undefined;
}
}
2023-03-03 15:23:46 +00:00
/**
* Blank out the "test" mail message from prapor
*/
protected removePraporTestMessage(): void
{
2023-11-10 16:49:29 -05:00
// Iterate over all languages (e.g. "en", "fr")
for (const localeKey in this.databaseServer.getTables().locales!.global)
2023-03-03 15:23:46 +00:00
{
this.databaseServer.getTables().locales!.global[localeKey]["61687e2c3e526901fa76baf9"] = "";
2023-03-03 15:23:46 +00:00
}
}
/**
* Make non-trigger-spawned raiders spawn earlier + always
*/
protected adjustLabsRaiderSpawnRate(): void
{
const labsBase = this.databaseServer.getTables().locations!.laboratory!.base;
const nonTriggerLabsBossSpawns = labsBase.BossLocationSpawn.filter(
(x) => x.TriggerId === "" && x.TriggerName === "",
2023-11-15 20:35:05 -05:00
);
2023-03-03 15:23:46 +00:00
if (nonTriggerLabsBossSpawns)
{
for (const boss of nonTriggerLabsBossSpawns)
{
boss.BossChance = 100;
boss.Time /= 10;
}
}
}
protected logProfileDetails(fullProfile: ISptProfile): void
2023-03-03 15:23:46 +00:00
{
this.logger.debug(`Profile made with: ${fullProfile.spt.version}`);
this.logger.debug(
`Server version: ${globalThis.G_SPTVERSION || this.coreConfig.sptVersion} ${globalThis.G_COMMIT}`,
);
2023-03-03 15:23:46 +00:00
this.logger.debug(`Debug enabled: ${globalThis.G_DEBUG_CONFIGURATION}`);
this.logger.debug(`Mods enabled: ${globalThis.G_MODS_ENABLED}`);
}
2023-11-15 20:35:05 -05:00
}