From bc61f120d7a2655d0073d19897de133869c13f05 Mon Sep 17 00:00:00 2001 From: hulkhan22 Date: Sat, 7 Jun 2025 17:53:12 +0200 Subject: [PATCH 1/3] Configurable BTR delivery time --- .../Assets/configs/btrdelivery.json | 4 + .../Callbacks/BtrDeliveryCallbacks.cs | 121 +++++++++++++ .../SPTarkov.Server.Core/DI/OnUpdateOrder.cs | 1 + .../Models/Eft/Profile/SptProfile.cs | 34 ++++ .../Models/Enums/ConfigTypes.cs | 3 + .../Models/Spt/Config/BtrDeliveryConfig.cs | 33 ++++ .../Services/BtrDeliveryService.cs | 159 ++++++++++++++++++ .../Services/CreateProfileService.cs | 1 + .../Services/LocationLifecycleService.cs | 86 +--------- 9 files changed, 362 insertions(+), 80 deletions(-) create mode 100644 Libraries/SPTarkov.Server.Assets/Assets/configs/btrdelivery.json create mode 100644 Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs create mode 100644 Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs create mode 100644 Libraries/SPTarkov.Server.Core/Services/BtrDeliveryService.cs diff --git a/Libraries/SPTarkov.Server.Assets/Assets/configs/btrdelivery.json b/Libraries/SPTarkov.Server.Assets/Assets/configs/btrdelivery.json new file mode 100644 index 00000000..7687b5cc --- /dev/null +++ b/Libraries/SPTarkov.Server.Assets/Assets/configs/btrdelivery.json @@ -0,0 +1,4 @@ +{ + "returnTimeOverrideSeconds": 0, + "runIntervalSeconds": 30 +} diff --git a/Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs b/Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs new file mode 100644 index 00000000..88b6870f --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs @@ -0,0 +1,121 @@ +using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.DI; +using SPTarkov.Server.Core.Helpers; +using SPTarkov.Server.Core.Models.Eft.Profile; +using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Utils; +using SPTarkov.Server.Core.Servers; +using SPTarkov.Server.Core.Services; +using SPTarkov.Server.Core.Utils; +using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; + +namespace SPTarkov.Server.Core.Callbacks; + +[Injectable(TypePriority = OnUpdateOrder.BtrDeliveryCallbacks)] +public class BtrDeliveryCallbacks( + ISptLogger _logger, + BtrDeliveryService _btrDeliveryService, + TimeUtil _timeUtil, + ConfigServer _configServer, + SaveServer _saveServer, + HashUtil _hashUtil, + ItemHelper _itemHelper +) + : IOnUpdate +{ + private readonly BtrDeliveryConfig _btrDeliveryConfig = _configServer.GetConfig(); + + public Task OnUpdate(long secondsSinceLastRun) + { + if (secondsSinceLastRun < _btrDeliveryConfig.RunIntervalSeconds) + { + return Task.FromResult(false); + } + + ProcessDeliveries(); + + return Task.FromResult(true); + } + + /// + /// Process BTR delivery items of all profiles prior to being given back to the player through the mail service + /// + private void ProcessDeliveries() + { + // Process each installed profile. + foreach (var sessionId in _saveServer.GetProfiles()) + { + ProcessDeliveryByProfile(sessionId.Key); + } + } + + /// + /// Process delivery items of a single profile prior to being given back to the player through the mail service + /// + /// Player id + public void ProcessDeliveryByProfile(string sessionId) + { + // Filter out items that don't need to be processed yet. + var toBeProcessed = FilterDeliveryItems(sessionId); + + // Do nothing if no items to process + if (toBeProcessed.Count == 0) + { + return; + } + + ProcessDeliveryItems(toBeProcessed, sessionId); + } + + /// + /// Get all delivery items that are ready to be processed in a specific profile + /// + /// Session/Player id + /// All delivery items that are ready to be processed + protected List FilterDeliveryItems(string sessionId) + { + var currentTime = _timeUtil.GetTimeStamp(); + + var deliveryList = _saveServer.GetProfile(sessionId).BtrDeliveryList; + if (deliveryList != null && deliveryList!.Count > 0) + { + if (_logger.IsLogEnabled(LogLevel.Debug)) + { + _logger.Debug($"Found {deliveryList.Count} BTR delivery package(s) in profile {sessionId}"); + } + return deliveryList.Where(toBeDelivered => currentTime >= toBeDelivered.ScheduledTime).ToList(); + } + + return []; + } + + /// + /// This method orchestrates the processing of delivery items in a profile + /// + /// The delivery items to process + /// session ID that should receive the processed items + protected void ProcessDeliveryItems(List packagesToBeDelivered, string sessionId) + { + if (_logger.IsLogEnabled(LogLevel.Debug)) + { + _logger.Debug( + $"Processing {packagesToBeDelivered.Count} BTR delivery package(s), which include a total of: {packagesToBeDelivered.Select(items => items.Items).Count()} items, in profile: {sessionId}" + ); + } + + // Iterate over each of the insurance packages. + foreach (var package in packagesToBeDelivered) + { + // Create a new root parent ID for the message we'll be sending the player + var rootItemParentId = _hashUtil.Generate(); + + // Update the delivery items to have the new root parent ID for root/orphaned items + package.Items = _itemHelper.AdoptOrphanedItems(rootItemParentId, package.Items); + + _btrDeliveryService.SendBTRDelivery(sessionId, package.Items); + + // Remove the fully processed BTR delivery package from the profile. + _btrDeliveryService.RemoveBTRDeliveryPackageFromProfile(sessionId, package); + } + } +} diff --git a/Libraries/SPTarkov.Server.Core/DI/OnUpdateOrder.cs b/Libraries/SPTarkov.Server.Core/DI/OnUpdateOrder.cs index 4efbf394..80d20384 100644 --- a/Libraries/SPTarkov.Server.Core/DI/OnUpdateOrder.cs +++ b/Libraries/SPTarkov.Server.Core/DI/OnUpdateOrder.cs @@ -5,4 +5,5 @@ public static class OnUpdateOrder public const int DialogueCallbacks = 1000; public const int HideoutCallbacks = 2000; public const int InsuranceCallbacks = 3000; + public const int BtrDeliveryCallbacks = 4000; } diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Profile/SptProfile.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Profile/SptProfile.cs index eee14c77..0c2a0334 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Profile/SptProfile.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Profile/SptProfile.cs @@ -79,6 +79,13 @@ public record SptProfile set; } + [JsonPropertyName("btrDelivery")] + public List? BtrDeliveryList + { + get; + set; + } + /// /// Assort purchases made by player since last trader refresh /// @@ -1056,3 +1063,30 @@ public record Insurance set; } } + +public record BtrDelivery +{ + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } + + [JsonPropertyName("_id")] + public string? Id + { + get; + set; + } + + [JsonPropertyName("scheduledTime")] + public int? ScheduledTime + { + get; + set; + } + + [JsonPropertyName("items")] + public List? Items + { + get; + set; + } +} diff --git a/Libraries/SPTarkov.Server.Core/Models/Enums/ConfigTypes.cs b/Libraries/SPTarkov.Server.Core/Models/Enums/ConfigTypes.cs index 0bfbcaff..6cc5f2d3 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Enums/ConfigTypes.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Enums/ConfigTypes.cs @@ -11,6 +11,7 @@ public static class ConfigTypesExtension ConfigTypes.AIRDROP => "spt-airdrop", ConfigTypes.BACKUP => "spt-backup", ConfigTypes.BOT => "spt-bot", + ConfigTypes.BTR_DELIVERY => "spt-btrdelivery", ConfigTypes.PMC => "spt-pmc", ConfigTypes.CORE => "spt-core", ConfigTypes.HEALTH => "spt-health", @@ -46,6 +47,7 @@ public static class ConfigTypesExtension ConfigTypes.AIRDROP => typeof(AirdropConfig), ConfigTypes.BACKUP => typeof(BackupConfig), ConfigTypes.BOT => typeof(BotConfig), + ConfigTypes.BTR_DELIVERY => typeof(BtrDeliveryConfig), ConfigTypes.PMC => typeof(PmcConfig), ConfigTypes.CORE => typeof(CoreConfig), ConfigTypes.HEALTH => typeof(HealthConfig), @@ -80,6 +82,7 @@ public enum ConfigTypes AIRDROP, BACKUP, BOT, + BTR_DELIVERY, PMC, CORE, HEALTH, diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs new file mode 100644 index 00000000..6ed5bf46 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace SPTarkov.Server.Core.Models.Spt.Config; + +public record BtrDeliveryConfig : BaseConfig +{ + [JsonPropertyName("kind")] + public override string Kind + { + get; + set; + } = "spt-btrdelivery"; + + /// + /// Override to control how quickly delivery is processed/returned in seconds + /// + [JsonPropertyName("returnTimeOverrideSeconds")] + public double ReturnTimeOverrideSeconds + { + get; + set; + } + + /// + /// How often server should process insurance in seconds + /// + [JsonPropertyName("runIntervalSeconds")] + public double RunIntervalSeconds + { + get; + set; + } +} diff --git a/Libraries/SPTarkov.Server.Core/Services/BtrDeliveryService.cs b/Libraries/SPTarkov.Server.Core/Services/BtrDeliveryService.cs new file mode 100644 index 00000000..6fb6b3de --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Services/BtrDeliveryService.cs @@ -0,0 +1,159 @@ +using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Models.Eft.Common.Tables; +using SPTarkov.Server.Core.Models.Eft.Match; +using SPTarkov.Server.Core.Models.Eft.Profile; +using SPTarkov.Server.Core.Models.Enums; +using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Utils; +using SPTarkov.Server.Core.Servers; +using SPTarkov.Server.Core.Utils; +using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; + +namespace SPTarkov.Server.Core.Services; + +[Injectable(InjectionType.Singleton)] +public class BtrDeliveryService( + ISptLogger _logger, + DatabaseService _databaseService, + RandomUtil _randomUtil, + HashUtil _hashUtil, + TimeUtil _timeUtil, + SaveServer _saveServer, + MailSendService _mailSendService, + ConfigServer _configServer, + LocalisationService _localisationService +) +{ + protected BtrDeliveryConfig _btrDeliveryConfig = _configServer.GetConfig(); + protected TraderConfig _traderConfig = _configServer.GetConfig(); + + protected static List _transferTypes = new() + { + "btr", + "transit" + }; + + /// + /// Check if player used BTR or transit item sending service and send items to player via mail if found + /// + /// Session ID + /// End raid request from client + public void HandleItemTransferEvent(string sessionId, EndLocalRaidRequestData request) + { + foreach (var transferType in _transferTypes) + { + var rootId = $"{Traders.BTR}_{transferType}"; + List? itemsToSend = null; + + // if rootId doesnt exist in TransferItems, skip + if (!request?.TransferItems?.TryGetValue(rootId, out itemsToSend) ?? false) + { + continue; + } + + // Filter out the btr container item from transferred items before delivering + itemsToSend = itemsToSend?.Where(item => item.Id != Traders.BTR).ToList(); + if (itemsToSend?.Count == 0) + { + continue; + } + + HandleTransferItemDelivery(sessionId, itemsToSend); + } + } + + protected void HandleTransferItemDelivery(string sessionId, List items) + { + var serverProfile = _saveServer.GetProfile(sessionId); + var pmcData = serverProfile.CharacterData.PmcData; + + // Remove any items that were returned by the item delivery, but also insured, from the player's insurance list + // This is to stop items being duplicated by being returned from both item delivery and insurance + var deliveredItemIds = items.Select(item => item.Id); + pmcData.InsuredItems = pmcData.InsuredItems + .Where(insuredItem => !deliveredItemIds.Contains(insuredItem.ItemId)) + .ToList(); + + if (_saveServer.GetProfile(sessionId).BtrDeliveryList == null) + { + _saveServer.GetProfile(sessionId).BtrDeliveryList = new List(); + } + + // Store delivery to send to player later in profile + _saveServer.GetProfile(sessionId).BtrDeliveryList.Add( + new BtrDelivery + { + Id = _hashUtil.Generate(), + ScheduledTime = (int) GetBTRDeliveryReturnTimestamp(), + Items = items + } + ); + } + + public void SendBTRDelivery(string sessionId, List items) + { + var dialogueTemplates = _databaseService.GetTrader(Traders.BTR).Dialogue; + if (dialogueTemplates is null) + { + _logger.Error(_localisationService.GetText("inraid-unable_to_deliver_item_no_trader_found", Traders.BTR)); + return; + } + + if (!dialogueTemplates.TryGetValue("itemsDelivered", out var itemsDelivered)) + { + _logger.Error("dialogueTemplates doesn't contain itemsDelivered"); + return; + } + + var messageId = _randomUtil.GetArrayValue(itemsDelivered); + var messageStoreTime = _timeUtil.GetHoursAsSeconds(_traderConfig.Fence.BtrDeliveryExpireHours); + + // Send the items to the player + _mailSendService.SendLocalisedNpcMessageToPlayer( + sessionId, + Traders.BTR, + MessageType.BtrItemsDelivery, + messageId, + items, + messageStoreTime + ); + } + + /// + /// Remove a BTR delivery package from a profile using the package's ID. + /// + /// The session ID of the profile to remove the package from. + /// The BTR delivery package to remove. + public void RemoveBTRDeliveryPackageFromProfile(string sessionId, BtrDelivery delivery) + { + var profile = _saveServer.GetProfile(sessionId); + profile.BtrDeliveryList = profile.BtrDeliveryList + .Where(package => package.Id != delivery.Id) + .ToList(); + + if (_logger.IsLogEnabled(LogLevel.Debug)) + { + _logger.Debug($"Removed processed BTR delivery package. Remaining packages: {profile.BtrDeliveryList.Count}"); + } + } + + /// + /// Get a timestamp of when items given to the BTR driver should be sent to player. + /// + /// Timestamp to return items to player in seconds + protected double GetBTRDeliveryReturnTimestamp() + { + // If override in config is non-zero, use that + if (_btrDeliveryConfig.ReturnTimeOverrideSeconds > 0) + { + if (_logger.IsLogEnabled(LogLevel.Debug)) + { + _logger.Debug($"BTR delivery override used: returning in {_btrDeliveryConfig.ReturnTimeOverrideSeconds} seconds"); + } + + return _timeUtil.GetTimeStamp() + _btrDeliveryConfig.ReturnTimeOverrideSeconds; + } + + return _timeUtil.GetTimeStamp(); + } +} diff --git a/Libraries/SPTarkov.Server.Core/Services/CreateProfileService.cs b/Libraries/SPTarkov.Server.Core/Services/CreateProfileService.cs index 941fbc23..e4fc48ab 100644 --- a/Libraries/SPTarkov.Server.Core/Services/CreateProfileService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/CreateProfileService.cs @@ -119,6 +119,7 @@ public class CreateProfileService( VitalityData = new Vitality(), InraidData = new Inraid(), InsuranceList = [], + BtrDeliveryList = [], TraderPurchases = new Dictionary?>(), FriendProfileIds = [], CustomisationUnlocks = [] diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index bbf207f2..e9fc034d 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -8,7 +8,6 @@ using SPTarkov.Server.Core.Models.Eft.Profile; using SPTarkov.Server.Core.Models.Eft.Quests; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; -using SPTarkov.Server.Core.Models.Spt.Location; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; @@ -54,6 +53,7 @@ public class LocationLifecycleService protected TimeUtil _timeUtil; protected TraderConfig _traderConfig; protected TraderHelper _traderHelper; + protected BtrDeliveryService _btrDeliveryService; public LocationLifecycleService( ISptLogger logger, @@ -83,7 +83,8 @@ public class LocationLifecycleService PmcWaveGenerator pmcWaveGenerator, QuestHelper questHelper, InsuranceService insuranceService, - MatchBotDetailsCacheService matchBotDetailsCacheService + MatchBotDetailsCacheService matchBotDetailsCacheService, + BtrDeliveryService btrDeliveryService ) { _logger = logger; @@ -114,6 +115,7 @@ public class LocationLifecycleService _questHelper = questHelper; _insuranceService = insuranceService; _matchBotDetailsCacheService = matchBotDetailsCacheService; + _btrDeliveryService = btrDeliveryService; _locationConfig = _configServer.GetConfig(); _inRaidConfig = _configServer.GetConfig(); @@ -442,7 +444,7 @@ public class LocationLifecycleService var isSurvived = IsPlayerSurvived(request.Results); // Handle items transferred via BTR or transit to player mailbox - HandleItemTransferEvent(sessionId, request); + _btrDeliveryService.HandleItemTransferEvent(sessionId, request); // Player is moving between maps if (isTransfer && request.LocationTransit is not null) @@ -458,7 +460,6 @@ public class LocationLifecycleService if (!isPmc) { HandlePostRaidPlayerScav(sessionId, pmcProfile, scavProfile, isDead, isTransfer, isSurvived, request); - return; } @@ -695,7 +696,7 @@ public class LocationLifecycleService { _logger.Error($"post raid fence data not found for: {sessionId}"); } - + scavProfile.TradersInfo[Traders.FENCE].Standing = Math.Min(Math.Max(postRaidFenceData.Standing.Value, fenceMin), fenceMax); // Successful extract as scav, give some rep @@ -1076,81 +1077,6 @@ public class LocationLifecycleService } } - /// - /// Check if player used BTR or transit item sending service and send items to player via mail if found - /// - /// Session ID - /// End raid request from client - protected void HandleItemTransferEvent(string sessionId, EndLocalRaidRequestData request) - { - var transferTypes = new List - { - "btr", - "transit" - }; - - foreach (var trasferType in transferTypes) - { - var rootId = $"{Traders.BTR}_{trasferType}"; - List? itemsToSend = null; - - // if rootId doesnt exist in TransferItems, skip - if (!request?.TransferItems?.TryGetValue(rootId, out itemsToSend) ?? false) - { - continue; - } - - // Filter out the btr container item from transferred items before delivering - itemsToSend = itemsToSend?.Where(item => item.Id != Traders.BTR).ToList(); - if (itemsToSend?.Count == 0) - { - continue; - } - - TransferItemDelivery(sessionId, Traders.BTR, itemsToSend); - } - } - - protected void TransferItemDelivery(string sessionId, string traderId, List items) - { - var serverProfile = _saveServer.GetProfile(sessionId); - var pmcData = serverProfile.CharacterData.PmcData; - - var dialogueTemplates = _databaseService.GetTrader(traderId).Dialogue; - if (dialogueTemplates is null) - { - _logger.Error(_localisationService.GetText("inraid-unable_to_deliver_item_no_trader_found", traderId)); - - return; - } - - if (!dialogueTemplates.TryGetValue("itemsDelivered", out var itemsDelivered)) - { - _logger.Error("dialogueTemplates doesn't contain itemsDelivered"); - return; - } - - var messageId = _randomUtil.GetArrayValue(itemsDelivered); - var messageStoreTime = _timeUtil.GetHoursAsSeconds(_traderConfig.Fence.BtrDeliveryExpireHours); - - // Remove any items that were returned by the item delivery, but also insured, from the player's insurance list - // This is to stop items being duplicated by being returned from both item delivery and insurance - var deliveredItemIds = items.Select(item => item.Id); - pmcData.InsuredItems = pmcData.InsuredItems.Where(insuredItem => !deliveredItemIds.Contains(insuredItem.ItemId) - ) - .ToList(); - - // Send the items to the player - _mailSendService.SendLocalisedNpcMessageToPlayer( - sessionId, - traderId, - MessageType.BtrItemsDelivery, - messageId, - items, - messageStoreTime - ); - } - protected void HandleInsuredItemLostEvent( string sessionId, PmcData preRaidPmcProfile, From 2fbac07b0952c792de0b4e761a106b82fe617099 Mon Sep 17 00:00:00 2001 From: hulkhan22 Date: Sat, 7 Jun 2025 18:47:26 +0200 Subject: [PATCH 2/3] Update comment --- .../SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs index 6ed5bf46..3d5c25c0 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BtrDeliveryConfig.cs @@ -22,7 +22,7 @@ public record BtrDeliveryConfig : BaseConfig } /// - /// How often server should process insurance in seconds + /// How often server should process BTR delivery in seconds /// [JsonPropertyName("runIntervalSeconds")] public double RunIntervalSeconds From 332497a12ca4903a86cf98711371d88f03200f6a Mon Sep 17 00:00:00 2001 From: hulkhan22 Date: Sat, 7 Jun 2025 18:49:02 +0200 Subject: [PATCH 3/3] Make method protected to ease modding --- .../SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs b/Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs index 88b6870f..7431d2c6 100644 --- a/Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs +++ b/Libraries/SPTarkov.Server.Core/Callbacks/BtrDeliveryCallbacks.cs @@ -40,7 +40,7 @@ public class BtrDeliveryCallbacks( /// /// Process BTR delivery items of all profiles prior to being given back to the player through the mail service /// - private void ProcessDeliveries() + protected void ProcessDeliveries() { // Process each installed profile. foreach (var sessionId in _saveServer.GetProfiles())