using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Exceptions.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; using BodyPartHealth = SPTarkov.Server.Core.Models.Eft.Common.Tables.BodyPartHealth; namespace SPTarkov.Server.Core.Helpers; [Injectable] public class HealthHelper(ISptLogger logger, TimeUtil timeUtil, ConfigServer configServer) { protected readonly HealthConfig HealthConfig = configServer.GetConfig(); protected readonly HashSet EffectsToSkip = ["Dehydration", "Exhaustion"]; /// /// Update player profile vitality values with changes from client request object /// /// Session id /// Player profile to apply changes to /// Changes to apply public void ApplyHealthChangesToProfile(MongoId sessionId, PmcData pmcProfileToUpdate, BotBaseHealth healthChanges) { /* TODO: Not used here, need to check node or a live profile, commented out for now to avoid the potential alloc - Cj var fullProfile = saveServer.GetProfile(sessionId); var profileEdition = fullProfile.ProfileInfo?.Edition; var profileSide = fullProfile.CharacterData?.PmcData?.Info?.Side; // Get matching 'side' e.g. USEC var matchingSide = profileHelper.GetProfileTemplateForSide(profileEdition, profileSide); var defaultTemperature = matchingSide?.Character?.Health?.Temperature ?? new CurrentMinMax { Current = 36.6 }; */ if (healthChanges.BodyParts is null) { const string message = "healthChanges.BodyParts is null when trying to apply health changes"; logger.Error(message); throw new HealthHelperException(message); } // Alter saved profiles Health with values from post-raid client data ModifyProfileHealthProperties(pmcProfileToUpdate, healthChanges.BodyParts, EffectsToSkip); // Adjust hydration/energy/temperature AdjustProfileHydrationEnergyTemperature(pmcProfileToUpdate, healthChanges); if (pmcProfileToUpdate.Health is null) { const string message = "pmcProfileToUpdate.Health is null when trying to apply health changes"; logger.Error(message); throw new HealthHelperException(message); } // Update last edited timestamp pmcProfileToUpdate.Health.UpdateTime = timeUtil.GetTimeStamp(); } /// /// Apply Health values to profile /// /// Player profile on server /// Changes to apply /// protected void ModifyProfileHealthProperties( PmcData profileToAdjust, Dictionary bodyPartChanges, HashSet? effectsToSkip = null ) { foreach (var (partName, partProperties) in bodyPartChanges) { // Pattern matching null and false because otherwise the compiler throws a fit because `matchingProfilePart` // might not be initialized, very cool if (profileToAdjust.Health?.BodyParts?.TryGetValue(partName, out var matchingProfilePart) is null or false) { continue; } if (partProperties.Health is null || matchingProfilePart.Health is null) { const string message = "partProperties.Health or matchingBodyPart.Health is null when trying to modify profile health properties"; logger.Error(message); throw new HealthHelperException(message); } if (HealthConfig.Save.Health) { // Apply hp changes to profile matchingProfilePart.Health.Current = partProperties.Health.Current == 0 ? partProperties.Health.Maximum * HealthConfig.HealthMultipliers.Blacked : partProperties.Health.Current; matchingProfilePart.Health.Maximum = partProperties.Health.Maximum; } // Process each effect for each part foreach (var (key, effectDetails) in partProperties.Effects ?? []) { // Null guard matchingProfilePart.Effects ??= new Dictionary(); // Effect already exists on limb in server profile, skip if (matchingProfilePart.Effects.ContainsKey(key)) { // Edge case - effect already exists at destination, but we don't want to overwrite details if (effectsToSkip is not null && effectsToSkip.Contains(key)) { matchingProfilePart.Effects[key] = null; } continue; } 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 (matchingProfilePart.Effects.TryAdd(key, effectToAdd)) { matchingProfilePart.Effects[key] = effectToAdd; } } } } /// /// Adjust hydration/energy/temperate /// /// Profile to update /// protected void AdjustProfileHydrationEnergyTemperature(PmcData profileToUpdate, BotBaseHealth healthChanges) { // Ensure current hydration/energy/temp are copied over and don't exceed maximum var profileHealth = profileToUpdate.Health; profileHealth.Hydration.Current = profileHealth.Hydration.Current > healthChanges.Hydration.Maximum ? healthChanges.Hydration.Maximum : Math.Round(healthChanges.Hydration.Current ?? 0); profileHealth.Energy.Current = profileHealth.Energy.Current > healthChanges.Energy.Maximum ? healthChanges.Energy.Maximum : Math.Round(healthChanges.Energy.Current ?? 0); profileHealth.Temperature.Current = profileHealth.Temperature.Current > healthChanges.Temperature.Maximum ? healthChanges.Temperature.Maximum : Math.Round(healthChanges.Temperature.Current ?? 0); } }