From b1d90358015d8e44c9672628b2fa748e79c967e8 Mon Sep 17 00:00:00 2001 From: CWX Date: Mon, 20 Jan 2025 15:31:53 +0000 Subject: [PATCH] insurance service done with raidtimeadjustmentservice --- Libraries/Core/Helpers/TraderHelper.cs | 4 +- .../Core/Models/Eft/Common/LocationBase.cs | 2 +- .../Core/Models/Eft/Common/Tables/Trader.cs | 2 +- .../Models/Eft/Game/GetRaidTimeResponse.cs | 12 +- .../Core/Models/Spt/Config/LocationConfig.cs | 2 +- .../Core/Models/Spt/Location/RaidChanges.cs | 4 +- Libraries/Core/Services/InsuranceService.cs | 245 ++++++++++++++++-- .../Services/RaidTimeAdjustmentService.cs | 210 ++++++++++++++- Libraries/Core/Utils/RandomUtil.cs | 2 +- Server/Program.cs | 4 +- 10 files changed, 441 insertions(+), 46 deletions(-) diff --git a/Libraries/Core/Helpers/TraderHelper.cs b/Libraries/Core/Helpers/TraderHelper.cs index ff885c1e..340614df 100644 --- a/Libraries/Core/Helpers/TraderHelper.cs +++ b/Libraries/Core/Helpers/TraderHelper.cs @@ -34,7 +34,7 @@ public class TraderHelper( /// Traders Id to get /// Players id /// Trader base - public TraderBase GetTrader(string traderID, string sessionID) + public TraderBase? GetTrader(string traderID, string sessionID) { if (traderID == "ragfair") { @@ -274,7 +274,7 @@ public class TraderHelper( /// /// Traders id /// Traders key - public Trader GetTraderById(string traderId) + public Trader? GetTraderById(string traderId) { throw new NotImplementedException(); } diff --git a/Libraries/Core/Models/Eft/Common/LocationBase.cs b/Libraries/Core/Models/Eft/Common/LocationBase.cs index b5564433..8452ae6a 100644 --- a/Libraries/Core/Models/Eft/Common/LocationBase.cs +++ b/Libraries/Core/Models/Eft/Common/LocationBase.cs @@ -253,7 +253,7 @@ public record LocationBase public List? Doors { get; set; } [JsonPropertyName("EscapeTimeLimit")] - public int? EscapeTimeLimit { get; set; } + public double? EscapeTimeLimit { get; set; } // BSG fucked up another property name [JsonPropertyName("escape_time_limit")] diff --git a/Libraries/Core/Models/Eft/Common/Tables/Trader.cs b/Libraries/Core/Models/Eft/Common/Tables/Trader.cs index 4d64e881..6548199f 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/Trader.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/Trader.cs @@ -14,7 +14,7 @@ public record Trader public TraderBase? Base { get; set; } [JsonPropertyName("dialogue")] - public Dictionary>? Dialogue { get; set; } + public Dictionary?>? Dialogue { get; set; } [JsonPropertyName("questassort")] public Dictionary>? QuestAssort { get; set; } diff --git a/Libraries/Core/Models/Eft/Game/GetRaidTimeResponse.cs b/Libraries/Core/Models/Eft/Game/GetRaidTimeResponse.cs index 9d18ebde..94cde485 100644 --- a/Libraries/Core/Models/Eft/Game/GetRaidTimeResponse.cs +++ b/Libraries/Core/Models/Eft/Game/GetRaidTimeResponse.cs @@ -5,13 +5,13 @@ namespace Core.Models.Eft.Game; public record GetRaidTimeResponse { [JsonPropertyName("RaidTimeMinutes")] - public int? RaidTimeMinutes { get; set; } + public double? RaidTimeMinutes { get; set; } [JsonPropertyName("NewSurviveTimeSeconds")] - public int? NewSurviveTimeSeconds { get; set; } + public double? NewSurviveTimeSeconds { get; set; } [JsonPropertyName("OriginalSurvivalTimeSeconds")] - public int? OriginalSurvivalTimeSeconds { get; set; } + public double? OriginalSurvivalTimeSeconds { get; set; } [JsonPropertyName("ExitChanges")] public List? ExitChanges { get; set; } @@ -23,11 +23,11 @@ public record ExtractChange public string? Name { get; set; } [JsonPropertyName("MinTime")] - public int? MinTime { get; set; } + public double? MinTime { get; set; } [JsonPropertyName("MaxTime")] - public int? MaxTime { get; set; } + public double? MaxTime { get; set; } [JsonPropertyName("Chance")] - public int? Chance { get; set; } + public double? Chance { get; set; } } diff --git a/Libraries/Core/Models/Spt/Config/LocationConfig.cs b/Libraries/Core/Models/Spt/Config/LocationConfig.cs index decd3b8d..8344748f 100644 --- a/Libraries/Core/Models/Spt/Config/LocationConfig.cs +++ b/Libraries/Core/Models/Spt/Config/LocationConfig.cs @@ -291,7 +291,7 @@ public record ScavRaidTimeSettings public ScavRaidTimeConfigSettings Settings { get; set; } [JsonPropertyName("maps")] - public Dictionary Maps { get; set; } + public Dictionary? Maps { get; set; } } public record ScavRaidTimeConfigSettings diff --git a/Libraries/Core/Models/Spt/Location/RaidChanges.cs b/Libraries/Core/Models/Spt/Location/RaidChanges.cs index 5aa53530..a62a3bab 100644 --- a/Libraries/Core/Models/Spt/Location/RaidChanges.cs +++ b/Libraries/Core/Models/Spt/Location/RaidChanges.cs @@ -5,10 +5,10 @@ namespace Core.Models.Spt.Location; public record RaidChanges { [JsonPropertyName("dynamicLootPercent")] - public float? DynamicLootPercent { get; set; } + public double? DynamicLootPercent { get; set; } [JsonPropertyName("staticLootPercent")] - public float? StaticLootPercent { get; set; } + public double? StaticLootPercent { get; set; } [JsonPropertyName("simulatedRaidStartSeconds")] public int? SimulatedRaidStartSeconds { get; set; } diff --git a/Libraries/Core/Services/InsuranceService.cs b/Libraries/Core/Services/InsuranceService.cs index 57c759c3..483f4971 100644 --- a/Libraries/Core/Services/InsuranceService.cs +++ b/Libraries/Core/Services/InsuranceService.cs @@ -1,13 +1,39 @@ -using SptCommon.Annotations; +using Core.Helpers; +using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; +using Core.Models.Eft.Profile; +using Core.Models.Enums; +using Core.Models.Spt.Config; using Core.Models.Spt.Services; +using Core.Models.Utils; +using Core.Servers; +using Core.Utils; +using Core.Utils.Cloners; +using Insurance = Core.Models.Eft.Profile.Insurance; namespace Core.Services; [Injectable(InjectionType.Singleton)] -public class InsuranceService +public class InsuranceService( + ISptLogger _logger, + DatabaseService _databaseService, + RandomUtil _randomUtil, + ItemHelper _itemHelper, + HashUtil _hashUtil, + TimeUtil _timeUtil, + SaveServer _saveServer, + TraderHelper _traderHelper, + ProfileHelper _profileHelper, + LocalisationService _localisationService, + MailSendService _mailSendService, + ConfigServer _configServer, + ICloner _cloner +) { + protected InsuranceConfig _insuranceConfig = _configServer.GetConfig(); + protected Dictionary>?> _insured = new Dictionary>?>(); + /// /// Does player have insurance dictionary exists /// @@ -15,7 +41,7 @@ public class InsuranceService /// True if exists public bool InsuranceDictionaryExists(string sessionId) { - throw new NotImplementedException(); + return _insured.TryGetValue(sessionId, out var _); } /// @@ -23,14 +49,17 @@ public class InsuranceService /// /// Profile id (session id) /// Item list - public Dictionary> GetInsurance(string sessionId) + public Dictionary>? GetInsurance(string sessionId) { - throw new NotImplementedException(); + return _insured[sessionId]; } public void ResetInsurance(string sessionId) { - throw new NotImplementedException(); + if (!_insured.TryAdd(sessionId, new Dictionary>())) + { + _insured[sessionId] = new Dictionary>(); + } } /// @@ -42,7 +71,70 @@ public class InsuranceService /// Id of the location player died/exited that caused the insurance to be issued on public void StartPostRaidInsuranceLostProcess(PmcData pmcData, string sessionID, string mapId) { - throw new NotImplementedException(); + // Get insurance items for each trader + var globals = _databaseService.GetGlobals(); + foreach (var trader in GetInsurance(sessionID)) + { + var traderEnum = _traderHelper.GetTraderById(trader.Key); + if (traderEnum is null) + { + _logger.Error(_localisationService.GetText("insurance-trader_missing_from_enum", trader.Key)); + + continue; + } + + var traderBase = _traderHelper.GetTrader(trader.Key, sessionID); + if (traderBase is null) + { + _logger.Error(_localisationService.GetText("insurance-unable_to_find_trader_by_id", trader.Key)); + + continue; + } + + var dialogueTemplates = _databaseService.GetTrader(trader.Key).Dialogue; + if (dialogueTemplates is null) + { + _logger.Error(_localisationService.GetText("insurance-trader_lacks_dialogue_property", trader.Key)); + + continue; + } + + SystemData systemData = new SystemData + { + Date = _timeUtil.GetDateMailFormat(), + Time = _timeUtil.GetTimeMailFormat(), + Location = mapId, + }; + + // Send "i will go look for your stuff" message from trader to player + _mailSendService.SendLocalisedNpcMessageToPlayer( + sessionID, + traderEnum.ToString(), + MessageType.NPC_TRADER, + _randomUtil.GetArrayValue(dialogueTemplates["insuranceStart"] ?? ["INSURANCE START MESSAGE MISSING"]), + null, + _timeUtil.GetHoursAsSeconds((int)globals.Configuration?.Insurance?.MaxStorageTimeInHour), + systemData + ); + + // Store insurance to send to player later in profile + // Store insurance return details in profile + "hey i found your stuff, here you go!" message details to send to player at a later date + _saveServer.GetProfile(sessionID) + .InsuranceList.Add( + new Insurance + { + ScheduledTime = (int)GetInsuranceReturnTimestamp(pmcData, traderBase), + TraderId = trader.ToString(), + MaxStorageTime = (int)GetMaxInsuranceStorageTime(traderBase), + SystemData = systemData, + MessageType = MessageType.INSURANCE_RETURN, + MessageTemplateId = _randomUtil.GetArrayValue(dialogueTemplates["insuranceFound"]), + Items = GetInsurance(sessionID)[trader.Key], + } + ); + } + + ResetInsurance(sessionID); } /// @@ -54,12 +146,59 @@ public class InsuranceService /// Timestamp to return items to player in seconds protected double GetInsuranceReturnTimestamp(PmcData pmcData, TraderBase trader) { - throw new NotImplementedException(); + // If override in config is non-zero, use that instead of trader values + if (_insuranceConfig.ReturnTimeOverrideSeconds > 0) + { + _logger.Debug($"Insurance override used: returning in {_insuranceConfig.ReturnTimeOverrideSeconds} seconds"); + return _timeUtil.GetTimeStamp() + _insuranceConfig.ReturnTimeOverrideSeconds; + } + + var insuranceReturnTimeBonusSum = _profileHelper.GetBonusValueFromProfile( + pmcData, + BonusType.InsuranceReturnTime + ); + + // A negative bonus implies a faster return, since we subtract later, invert the value here + var insuranceReturnTimeBonusPercent = -(insuranceReturnTimeBonusSum / 100); + + var traderMinReturnAsSeconds = trader.Insurance.MinReturnHour * TimeUtil.OneHourAsSeconds; + var traderMaxReturnAsSeconds = trader.Insurance.MaxReturnHour * TimeUtil.OneHourAsSeconds; + var randomisedReturnTimeSeconds = _randomUtil.GetInt((int)traderMinReturnAsSeconds, (int)traderMaxReturnAsSeconds); + + // Check for Mark of The Unheard in players special slots (only slot item can fit) + var globals = _databaseService.GetGlobals(); + var hasMarkOfUnheard = _itemHelper.hasItemWithTpl( + pmcData.Inventory.Items, + ItemTpl.MARKOFUNKNOWN_MARK_OF_THE_UNHEARD, + "SpecialSlot" + ); + if (hasMarkOfUnheard) + { + // Reduce return time by globals multipler value + randomisedReturnTimeSeconds *= (int)globals.Configuration.Insurance.CoefOfHavingMarkOfUnknown; + } + + // EoD has 30% faster returns + var editionModifier = globals.Configuration.Insurance.EditionSendingMessageTime[pmcData.Info.GameVersion]; + if (editionModifier is not null) + { + randomisedReturnTimeSeconds *= (int)editionModifier.Multiplier; + } + + // Calculate the final return time based on our bonus percent + var finalReturnTimeSeconds = randomisedReturnTimeSeconds * (1.0 - insuranceReturnTimeBonusPercent); + return _timeUtil.GetTimeStamp() + finalReturnTimeSeconds; } protected double GetMaxInsuranceStorageTime(TraderBase traderBase) { - throw new NotImplementedException(); + if (_insuranceConfig.StorageTimeOverrideSeconds > 0) + { + // Override exists, use instead of traders value + return _insuranceConfig.StorageTimeOverrideSeconds; + } + + return _timeUtil.GetHoursAsSeconds((int)traderBase.Insurance.MaxStorageTime); } /// @@ -68,7 +207,11 @@ public class InsuranceService /// Gear to store - generated by GetGearLostInRaid() public void StoreGearLostInRaidToSendLater(string sessionID, List equipmentPkg) { - throw new NotImplementedException(); + // Process all insured items lost in-raid + foreach (var gear in equipmentPkg) + { + AddGearToSend(gear); + } } /// @@ -78,12 +221,38 @@ public class InsuranceService /// Insured items lost in a raid /// Player profile /// InsuranceEquipmentPkg list - public List MapInsuredItemsToTrader( - string sessionId, - List lostInsuredItems, - PmcData pmcProfile) + public List MapInsuredItemsToTrader(string sessionId, List lostInsuredItems, PmcData pmcProfile) { - throw new NotImplementedException(); + List result = []; + + foreach (var lostItem in lostInsuredItems) + { + var insuranceDetails = pmcProfile.InsuredItems.FirstOrDefault(insuredItem => insuredItem.ItemId == lostItem.Id); + if (insuranceDetails is null) + { + _logger.Error($"unable to find insurance details for item id: {lostItem.Id} with tpl: {lostItem.Template}"); + + continue; + } + + if (ItemCannotBeLostOnDeath(lostItem, pmcProfile.Inventory.Items)) + { + continue; + } + + // Add insured item + details to return array + result.Add( + new InsuranceEquipmentPkg + { + SessionId = sessionId, + ItemToReturnToPlayer = lostItem, + PmcData = pmcProfile, + TraderId = insuranceDetails.TId, + } + ); + } + + return result; } /// @@ -94,7 +263,18 @@ public class InsuranceService /// True if item protected bool ItemCannotBeLostOnDeath(Item lostItem, List inventoryItems) { - throw new NotImplementedException(); + if (lostItem.SlotId?.ToLower().StartsWith("specialslot") ?? false) + { + return true; + } + + // We check secure container items even tho they are omitted from lostInsuredItems, just in case + if (_itemHelper.ItemIsInsideContainer(lostItem, "SecuredContainer", inventoryItems)) + { + return true; + } + + return false; } /// @@ -103,7 +283,27 @@ public class InsuranceService /// Gear to send protected void AddGearToSend(InsuranceEquipmentPkg gear) { - throw new NotImplementedException(); + var sessionId = gear.SessionId; + var pmcData = gear.PmcData; + var itemToReturnToPlayer = gear.ItemToReturnToPlayer; + var traderId = gear.TraderId; + + // Ensure insurance array is init + if (!InsuranceDictionaryExists(sessionId)) + { + ResetInsurance(sessionId); + } + + // init trader insurance array + if (!InsuranceTraderArrayExists(sessionId, traderId)) + { + ResetInsuranceTraderArray(sessionId, traderId); + } + + AddInsuranceItemToArray(sessionId, traderId, itemToReturnToPlayer); + + // Remove item from insured items array as its been processed + pmcData.InsuredItems = pmcData.InsuredItems.Where(item => { return item.ItemId != itemToReturnToPlayer.Id; }).ToList(); } /// @@ -114,7 +314,7 @@ public class InsuranceService /// True if exists protected bool InsuranceTraderArrayExists(string sessionId, string traderId) { - throw new NotImplementedException(); + return this._insured[sessionId][traderId] is not null; } /// @@ -124,7 +324,7 @@ public class InsuranceService /// Trader items insured with public void ResetInsuranceTraderArray(string sessionId, string traderId) { - throw new NotImplementedException(); + _insured[sessionId][traderId] = []; } /// @@ -135,7 +335,7 @@ public class InsuranceService /// Insured item (with children) public void AddInsuranceItemToArray(string sessionId, string traderId, Item itemToAdd) { - throw new NotImplementedException(); + _insured[sessionId][traderId].Add(itemToAdd); } /// @@ -147,6 +347,9 @@ public class InsuranceService /// price in roubles public double GetRoublePriceToInsureItemWithTrader(PmcData? pmcData, Item inventoryItem, string traderId) { - throw new NotImplementedException(); + var price = _itemHelper.GetStaticItemPrice(inventoryItem.Template) * + (_traderHelper.GetLoyaltyLevel(traderId, pmcData).InsurancePriceCoefficient / 100); + + return Math.Ceiling(price ?? 1); } } diff --git a/Libraries/Core/Services/RaidTimeAdjustmentService.cs b/Libraries/Core/Services/RaidTimeAdjustmentService.cs index 982d7723..2e433dc4 100644 --- a/Libraries/Core/Services/RaidTimeAdjustmentService.cs +++ b/Libraries/Core/Services/RaidTimeAdjustmentService.cs @@ -1,14 +1,28 @@ -using SptCommon.Annotations; +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; namespace Core.Services; [Injectable(InjectionType.Singleton)] -public class RaidTimeAdjustmentService +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 @@ -17,7 +31,20 @@ public class RaidTimeAdjustmentService /// Map to adjust public void MakeAdjustmentsToMap(RaidChanges raidAdjustments, LocationBase mapBase) { - throw new NotImplementedException(); + _logger.Debug( + $"Adjusting dynamic loot multipliers to {raidAdjustments.DynamicLootPercent}% and static loot multipliers to {raidAdjustments.StaticLootPercent}% of original" + ); + + // Change loot multipler 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); + } } /// @@ -25,9 +52,14 @@ public class RaidTimeAdjustmentService /// /// Multipliers to adjust /// Percent to change values to - protected void AdjustLootMultipliers(LootMultiplier mapLootMultipliers, float loosePercent) + protected void AdjustLootMultipliers(LootMultiplier mapLootMultiplers, double? loosePercent) { - throw new NotImplementedException(); + var props = mapLootMultiplers.GetType().GetProperties(); + foreach (var multiplier in props) + { + var propValue = (double)multiplier.GetValue(mapLootMultiplers); + multiplier.SetValue(mapLootMultiplers, _randomUtil.GetPercentOfValue(propValue, loosePercent ?? 1)); + } } /// @@ -37,7 +69,21 @@ public class RaidTimeAdjustmentService /// Map adjustments protected void AdjustWaves(LocationBase mapBase, RaidChanges raidAdjustments) { - throw new NotImplementedException(); + // 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" + ); } /// @@ -48,7 +94,81 @@ public class RaidTimeAdjustmentService /// Response to send to client public GetRaidTimeResponse GetRaidAdjustments(string sessionId, GetRaidTimeRequest request) { - throw new NotImplementedException(); + 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; } /// @@ -58,7 +178,14 @@ public class RaidTimeAdjustmentService /// ScavRaidTimeLocationSettings protected ScavRaidTimeLocationSettings GetMapSettings(string location) { - throw new NotImplementedException(); + 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; } /// @@ -67,8 +194,71 @@ public class RaidTimeAdjustmentService /// 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, int newRaidTimeMinutes) + protected List GetExitAdjustments(LocationBase mapBase, double newRaidTimeMinutes) { - throw new NotImplementedException(); + 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; } } diff --git a/Libraries/Core/Utils/RandomUtil.cs b/Libraries/Core/Utils/RandomUtil.cs index a5dc8fe9..747d6957 100644 --- a/Libraries/Core/Utils/RandomUtil.cs +++ b/Libraries/Core/Utils/RandomUtil.cs @@ -105,7 +105,7 @@ public class RandomUtil /// The original number to be reduced. /// The percentage by which to reduce the number. /// The reduced number after applying the percentage reduction. - public float ReduceValueByPercent(float number, float percentage) + public double ReduceValueByPercent(double number, double percentage) { var reductionAmount = number * percentage / 100; diff --git a/Server/Program.cs b/Server/Program.cs index 1d09bd34..9f478169 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,3 +1,4 @@ +using System.Runtime; using Core.Context; using Core.Models.External; using Core.Models.Spt.Config; @@ -52,7 +53,8 @@ public static class Program // Get the Built app and run it var app = serviceProvider.GetService(); app?.Run().Wait(); - + GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive); var httpConfig = serviceProvider.GetService()?.GetConfig(); // When we application gets started by the HttpServer it will add into the AppContext the WebApplication // object, which we can use here to start the webapp.