using Core.Context; using Core.Helpers; using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Game; using Core.Models.Spt.Config; using Core.Models.Spt.Location; using Core.Models.Utils; using Core.Servers; using Core.Utils; using Microsoft.AspNetCore.DataProtection.KeyManagement; namespace Core.Services; [Injectable(InjectionType.Singleton)] public class RaidTimeAdjustmentService( ISptLogger _logger, DatabaseService _databaseService, RandomUtil _randomUtil, WeightedRandomHelper _weightedRandomHelper, ApplicationContext _applicationContext, ConfigServer _configServer ) { protected LocationConfig _locationConfig = _configServer.GetConfig(); /// /// Make alterations to the base map data passed in /// Loot multipliers/waves/wave start times /// /// Changes to process on map /// Map to adjust public void MakeAdjustmentsToMap(RaidChanges raidAdjustments, LocationBase mapBase) { _logger.Debug( $"Adjusting dynamic loot multipliers to {raidAdjustments.DynamicLootPercent}% and static loot multipliers to {raidAdjustments.StaticLootPercent}% of original" ); // Change loot multiplier values before they're used below AdjustLootMultipliers(_locationConfig.LooseLootMultiplier, raidAdjustments.DynamicLootPercent); AdjustLootMultipliers(_locationConfig.StaticLootMultiplier, raidAdjustments.StaticLootPercent); var mapSettings = GetMapSettings(mapBase.Id); if (mapSettings.AdjustWaves) { // Make alterations to bot spawn waves now player is simulated spawning later AdjustWaves(mapBase, raidAdjustments); } } /// /// Adjust the loot multiplier values passed in to be a % of their original value /// /// Multipliers to adjust /// Percent to change values to protected void AdjustLootMultipliers(Dictionary mapLootMultiplers, double? loosePercent) { foreach (var location in mapLootMultiplers) { mapLootMultiplers[location.Key] = _randomUtil.GetPercentOfValue(mapLootMultiplers[location.Key], loosePercent ?? 1); } } /// /// Adjust bot waves to act as if player spawned later /// /// Map to adjust /// Map adjustments protected void AdjustWaves(LocationBase mapBase, RaidChanges raidAdjustments) { // Remove waves that spawned before the player joined var originalWaveCount = mapBase.Waves.Count; mapBase.Waves = mapBase.Waves.Where(x => x.TimeMax > raidAdjustments.SimulatedRaidStartSeconds).ToList(); // Adjust wave min/max times to match new simulated start foreach (var wave in mapBase.Waves) { // Dont let time fall below 0 wave.TimeMax -= Math.Max(raidAdjustments.SimulatedRaidStartSeconds ?? 1, 0); wave.TimeMax -= Math.Max(raidAdjustments.SimulatedRaidStartSeconds ?? 1, 0); } _logger.Debug( $"Removed {originalWaveCount - mapBase.Waves.Count} wave from map due to simulated raid start time of {raidAdjustments.SimulatedRaidStartSeconds / 60} minutes" ); } /// /// Create a randomised adjustment to the raid based on map data in location.json /// /// Session id /// Raid adjustment request /// Response to send to client public GetRaidTimeResponse GetRaidAdjustments(string sessionId, GetRaidTimeRequest request) { var globals = _databaseService.GetGlobals(); LocationBase mapBase = _databaseService.GetLocation(request.Location.ToLower()).Base; var baseEscapeTimeMinutes = mapBase.EscapeTimeLimit; // Prep result object to return GetRaidTimeResponse result = new GetRaidTimeResponse { RaidTimeMinutes = baseEscapeTimeMinutes, ExitChanges = [], NewSurviveTimeSeconds = null, OriginalSurvivalTimeSeconds = globals.Configuration.Exp.MatchEnd.SurvivedSecondsRequirement, }; // Pmc raid, send default if (request.Side.ToLower() == "pmc") { return result; } // We're scav adjust values var mapSettings = GetMapSettings(request.Location); // Chance of reducing raid time for scav, not guaranteed if (!_randomUtil.GetChance100(mapSettings.ReducedChancePercent)) { // Send default return result; } // Get the weighted percent to reduce the raid time by var chosenRaidReductionPercent = int.Parse( _weightedRandomHelper.GetWeightedValue(mapSettings.ReductionPercentWeights) ); var raidTimeRemainingPercent = 100 - chosenRaidReductionPercent; // How many minutes raid will last var newRaidTimeMinutes = Math.Floor( _randomUtil.ReduceValueByPercent(baseEscapeTimeMinutes ?? 1, chosenRaidReductionPercent) ); // Time player spawns into the raid if it was online var simulatedRaidStartTimeMinutes = baseEscapeTimeMinutes - newRaidTimeMinutes; if (mapSettings.ReduceLootByPercent) { // Store time reduction percent in app context so loot gen can pick it up later _applicationContext.AddValue( ContextVariableType.RAID_ADJUSTMENTS, new { dynamicLootPercent = Math.Max(raidTimeRemainingPercent, mapSettings.MinDynamicLootPercent), staticLootPercent = Math.Max(raidTimeRemainingPercent, mapSettings.MinStaticLootPercent), simulatedRaidStartSeconds = simulatedRaidStartTimeMinutes * 60, } ); } // Update result object with new time result.RaidTimeMinutes = newRaidTimeMinutes; _logger.Debug($"Reduced: {request.Location} raid time by: {chosenRaidReductionPercent}% to {newRaidTimeMinutes} minutes"); // Calculate how long player needs to be in raid to get a `survived` extract status result.NewSurviveTimeSeconds = Math.Max( (result.OriginalSurvivalTimeSeconds - (baseEscapeTimeMinutes - newRaidTimeMinutes) * 60) ?? 0, 0D ); var exitAdjustments = GetExitAdjustments(mapBase, newRaidTimeMinutes); if (exitAdjustments is not null) { result.ExitChanges.AddRange(exitAdjustments); } return result; } /// /// Get raid start time settings for specific map /// /// Map Location e.g. bigmap /// ScavRaidTimeLocationSettings protected ScavRaidTimeLocationSettings GetMapSettings(string location) { var mapSettings = _locationConfig.ScavRaidTimeSettings.Maps?[location.ToLower()]; if (mapSettings is null) { _logger.Warning($"Unable to find scav raid time settings for map: {location}, using defaults"); return new ScavRaidTimeLocationSettings(); } return mapSettings; } /// /// Adjust exit times to handle scavs entering raids part-way through /// /// Map base file player is on /// How long raid is in minutes /// List of exit changes to send to client protected List GetExitAdjustments(LocationBase mapBase, double newRaidTimeMinutes) { List result = []; // Adjust train exits only foreach (var exit in mapBase.Exits) { if (exit.PassageRequirement != "Train") { continue; } // Prepare train adjustment object ExtractChange exitChange = new ExtractChange { Name = exit.Name, MinTime = null, MaxTime = null, Chance = null, }; // At what minute we simulate the player joining the raid var simulatedRaidEntryTimeMinutes = mapBase.EscapeTimeLimit - newRaidTimeMinutes; // How many seconds have elapsed in the raid when the player joins var reductionSeconds = simulatedRaidEntryTimeMinutes * 60; // Delay between the train extract activating and it becoming available to board // // Test method for determining this value: // 1) Set MinTime, MaxTime, and Count for the train extract all to 120 // 2) Load into Reserve or Lighthouse as a PMC (both have the same result) // 3) Board the train when it arrives // 4) Check the raid time on the Raid Ended Screen (it should always be the same) // // trainArrivalDelaySeconds = [raid time on raid-ended screen] - MaxTime - Count - ExfiltrationTime // Example: Raid Time = 5:33 = 333 seconds // trainArrivalDelaySeconds = 333 - 120 - 120 - 5 = 88 // // I added 2 seconds just to be safe... // var trainArrivalDelaySeconds = _locationConfig.ScavRaidTimeSettings.Settings.TrainArrivalDelayObservedSeconds; // Determine the earliest possible time in the raid when the train would leave var earliestPossibleDepartureMinutes = (exit.MinTime + exit.Count + exit.ExfiltrationTime + trainArrivalDelaySeconds) / 60; // If raid is after last moment train can leave, assume train has already left, disable extract var mostPossibleTimeRemainingAfterDeparture = mapBase.EscapeTimeLimit - earliestPossibleDepartureMinutes; if (newRaidTimeMinutes < mostPossibleTimeRemainingAfterDeparture) { exitChange.Chance = 0; _logger.Debug($"Train Exit: {exit.Name} disabled as new raid time {newRaidTimeMinutes} minutes is below {mostPossibleTimeRemainingAfterDeparture} minutes"); result.Add(exitChange); continue; } // Reduce extract arrival times. Negative values seem to make extract turn red in game. exitChange.MinTime = Math.Max((exit.MinTime - reductionSeconds) ?? 0, 0); exitChange.MaxTime = Math.Max((exit.MaxTime - reductionSeconds) ?? 0, 0); _logger.Debug($"Train appears between: {exitChange.MinTime} and {exitChange.MaxTime} seconds raid time"); result.Add(exitChange); } return result.Count > 0 ? result : null; } }