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.