diff --git a/project/src/callbacks/InraidCallbacks.ts b/project/src/callbacks/InraidCallbacks.ts index 12466fad..185d394b 100644 --- a/project/src/callbacks/InraidCallbacks.ts +++ b/project/src/callbacks/InraidCallbacks.ts @@ -42,11 +42,12 @@ export class InraidCallbacks */ public saveProgress(url: string, info: ISaveProgressRequestData, sessionID: string): INullResponseData { - this.inraidController.savePostRaidProgress(info, sessionID); + this.inraidController.savePostRaidProgressLegacy(info, sessionID); return this.httpResponse.nullResponse(); } /** + * TODO - remove * Handle singleplayer/settings/raid/endstate * @returns */ diff --git a/project/src/callbacks/MatchCallbacks.ts b/project/src/callbacks/MatchCallbacks.ts index 97b3b20e..c39a6c78 100644 --- a/project/src/callbacks/MatchCallbacks.ts +++ b/project/src/callbacks/MatchCallbacks.ts @@ -139,9 +139,8 @@ export class MatchCallbacks return this.httpResponse.getBody(true); } - /** @deprecated - not called on raid start/end or game start/exit */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - public putMetrics(url: string, info: IPutMetricsRequestData, sessionId: string): INullResponseData + public putMetrics(url: string, request: IPutMetricsRequestData, sessionId: string): INullResponseData { return this.httpResponse.nullResponse(); } diff --git a/project/src/controllers/InraidController.ts b/project/src/controllers/InraidController.ts index 43daf162..39f322ef 100644 --- a/project/src/controllers/InraidController.ts +++ b/project/src/controllers/InraidController.ts @@ -120,7 +120,7 @@ export class InraidController * @param offraidData post-raid request data * @param sessionID Session id */ - public savePostRaidProgress(offraidData: ISaveProgressRequestData, sessionID: string): void + public savePostRaidProgressLegacy(offraidData: ISaveProgressRequestData, sessionID: string): void { this.logger.debug(`Raid outcome: ${offraidData.exit}`); @@ -159,7 +159,7 @@ export class InraidController const serverPmcProfile = serverProfile.characters.pmc; const serverScavProfile = serverProfile.characters.scav; - const isDead = this.isPlayerDead(postRaidRequest.exit); + const isDead = true;// this.isPlayerDead(postRaidRequest.exit); const preRaidGear = this.inRaidHelper.getPlayerGear(serverPmcProfile.Inventory.items); serverProfile.inraid.character = "pmc"; @@ -355,7 +355,7 @@ export class InraidController { const serverPmcProfile = this.profileHelper.getPmcProfile(sessionID); const serverScavProfile = this.profileHelper.getScavProfile(sessionID); - const isDead = this.isPlayerDead(postRaidRequest.exit); + const isDead = true;// this.isPlayerDead(postRaidRequest.exit); const preRaidScavCharismaProgress = this.profileHelper.getSkillFromProfile( serverScavProfile, SkillTypes.CHARISMA, @@ -537,16 +537,6 @@ export class InraidController } } - /** - * Is the player dead after a raid - dead is anything other than "survived" / "runner" - * @param statusOnExit exit value from offraidData object - * @returns true if dead - */ - protected isPlayerDead(statusOnExit: PlayerRaidEndState): boolean - { - return statusOnExit !== PlayerRaidEndState.SURVIVED && statusOnExit !== PlayerRaidEndState.RUNNER; - } - /** * Mark inventory items as FiR if player survived raid, otherwise remove FiR from them * @param offraidData Save Progress Request diff --git a/project/src/controllers/LocationController.ts b/project/src/controllers/LocationController.ts index fd77a79d..25b0f7c1 100644 --- a/project/src/controllers/LocationController.ts +++ b/project/src/controllers/LocationController.ts @@ -90,13 +90,13 @@ export class LocationController } // Check for a loot multipler adjustment in app context and apply if one is found - let locationConfigCopy: ILocationConfig; + let locationConfigClone: ILocationConfig; const raidAdjustments = this.applicationContext .getLatestValue(ContextVariableType.RAID_ADJUSTMENTS) ?.getValue(); if (raidAdjustments) { - locationConfigCopy = this.cloner.clone(this.locationConfig); // Clone values so they can be used to reset originals later + locationConfigClone = this.cloner.clone(this.locationConfig); // Clone values so they can be used to reset originals later this.raidTimeAdjustmentService.makeAdjustmentsToMap(raidAdjustments, locationBaseClone); } @@ -128,8 +128,8 @@ export class LocationController if (raidAdjustments) { this.logger.debug("Resetting loot multipliers back to their original values"); - this.locationConfig.staticLootMultiplier = locationConfigCopy.staticLootMultiplier; - this.locationConfig.looseLootMultiplier = locationConfigCopy.looseLootMultiplier; + this.locationConfig.staticLootMultiplier = locationConfigClone.staticLootMultiplier; + this.locationConfig.looseLootMultiplier = locationConfigClone.looseLootMultiplier; this.applicationContext.clearValues(ContextVariableType.RAID_ADJUSTMENTS); } diff --git a/project/src/controllers/MatchController.ts b/project/src/controllers/MatchController.ts index 18c7e8d3..675c727b 100644 --- a/project/src/controllers/MatchController.ts +++ b/project/src/controllers/MatchController.ts @@ -1,13 +1,17 @@ import { inject, injectable } from "tsyringe"; import { ApplicationContext } from "@spt/context/ApplicationContext"; import { ContextVariableType } from "@spt/context/ContextVariableType"; +import { InraidController } from "@spt/controllers/InraidController"; import { LocationController } from "@spt/controllers/LocationController"; import { LootGenerator } from "@spt/generators/LootGenerator"; +import { HealthHelper } from "@spt/helpers/HealthHelper"; +import { InRaidHelper } from "@spt/helpers/InRaidHelper"; import { ProfileHelper } from "@spt/helpers/ProfileHelper"; import { TraderHelper } from "@spt/helpers/TraderHelper"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; +import { Common } from "@spt/models/eft/common/tables/IBotBase"; import { Item } from "@spt/models/eft/common/tables/IItem"; -import { IEndLocalRaidRequestData } from "@spt/models/eft/match/IEndLocalRaidRequestData"; +import { IEndLocalRaidRequestData, IEndRaidResult } from "@spt/models/eft/match/IEndLocalRaidRequestData"; import { IEndOfflineRaidRequestData } from "@spt/models/eft/match/IEndOfflineRaidRequestData"; import { IGetRaidConfigurationRequestData } from "@spt/models/eft/match/IGetRaidConfigurationRequestData"; import { IMatchGroupStartGameRequest } from "@spt/models/eft/match/IMatchGroupStartGameRequest"; @@ -19,9 +23,11 @@ import { IStartLocalRaidResponseData } from "@spt/models/eft/match/IStartLocalRa import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { MessageType } from "@spt/models/enums/MessageType"; import { Traders } from "@spt/models/enums/Traders"; +import { IHideoutConfig } from "@spt/models/spt/config/IHideoutConfig"; import { IInRaidConfig } from "@spt/models/spt/config/IInRaidConfig"; import { IMatchConfig } from "@spt/models/spt/config/IMatchConfig"; import { IPmcConfig } from "@spt/models/spt/config/IPmcConfig"; +import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig"; import { ITraderConfig } from "@spt/models/spt/config/ITraderConfig"; import { ILogger } from "@spt/models/spt/utils/ILogger"; import { ConfigServer } from "@spt/servers/ConfigServer"; @@ -43,6 +49,8 @@ export class MatchController protected inRaidConfig: IInRaidConfig; protected traderConfig: ITraderConfig; protected pmcConfig: IPmcConfig; + protected ragfairConfig: IRagfairConfig; + protected hideoutConfig: IHideoutConfig; constructor( @inject("PrimaryLogger") protected logger: ILogger, @@ -53,6 +61,9 @@ export class MatchController @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("DatabaseService") protected databaseService: DatabaseService, @inject("LocationController") protected locationController: LocationController, + @inject("InraidController") protected inRaidController: InraidController, + @inject("InRaidHelper") protected inRaidHelper: InRaidHelper, + @inject("HealthHelper") protected healthHelper: HealthHelper, @inject("MatchLocationService") protected matchLocationService: MatchLocationService, @inject("TraderHelper") protected traderHelper: TraderHelper, @inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService, @@ -68,6 +79,8 @@ export class MatchController this.inRaidConfig = this.configServer.getConfig(ConfigTypes.IN_RAID); this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER); this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC); + this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); + this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT); } public getEnabled(): boolean @@ -359,7 +372,7 @@ export class MatchController const playerProfile = this.profileHelper.getPmcProfile(sessionId); const result: IStartLocalRaidResponseData = { - serverId: this.hashUtil.generate(), // TODO - does this need to be more verbose - investigate client? + serverId: `${request.location}.${request.playerSide}.${this.timeUtil.getTimestamp()}`, // TODO - does this need to be more verbose - investigate client? serverSettings: this.databaseService.getLocationServices(), // TODO - is this per map or global? profile: { insuredItems: playerProfile.InsuredItems }, locationLoot: this.locationController.generate(request.location), @@ -370,7 +383,10 @@ export class MatchController public endLocalRaid(sessionId: string, request: IEndLocalRaidRequestData): void { - const playerProfile = this.profileHelper.getPmcProfile(sessionId); + const fullProfile = this.profileHelper.getFullProfile(sessionId); + const pmcProfile = fullProfile.characters.pmc; + const scavProfile = fullProfile.characters.scav; + const postRaidProfile = request.results.profile!; // TODO: // Update profile @@ -382,7 +398,101 @@ export class MatchController // Limb health // Limb effects // Skills - // Inventory + // Inventory - items not lost on death // Stats + // stats/eft/aggressor - weird values (EFT.IProfileDataContainer.Nickname) + + this.logger.debug(`Raid outcome: ${request.results.result}`); + + // Set flea interval time to out-of-raid value + this.ragfairConfig.runIntervalSeconds = this.ragfairConfig.runIntervalValues.outOfRaid; + this.hideoutConfig.runIntervalSeconds = this.hideoutConfig.runIntervalValues.outOfRaid; + + // ServerId has various info stored in it, delimited by a period + const serverDetails = request.serverId.split("."); + + const locationName = serverDetails[0].toLowerCase(); + const isPmc = serverDetails[1].toLowerCase() === "pmc"; + const map = this.databaseService.getLocation(locationName).base; + const isDead = this.isPlayerDead(request.results); + + // Update inventory + this.inRaidHelper.setInventory(sessionId, pmcProfile, postRaidProfile); + + pmcProfile.Info.Level = postRaidProfile.Info.Level; + + // Add experience points + pmcProfile.Info.Experience += postRaidProfile.Stats.Eft.TotalSessionExperience; + + // Profile common/mastering skills + pmcProfile.Skills = postRaidProfile.Skills; + + pmcProfile.Stats.Eft = postRaidProfile.Stats.Eft; + + // Must occur after experience is set and stats copied over + pmcProfile.Stats.Eft.TotalSessionExperience = 0; + + pmcProfile.Achievements = postRaidProfile.Achievements; + + // Remove skill fatigue values + this.resetSkillPointsEarnedDuringRaid(pmcProfile.Skills.Common); + + // Straight copy + pmcProfile.TaskConditionCounters = postRaidProfile.TaskConditionCounters; + + pmcProfile.Encyclopedia = postRaidProfile.Encyclopedia; + + // Must occur after encyclopedia updated + this.mergePmcAndScavEncyclopedias(pmcProfile, scavProfile); + + // Handle temp, hydration, limb hp/effects + this.healthHelper.updateProfileHealthPostRaid(pmcProfile, postRaidProfile.Health, sessionId, isDead); + } + + /** + * Is the player dead after a raid - dead = anything other than "survived" / "runner" + * @param statusOnExit Exit value from offraidData object + * @returns true if dead + */ + protected isPlayerDead(results: IEndRaidResult): boolean + { + return ["killed", "missinginaction"].includes(results.result.toLowerCase()); + } + + /** + * Reset the skill points earned in a raid to 0, ready for next raid + * @param commonSkills Profile common skills to update + */ + protected resetSkillPointsEarnedDuringRaid(commonSkills: Common[]): void + { + for (const skill of commonSkills) + { + skill.PointsEarnedDuringSession = 0.0; + } + } + + /** + * merge two dictionaries together + * Prioritise pair that has true as a value + * @param primary main dictionary + * @param secondary Secondary dictionary + */ + protected mergePmcAndScavEncyclopedias(primary: IPmcData, secondary: IPmcData): void + { + function extend(target: { [key: string]: boolean }, source: Record) + { + for (const key in source) + { + if (Object.hasOwn(source, key)) + { + target[key] = source[key]; + } + } + return target; + } + + const merged = extend(extend({}, primary.Encyclopedia), secondary.Encyclopedia); + primary.Encyclopedia = merged; + secondary.Encyclopedia = merged; } } diff --git a/project/src/helpers/HealthHelper.ts b/project/src/helpers/HealthHelper.ts index 73d0bb5c..801cbe5b 100644 --- a/project/src/helpers/HealthHelper.ts +++ b/project/src/helpers/HealthHelper.ts @@ -1,5 +1,6 @@ import { inject, injectable } from "tsyringe"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; +import { BodyPartsHealth, Health } from "@spt/models/eft/common/tables/IBotBase"; import { ISyncHealthRequestData } from "@spt/models/eft/health/ISyncHealthRequestData"; import { Effects, ISptProfile } from "@spt/models/eft/profile/ISptProfile"; import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; @@ -66,6 +67,113 @@ export class HealthHelper return profile; } + /** + * Update player profile vitality values with changes from client request object + * @param pmcData Player profile + * @param postRaidHealth Post raid data + * @param sessionID Session id + * @param isDead Is player dead + * @param addEffects Should effects be added to profile (default - true) + * @param deleteExistingEffects Should all prior effects be removed before apply new ones (default - true) + */ + public updateProfileHealthPostRaid( + pmcData: IPmcData, + postRaidHealth: Health, + sessionID: string, + isDead: boolean, + ): void + { + const fullProfile = this.saveServer.getProfile(sessionID); + + this.storeHydrationEnergyTempInProfile( + fullProfile, + postRaidHealth.Hydration.Current, + postRaidHealth.Energy.Current, + postRaidHealth.Temperature.Current); + + // Store limb effects from post-raid in profile + for (const bodyPart in postRaidHealth.BodyParts) + { + // Effects + if (postRaidHealth.BodyParts[bodyPart].Effects) + { + fullProfile.vitality.effects[bodyPart] = postRaidHealth.BodyParts[bodyPart].Effects; + } + + // Limb hp + if (!isDead) + { + // Player alive, not is limb alive + fullProfile.vitality.health[bodyPart] = postRaidHealth.BodyParts[bodyPart].Current; + } + else + { + fullProfile.vitality.health[bodyPart] + = pmcData.Health.BodyParts[bodyPart].Health.Maximum * this.healthConfig.healthMultipliers.death; + } + } + + this.transferPostRaidLimbEffectsToProfile(postRaidHealth.BodyParts, pmcData); + + // Adjust hydration/energy/temp and limb hp using temp storage hydated above + this.saveHealth(pmcData, sessionID); + + // Reset temp storage + this.resetVitality(sessionID); + + // Update last edited timestamp + pmcData.Health.UpdateTime = this.timeUtil.getTimestamp(); + } + + protected storeHydrationEnergyTempInProfile( + fullProfile: ISptProfile, + hydration: number, + energy: number, + temprature: number): void + { + fullProfile.vitality.health.Hydration = hydration; + fullProfile.vitality.health.Energy = energy; + fullProfile.vitality.health.Temperature = temprature; + } + + /** + * Take body part effects from client profile and apply to server profile + * @param postRaidBodyParts Post-raid body part data + * @param profileData Player profile on server + */ + protected transferPostRaidLimbEffectsToProfile( + postRaidBodyParts: BodyPartsHealth, + profileData: IPmcData, + ): void + { + // Iterate over each body part + for (const bodyPartId in postRaidBodyParts) + { + // Get effects on body part from profile + const bodyPartEffects = postRaidBodyParts[bodyPartId].Effects; + for (const effect in bodyPartEffects) + { + const effectDetails = bodyPartEffects[effect]; + + // Null guard + if (!profileData.Health.BodyParts[bodyPartId].Effects) + { + profileData.Health.BodyParts[bodyPartId].Effects = {}; + } + + // Already exists on server profile, skip + const profileBodyPartEffects = profileData.Health.BodyParts[bodyPartId].Effects; + if (profileBodyPartEffects[effect]) + { + continue; + } + + // Add effect to server profile + profileBodyPartEffects[effect] = { Time: effectDetails.Time ?? -1 }; + } + } + } + /** * Update player profile vitality values with changes from client request object * @param pmcData Player profile @@ -83,13 +191,14 @@ export class HealthHelper ): void { const postRaidBodyParts = request.Health; // post raid health settings - const profile = this.saveServer.getProfile(sessionID); - const profileHealth = profile.vitality.health; - const profileEffects = profile.vitality.effects; + const fullProfile = this.saveServer.getProfile(sessionID); + const profileEffects = fullProfile.vitality.effects; - profileHealth.Hydration = request.Hydration!; - profileHealth.Energy = request.Energy!; - profileHealth.Temperature = request.Temperature!; + this.storeHydrationEnergyTempInProfile( + fullProfile, + request.Hydration!, + request.Energy!, + request.Temperature!); // Process request data into profile for (const bodyPart in postRaidBodyParts) @@ -103,11 +212,11 @@ export class HealthHelper if (request.IsAlive) { // Player alive, not is limb alive - profileHealth[bodyPart] = postRaidBodyParts[bodyPart].Current; + fullProfile.vitality.health[bodyPart] = postRaidBodyParts[bodyPart].Current; } else { - profileHealth[bodyPart] + fullProfile.vitality.health[bodyPart] = pmcData.Health.BodyParts[bodyPart].Health.Maximum * this.healthConfig.healthMultipliers.death; } } diff --git a/project/src/models/eft/match/IEndLocalRaidRequestData.ts b/project/src/models/eft/match/IEndLocalRaidRequestData.ts index eca97396..6e3cce20 100644 --- a/project/src/models/eft/match/IEndLocalRaidRequestData.ts +++ b/project/src/models/eft/match/IEndLocalRaidRequestData.ts @@ -5,7 +5,7 @@ import { Item } from "../common/tables/IItem"; export interface IEndLocalRaidRequestData { serverId: string - result: IEndRaidResult + results: IEndRaidResult lostInsuredItems: Item[] transferItems: Record } @@ -13,6 +13,7 @@ export interface IEndLocalRaidRequestData export interface IEndRaidResult { profile: IPmcData + result: string ExitStatus: ExitStatus killerId: string killerAid: string diff --git a/project/src/models/eft/match/IPutMetricsRequestData.ts b/project/src/models/eft/match/IPutMetricsRequestData.ts index 198b256d..2eea6cd8 100644 --- a/project/src/models/eft/match/IPutMetricsRequestData.ts +++ b/project/src/models/eft/match/IPutMetricsRequestData.ts @@ -2,10 +2,63 @@ export interface IPutMetricsRequestData { sid: string settings: any - SharedSettings: any - HardwareDescription: any + SharedSettings: ISharedSettings + HardwareDescription: IHardwareDescription Location: string Metrics: any - ClientEvents: any + ClientEvents: IClientEvents SpikeSamples: any[] + mode: string +} + +export interface ISharedSettings +{ + StatedFieldOfView: number +} + +export interface IHardwareDescription +{ + deviceUniqueIdentifier: string + systemMemorySize: number + graphicsDeviceID: number + graphicsDeviceName: string + graphicsDeviceType: string + graphicsDeviceVendor: string + graphicsDeviceVendorID: number + graphicsDeviceVersion: string + graphicsMemorySize: number + graphicsMultiThreaded: boolean + graphicsShaderLevel: number + operatingSystem: string + processorCount: number + processorFrequency: number + processorType: string + driveType: string + swapDriveType: string +} + +export interface IClientEvents +{ + MatchingCompleted: number + MatchingCompletedReal: number + LocationLoaded: number + LocationLoadedReal: number + GamePrepared: number + GamePreparedReal: number + GameCreated: number + GameCreatedReal: number + GamePooled: number + GamePooledReal: number + GameRunned: number + GameRunnedReal: number + GameSpawn: number + GameSpawnReal: number + PlayerSpawnEvent: number + PlayerSpawnEventReal: number + GameSpawned: number + GameSpawnedReal: number + GameStarting: number + GameStartingReal: number + GameStarted: number + GameStartedReal: number }