diff --git a/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs index 025ad36c..d186b641 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/HealthHelper.cs @@ -37,40 +37,40 @@ public class HealthHelper( /// /// Update player profile vitality values with changes from client request object /// - /// Player profile - /// Post raid data /// Session id - /// Is player dead - public void UpdateProfileHealthPostRaid( - PmcData pmcData, - BotBaseHealth postRaidHealth, + /// Player profile to apply changes to + /// Changes to apply + /// OPTIONAL - Is player dead + public void ApplyHealthChangesToProfile( string sessionID, - bool isDead + PmcData pmcProfileToUpdate, + BotBaseHealth healthChanges, + bool isDead = false ) { var fullProfile = _saveServer.GetProfile(sessionID); var profileEdition = fullProfile.ProfileInfo.Edition; var profileSide = fullProfile.CharacterData.PmcData.Info.Side; - // Get matching 'side e.g. USEC + // Get matching 'side' e.g. USEC var matchingSide = _profileHelper.GetProfileTemplateForSide(profileEdition, profileSide); var defaultTemperature = matchingSide?.Character?.Health?.Temperature ?? new CurrentMinMax { Current = 36.6 }; fullProfile.StoreHydrationEnergyTempInProfile( - postRaidHealth.Hydration.Current ?? 0, - postRaidHealth.Energy.Current ?? 0, + healthChanges.Hydration.Current ?? 0, + healthChanges.Energy.Current ?? 0, defaultTemperature.Current ?? 0 // Reset profile temp to the default to prevent very cold/hot temps persisting into next raid ); // Store limb effects from post-raid in profile - foreach (var bodyPart in postRaidHealth.BodyParts) + foreach (var bodyPart in healthChanges.BodyParts) { // Effects - if (postRaidHealth.BodyParts[bodyPart.Key].Effects is not null) + if (healthChanges.BodyParts[bodyPart.Key].Effects is not null) { - fullProfile.VitalityData.Health[bodyPart.Key].Effects = postRaidHealth + fullProfile.VitalityData.Health[bodyPart.Key].Effects = healthChanges .BodyParts[bodyPart.Key] .Effects; } @@ -80,75 +80,77 @@ public class HealthHelper( // Player alive, not is limb alive { fullProfile.VitalityData.Health[bodyPart.Key].Health.Current = - postRaidHealth.BodyParts[bodyPart.Key].Health.Current ?? 0; + healthChanges.BodyParts[bodyPart.Key].Health.Current ?? 0; } else { fullProfile.VitalityData.Health[bodyPart.Key].Health.Current = - pmcData.Health.BodyParts[bodyPart.Key].Health.Maximum + pmcProfileToUpdate.Health.BodyParts[bodyPart.Key].Health.Maximum * _healthConfig.HealthMultipliers.Death ?? 0; } } - - TransferPostRaidLimbEffectsToProfile(postRaidHealth.BodyParts, pmcData); + // Alter saved profiles Health with values from post-raid client data + ModifyProfileHeathProperties( + healthChanges.BodyParts, + pmcProfileToUpdate, + ["Dehydration", "Exhaustion"] + ); // Adjust hydration/energy/temp and limb hp using temp storage hydrated above - SaveHealth(pmcData, sessionID); + SaveHealth(pmcProfileToUpdate, sessionID); // Reset temp storage ResetVitality(sessionID); // Update last edited timestamp - pmcData.Health.UpdateTime = _timeUtil.GetTimeStamp(); + pmcProfileToUpdate.Health.UpdateTime = _timeUtil.GetTimeStamp(); } /// - /// Take body part effects from client profile and apply to server profile + /// Apply Health values to profile /// - /// Post-raid body part data - /// Player profile on server - protected void TransferPostRaidLimbEffectsToProfile( - Dictionary postRaidBodyParts, - PmcData profileData + /// Changes to apply + /// Player profile on server + /// + protected void ModifyProfileHeathProperties( + Dictionary bodyPartChanges, + PmcData profileToAdjust, + HashSet? effectsToSkip = null ) { - // Iterate over each body part - HashSet effectsToIgnore = ["Dehydration", "Exhaustion"]; - foreach (var bodyPartId in postRaidBodyParts) + foreach (var (partName, partProperties) in bodyPartChanges) { - // Get effects on body part from profile - var bodyPartEffects = postRaidBodyParts[bodyPartId.Key].Effects; - foreach (var (key, effectDetails) in bodyPartEffects) + // Process each effect for each part + foreach (var (key, effectDetails) in partProperties.Effects) { // Null guard - profileData.Health.BodyParts[bodyPartId.Key].Effects ??= - new Dictionary(); + var matchingProfilePart = profileToAdjust.Health.BodyParts[partName]; + matchingProfilePart.Effects ??= new Dictionary(); // Effect already exists on limb in server profile, skip - var profileBodyPartEffects = profileData.Health.BodyParts[bodyPartId.Key].Effects; - if (profileBodyPartEffects.ContainsKey(key)) + if (matchingProfilePart.Effects.ContainsKey(key)) { - if (effectsToIgnore.Contains(key)) - // Get rid of certain effects we don't want to persist out of raid + // Edge case - effect already exists at destination, but we don't want to overwrite details + if (effectsToSkip is not null && effectsToSkip.Contains(key)) { - profileBodyPartEffects[key] = null; + matchingProfilePart.Effects[key] = null; } continue; } - if (effectsToIgnore.Contains(key)) - // Do not pass some effects to out of raid profile + if (effectsToSkip is not null && effectsToSkip.Contains(key)) + // Do not pass skipped effect into profile { continue; } var effectToAdd = new BodyPartEffectProperties { Time = effectDetails.Time ?? -1 }; // Add effect to server profile - if (profileBodyPartEffects.TryAdd(key, effectToAdd)) + if (matchingProfilePart.Effects.TryAdd(key, effectToAdd)) { - profileBodyPartEffects[key] = effectToAdd; + matchingProfilePart.Effects[key] = effectToAdd; } } } @@ -187,22 +189,21 @@ public class HealthHelper( pmcData.Health.Energy.Current = Math.Round(profileHealth.Energy ?? 0); pmcData.Health.Temperature.Current = Math.Round(profileHealth.Temperature ?? 0); - foreach (var bodyPart in pmcData.Health.BodyParts) + foreach (var (partName, partProperties) in pmcData.Health.BodyParts) { - if (profileHealth.Health[bodyPart.Key].Health.Maximum > bodyPart.Value.Health.Maximum) + var matchingProfilePart = profileHealth.Health[partName]; + if (matchingProfilePart.Health.Maximum > partProperties.Health.Maximum) { - profileHealth.Health[bodyPart.Key].Health.Maximum = bodyPart.Value.Health.Maximum; + matchingProfilePart.Health.Maximum = partProperties.Health.Maximum; } - if (profileHealth.Health[bodyPart.Key].Health.Current == 0) + if (matchingProfilePart.Health.Current == 0) { - profileHealth.Health[bodyPart.Key].Health.Current = - bodyPart.Value.Health.Maximum * _healthConfig.HealthMultipliers.Blacked; + matchingProfilePart.Health.Current = + partProperties.Health.Maximum * _healthConfig.HealthMultipliers.Blacked; } - bodyPart.Value.Health.Current = Math.Round( - profileHealth.Health[bodyPart.Key].Health.Current ?? 0 - ); + partProperties.Health.Current = Math.Round(matchingProfilePart.Health.Current ?? 0); } } diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 576803e3..1d9882d3 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -459,7 +459,7 @@ public class LocationLifecycleService { // Manually store the map player just left request.LocationTransit.SptLastVisitedLocation = locationName; - // TODO - Persist each players last visited location history over multiple transits, e.g using InMemoryCacheService, need to take care to not let data get stored forever + // TODO - Persist each players last visited location history over multiple transits, e.g. using InMemoryCacheService, need to take care to not let data get stored forever // Store transfer data for later use in `startLocalRaid()` when next raid starts request.LocationTransit.SptExitName = request.Results.ExitName; _profileActivityService.GetProfileActivityRaidData(sessionId).LocationTransit = @@ -881,7 +881,7 @@ public class LocationLifecycleService /// Handles PMC Profile after the raid /// /// Player id - /// Pmc profile + /// Pmc profile from server /// Scav profile /// Player died/got left behind in raid /// Not same as opposite of `isDead`, specific status @@ -890,7 +890,7 @@ public class LocationLifecycleService /// Current finished Raid location protected void HandlePostRaidPmc( string sessionId, - SptProfile fullProfile, + SptProfile fullServerProfile, PmcData scavProfile, bool isDead, bool isSurvived, @@ -899,78 +899,84 @@ public class LocationLifecycleService string locationName ) { - var pmcProfile = fullProfile.CharacterData.PmcData; + var serverPmcProfile = fullServerProfile.CharacterData.PmcData; var postRaidProfile = request.Results.Profile; - var preRaidProfileQuestDataClone = _cloner.Clone(pmcProfile.Quests); + var preRaidProfileQuestDataClone = _cloner.Clone(serverPmcProfile.Quests); // MUST occur BEFORE inventory actions (setInventory()) occur // Player died, get quest items they lost for use later var lostQuestItems = postRaidProfile.GetQuestItemsInProfile(); // Update inventory - _inRaidHelper.SetInventory(sessionId, pmcProfile, postRaidProfile, isSurvived, isTransfer); + _inRaidHelper.SetInventory( + sessionId, + serverPmcProfile, + postRaidProfile, + isSurvived, + isTransfer + ); - pmcProfile.Info.Level = postRaidProfile.Info.Level; - pmcProfile.Skills = postRaidProfile.Skills; - pmcProfile.Stats.Eft = postRaidProfile.Stats.Eft; - pmcProfile.Encyclopedia = postRaidProfile.Encyclopedia; - pmcProfile.TaskConditionCounters = postRaidProfile.TaskConditionCounters; - pmcProfile.SurvivorClass = postRaidProfile.SurvivorClass; + serverPmcProfile.Info.Level = postRaidProfile.Info.Level; + serverPmcProfile.Skills = postRaidProfile.Skills; + serverPmcProfile.Stats.Eft = postRaidProfile.Stats.Eft; + serverPmcProfile.Encyclopedia = postRaidProfile.Encyclopedia; + serverPmcProfile.TaskConditionCounters = postRaidProfile.TaskConditionCounters; + serverPmcProfile.SurvivorClass = postRaidProfile.SurvivorClass; // MUST occur prior to profile achievements being overwritten by post-raid achievements - ProcessAchievementRewards(fullProfile, postRaidProfile.Achievements); + ProcessAchievementRewards(fullServerProfile, postRaidProfile.Achievements); - pmcProfile.Achievements = postRaidProfile.Achievements; - pmcProfile.Quests = ProcessPostRaidQuests(postRaidProfile.Quests); + serverPmcProfile.Achievements = postRaidProfile.Achievements; + serverPmcProfile.Quests = ProcessPostRaidQuests(postRaidProfile.Quests); // Handle edge case - must occur AFTER processPostRaidQuests() LightkeeperQuestWorkaround( sessionId, postRaidProfile.Quests, preRaidProfileQuestDataClone, - pmcProfile + serverPmcProfile ); - pmcProfile.WishList = postRaidProfile.WishList; + serverPmcProfile.WishList = postRaidProfile.WishList; - pmcProfile.Info.Experience = postRaidProfile.Info.Experience; + serverPmcProfile.Info.Experience = postRaidProfile.Info.Experience; - ApplyTraderStandingAdjustments(pmcProfile.TradersInfo, postRaidProfile.TradersInfo); + ApplyTraderStandingAdjustments(serverPmcProfile.TradersInfo, postRaidProfile.TradersInfo); // Must occur AFTER experience is set and stats copied over - pmcProfile.Stats.Eft.TotalSessionExperience = 0; + serverPmcProfile.Stats.Eft.TotalSessionExperience = 0; const string fenceId = Traders.FENCE; // Clamp fence standing var currentFenceStanding = postRaidProfile.TradersInfo[fenceId].Standing; - pmcProfile.TradersInfo[fenceId].Standing = Math.Min( + serverPmcProfile.TradersInfo[fenceId].Standing = Math.Min( Math.Max((double)currentFenceStanding, -7), 15 ); // Ensure it stays between -7 and 15 // Copy fence values to Scav - scavProfile.TradersInfo[fenceId] = pmcProfile.TradersInfo[fenceId]; + scavProfile.TradersInfo[fenceId] = serverPmcProfile.TradersInfo[fenceId]; // MUST occur AFTER encyclopedia updated - MergePmcAndScavEncyclopedias(pmcProfile, scavProfile); + MergePmcAndScavEncyclopedias(serverPmcProfile, scavProfile); // Handle temp, hydration, limb hp/effects - _healthHelper.UpdateProfileHealthPostRaid( - pmcProfile, - postRaidProfile.Health, + _healthHelper.ApplyHealthChangesToProfile( sessionId, + serverPmcProfile, + postRaidProfile.Health, isDead ); if (isTransfer) { // Adjust limb hp and effects while transiting - UpdateLimbValuesAfterTransit(pmcProfile.Health); + UpdateLimbValuesAfterTransit(serverPmcProfile.Health); } // This must occur _BEFORE_ `deleteInventory`, as that method clears insured items - HandleInsuredItemLostEvent(sessionId, pmcProfile, request, locationName); + HandleInsuredItemLostEvent(sessionId, serverPmcProfile, request, locationName); if (isDead) { @@ -978,7 +984,11 @@ public class LocationLifecycleService // MUST occur AFTER quests have post raid quest data has been merged "processPostRaidQuests()" // Player is dead + had quest items, check and fix any broken find item quests { - CheckForAndFixPickupQuestsAfterDeath(sessionId, lostQuestItems, pmcProfile.Quests); + CheckForAndFixPickupQuestsAfterDeath( + sessionId, + lostQuestItems, + serverPmcProfile.Quests + ); } if (postRaidProfile.Stats.Eft.Aggressor is not null) @@ -987,16 +997,16 @@ public class LocationLifecycleService postRaidProfile.Stats.Eft.Aggressor.ProfileId = request.Results.KillerId; _pmcChatResponseService.SendKillerResponse( sessionId, - pmcProfile, + serverPmcProfile, postRaidProfile.Stats.Eft.Aggressor ); } - _inRaidHelper.DeleteInventory(pmcProfile, sessionId); + _inRaidHelper.DeleteInventory(serverPmcProfile, sessionId); _inRaidHelper.RemoveFiRStatusFromItemsInContainer( sessionId, - pmcProfile, + serverPmcProfile, "SecuredContainer" ); } @@ -1012,7 +1022,7 @@ public class LocationLifecycleService if (victims?.Count > 0) // Player killed PMCs, send some mail responses to them { - _pmcChatResponseService.SendVictimResponse(sessionId, victims, pmcProfile); + _pmcChatResponseService.SendVictimResponse(sessionId, victims, serverPmcProfile); } }