From 4dee774efc876f12fbe67b73fa0149191aac52ce Mon Sep 17 00:00:00 2001 From: DrakiaXYZ Date: Mon, 15 Jan 2024 09:09:31 +0000 Subject: [PATCH] Refactor trader services into its own manager in Aki.Singleplayer (!61) I've moved trader services handling from Aki.Debugging BTR code into Aki.SinglePlayer This simplifies some of the code, and allows a more "generic" implementation. I've also patched the GetTraderServicesDataFromServer and TryPurchaseTraderService methods to properly utilize the TraderServicesManager for storing service state For now, this makes Aki.Debugging depend on Aki.SinglePlayer, this can be reverted once the BTR stuff is moved Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Modules/pulls/61 Co-authored-by: DrakiaXYZ Co-committed-by: DrakiaXYZ --- project/Aki.Debugging/Aki.Debugging.csproj | 1 + project/Aki.Debugging/BTR/BTRManager.cs | 3 +- .../BTR/Patches/BTRDebugCommandPatch.cs | 3 +- .../BTR/Patches/BTRTransferItemsPatch.cs | 14 +- project/Aki.Debugging/BTR/Utils/BTRUtil.cs | 122 ------------- .../Aki.SinglePlayer/AkiSingleplayerPlugin.cs | 3 + .../TraderServices/GetTraderServicesPatch.cs | 25 +++ .../PurchaseTraderServicePatch.cs | 36 ++++ .../TraderServices}/TraderServiceModel.cs | 4 +- .../TraderServices/TraderServicesManager.cs | 164 ++++++++++++++++++ 10 files changed, 237 insertions(+), 138 deletions(-) create mode 100644 project/Aki.SinglePlayer/Patches/TraderServices/GetTraderServicesPatch.cs create mode 100644 project/Aki.SinglePlayer/Patches/TraderServices/PurchaseTraderServicePatch.cs rename project/{Aki.Debugging/BTR/Models => Aki.SinglePlayer/Utils/TraderServices}/TraderServiceModel.cs (89%) create mode 100644 project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs diff --git a/project/Aki.Debugging/Aki.Debugging.csproj b/project/Aki.Debugging/Aki.Debugging.csproj index 1c37456..d422fc3 100644 --- a/project/Aki.Debugging/Aki.Debugging.csproj +++ b/project/Aki.Debugging/Aki.Debugging.csproj @@ -29,6 +29,7 @@ + diff --git a/project/Aki.Debugging/BTR/BTRManager.cs b/project/Aki.Debugging/BTR/BTRManager.cs index 75c9e20..fc5031e 100644 --- a/project/Aki.Debugging/BTR/BTRManager.cs +++ b/project/Aki.Debugging/BTR/BTRManager.cs @@ -1,4 +1,5 @@ using Aki.Debugging.BTR.Utils; +using Aki.SinglePlayer.Utils.TraderServices; using Comfort.Common; using EFT; using EFT.InventoryLogic; @@ -220,7 +221,7 @@ namespace Aki.Debugging.BTR btrMachineGunWeapon = BTRUtil.CreateItem(BTRUtil.BTRMachineGunWeaponTplId); // Pull services data for the BTR from the server - BTRUtil.PopulateTraderServicesData(BTRUtil.BTRTraderId); + TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId); } /** diff --git a/project/Aki.Debugging/BTR/Patches/BTRDebugCommandPatch.cs b/project/Aki.Debugging/BTR/Patches/BTRDebugCommandPatch.cs index e8e62f6..fc98aaa 100644 --- a/project/Aki.Debugging/BTR/Patches/BTRDebugCommandPatch.cs +++ b/project/Aki.Debugging/BTR/Patches/BTRDebugCommandPatch.cs @@ -1,5 +1,6 @@ using Aki.Debugging.BTR.Utils; using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; using EFT; using EFT.Console.Core; using EFT.UI; @@ -42,7 +43,7 @@ namespace Aki.Debugging.BTR.Patches internal static void PatchPrefix(Profile.ETraderServiceSource traderServiceSourceType, ref bool useDebugData) { useDebugData = false; - BTRUtil.PopulateTraderServicesData(Profile.TraderInfo.TraderServiceToId[traderServiceSourceType]); + TraderServicesManager.Instance.GetTraderServicesDataFromServer(Profile.TraderInfo.TraderServiceToId[traderServiceSourceType]); } } } diff --git a/project/Aki.Debugging/BTR/Patches/BTRTransferItemsPatch.cs b/project/Aki.Debugging/BTR/Patches/BTRTransferItemsPatch.cs index c459d7f..9649d38 100644 --- a/project/Aki.Debugging/BTR/Patches/BTRTransferItemsPatch.cs +++ b/project/Aki.Debugging/BTR/Patches/BTRTransferItemsPatch.cs @@ -1,11 +1,11 @@ using Aki.Debugging.BTR.Utils; using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; using Comfort.Common; using EFT; using EFT.UI; using HarmonyLib; using System.Reflection; -using System.Threading.Tasks; namespace Aki.Debugging.BTR.Patches { @@ -26,18 +26,8 @@ namespace Aki.Debugging.BTR.Patches return; } - GameWorld gameWorld = Singleton.Instance; - var player = gameWorld?.MainPlayer; - var btrManager = gameWorld?.GetComponent(); - if (gameWorld == null || player == null || btrManager == null) - { - Logger.LogError("[AKI-BTR] BTRTransferItemsPatch - Error fetching game objects"); - return; - } - // Update the trader services information now that we've used this service - btrManager.SetServicePurchased(ETraderServiceType.BtrItemsDelivery, BTRUtil.BTRTraderId); - BTRUtil.UpdateTraderServices(BTRUtil.BTRTraderId); + TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId); } } } diff --git a/project/Aki.Debugging/BTR/Utils/BTRUtil.cs b/project/Aki.Debugging/BTR/Utils/BTRUtil.cs index 2bb9b46..b672a5a 100644 --- a/project/Aki.Debugging/BTR/Utils/BTRUtil.cs +++ b/project/Aki.Debugging/BTR/Utils/BTRUtil.cs @@ -1,5 +1,4 @@ using Aki.Common.Http; -using Aki.Debugging.BTR.Models; using Comfort.Common; using EFT; using EFT.InventoryLogic; @@ -29,127 +28,6 @@ namespace Aki.Debugging.BTR.Utils _ = nameof(TraderServiceClass.WasPurchasedInThisRaid); } - /** - * Populate the given trader's services with server data - */ - public static void PopulateTraderServicesData(string traderId) - { - if (!GetGameObjects(out GameWorld gameWorld, out BTRManager btrManager, out Player player)) - { - Debug.LogError("[AKI-BTR] PopulateTraderServicesData - Error fetching game objects"); - return; - } - - if (!player.Profile.TradersInfo.TryGetValue(traderId, out Profile.TraderInfo traderInfo)) - { - Debug.LogError("[AKI-BTR] PopulateTraderServicesData - Error fetching profile trader info"); - return; - } - - string json = RequestHandler.GetJson($"/singleplayer/traderServices/getTraderServices/{traderId}"); - var traderServiceModels = JsonConvert.DeserializeObject>(json); - - Dictionary servicesData = Singleton.Instance.ServicesData; - foreach (var traderServiceModel in traderServiceModels) - { - ServiceData serviceData; - - // Only populate trader services that don't exist yet - // Note: This is required because otherwise we overwrite some values the client sets itself. - // Normally this state would be handled via the server I guess - if (!servicesData.ContainsKey(traderServiceModel.ServiceType)) - { - TraderServiceClass traderService = new TraderServiceClass(); - traderService.TraderId = traderId; - traderService.ServiceType = traderServiceModel.ServiceType; - traderService.ItemsToPay = new Dictionary(); - if (traderServiceModel.ItemsToPay != null) - { - foreach (var item in traderServiceModel.ItemsToPay) - { - traderService.ItemsToPay[item.Key] = item.Value; - } - } - - // SubServices seem to be populated dynamically in the client (For BTR taxi atleast), so we can just ignore it - // NOTE: For future reference, this is a dict of `item _tpl` to `quantity`. - traderService.SubServices = new Dictionary(); - - // TODO: What is this used for? Maybe lightkeeper? - traderService.UniqueItems = new MongoID[] { }; - - // Convert our format to the backend settings format and store it - serviceData = new ServiceData(traderService); - servicesData[serviceData.ServiceType] = serviceData; - - // Set the service as available - traderInfo.SetServiceAvailability(serviceData.ServiceType, true, false); - } - } - - UpdateTraderServices(traderId); - } - - /** - * Update the trader services for the given trader with new WasPurchased data - */ - public static void UpdateTraderServices(string traderId) - { - if (!GetGameObjects(out GameWorld gameWorld, out BTRManager btrManager, out Player player)) - { - Debug.LogError("[AKI-BTR] UpdateTraderServices - Error fetching game objects"); - return; - } - - if (player.Profile.TradersInfo.TryGetValue(traderId, out Profile.TraderInfo traderInfo)) - { - var traderServices = _traderAvailableServicesField.GetValue(traderInfo) as HashSet; - foreach (var traderService in traderServices) - { - // TODO: We should probably actually calculate this? - var CanAfford = true; - - // Check whether we've purchased this service yet - var WasPurchasedInThisRaid = btrManager.IsServicePurchased(traderService, traderId); - - // Update the affordable and WasPurchased flags for the service, for this trader - traderInfo.SetServiceAvailability(traderService, CanAfford, WasPurchasedInThisRaid); - } - } - } - - /** - * Fetch common game properties via out parameters - * - * Return false if any value is null - */ - public static bool GetGameObjects(out GameWorld gameWorld, out BTRManager btrManager, out Player player) - { - gameWorld = Singleton.Instance; - btrManager = gameWorld?.GetComponent(); - player = gameWorld?.MainPlayer; - - if (gameWorld == null) - { - Debug.LogError($"[AKI-BTR]: GetGameObjects - GameWorld is null"); - return false; - } - - if (btrManager == null) - { - Debug.LogError($"[AKI-BTR]: GetGameObjects - BTRManagerr is null"); - return false; - } - - if (player == null) - { - Debug.LogError($"[AKI-BTR]: GetGameObjects - Player is null"); - return false; - } - - return true; - } - /// /// Used to create an instance of the item in-raid. /// diff --git a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs index 8a66cfa..d2fa9a3 100644 --- a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs +++ b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs @@ -6,6 +6,7 @@ using Aki.SinglePlayer.Patches.Progression; using Aki.SinglePlayer.Patches.Quests; using Aki.SinglePlayer.Patches.RaidFix; using Aki.SinglePlayer.Patches.ScavMode; +using Aki.SinglePlayer.Patches.TraderServices; using BepInEx; namespace Aki.SinglePlayer @@ -57,6 +58,8 @@ namespace Aki.SinglePlayer new LabsKeycardRemovalPatch().Enable(); new ScavLateStartPatch().Enable(); new MidRaidAchievementChangePatch().Enable(); + new GetTraderServicesPatch().Enable(); + new PurchaseTraderServicePatch().Enable(); } catch (Exception ex) { diff --git a/project/Aki.SinglePlayer/Patches/TraderServices/GetTraderServicesPatch.cs b/project/Aki.SinglePlayer/Patches/TraderServices/GetTraderServicesPatch.cs new file mode 100644 index 0000000..ed8c6db --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/TraderServices/GetTraderServicesPatch.cs @@ -0,0 +1,25 @@ +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using HarmonyLib; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.TraderServices +{ + public class GetTraderServicesPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InventoryControllerClass), nameof(InventoryControllerClass.GetTraderServicesDataFromServer)); + } + + [PatchPrefix] + public static bool PatchPrefix(string traderId) + { + Logger.LogInfo($"Loading {traderId} services from servers"); + TraderServicesManager.Instance.GetTraderServicesDataFromServer(traderId); + + // Skip original + return false; + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/TraderServices/PurchaseTraderServicePatch.cs b/project/Aki.SinglePlayer/Patches/TraderServices/PurchaseTraderServicePatch.cs new file mode 100644 index 0000000..8ad20c7 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/TraderServices/PurchaseTraderServicePatch.cs @@ -0,0 +1,36 @@ +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using EFT; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Aki.SinglePlayer.Patches.TraderServices +{ + public class PurchaseTraderServicePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InventoryControllerClass), nameof(InventoryControllerClass.TryPurchaseTraderService)); + } + + [PatchPostfix] + public static async void PatchPostFix(Task __result, ETraderServiceType serviceType, AbstractQuestControllerClass questController, string subServiceId) + { + bool purchased = await __result; + if (purchased) + { + Logger.LogInfo($"Player purchased service {serviceType}"); + TraderServicesManager.Instance.AfterPurchaseTraderService(serviceType, questController, subServiceId); + } + else + { + Logger.LogInfo($"Player failed to purchase service {serviceType}"); + } + } + } +} diff --git a/project/Aki.Debugging/BTR/Models/TraderServiceModel.cs b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServiceModel.cs similarity index 89% rename from project/Aki.Debugging/BTR/Models/TraderServiceModel.cs rename to project/Aki.SinglePlayer/Utils/TraderServices/TraderServiceModel.cs index 2128c1e..facc783 100644 --- a/project/Aki.Debugging/BTR/Models/TraderServiceModel.cs +++ b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServiceModel.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using System.Collections.Generic; -namespace Aki.Debugging.BTR.Models +namespace Aki.SinglePlayer.Utils.TraderServices { public class TraderServiceModel { @@ -15,4 +15,4 @@ namespace Aki.Debugging.BTR.Models [JsonProperty("subServices")] public Dictionary SubServices { get; set; } } -} +} \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs new file mode 100644 index 0000000..f82e5ea --- /dev/null +++ b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs @@ -0,0 +1,164 @@ +using Aki.Common.Http; +using Comfort.Common; +using EFT; +using HarmonyLib; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using static BackendConfigSettingsClass; +using TraderServiceClass = GClass1789; + +namespace Aki.SinglePlayer.Utils.TraderServices +{ + public class TraderServicesManager + { + private static TraderServicesManager _instance; + + public static TraderServicesManager Instance + { + get + { + if (_instance == null) + { + _instance = new TraderServicesManager(); + } + + return _instance; + } + } + + private Dictionary> _servicePurchased { get; set; } + private HashSet _cachedTraders = new HashSet(); + + public TraderServicesManager() + { + _servicePurchased = new Dictionary>(); + } + + public void GetTraderServicesDataFromServer(string traderId) + { + Dictionary servicesData = Singleton.Instance.ServicesData; + var gameWorld = Singleton.Instance; + var player = gameWorld?.MainPlayer; + + if (gameWorld == null || player == null) + { + Debug.LogError("GetTraderServicesDataFromServer - Error fetching game objects"); + return; + } + + if (!player.Profile.TradersInfo.TryGetValue(traderId, out Profile.TraderInfo traderInfo)) + { + Debug.LogError("GetTraderServicesDataFromServer - Error fetching profile trader info"); + return; + } + + // Only request data from the server if it's not already cached + if (!_cachedTraders.Contains(traderId)) + { + var json = RequestHandler.GetJson($"/singleplayer/traderServices/getTraderServices/{traderId}"); + var traderServiceModels = JsonConvert.DeserializeObject>(json); + + foreach (var traderServiceModel in traderServiceModels) + { + ETraderServiceType serviceType = traderServiceModel.ServiceType; + ServiceData serviceData; + + // Only populate trader services that don't exist yet + if (!servicesData.ContainsKey(traderServiceModel.ServiceType)) + { + TraderServiceClass traderService = new TraderServiceClass(); + traderService.TraderId = traderId; + traderService.ServiceType = serviceType; + traderService.ItemsToPay = new Dictionary(); + if (traderServiceModel.ItemsToPay != null) + { + foreach (var item in traderServiceModel.ItemsToPay) + { + traderService.ItemsToPay[item.Key] = item.Value; + } + } + + // SubServices seem to be populated dynamically in the client (For BTR taxi atleast), so we can just ignore it + // NOTE: For future reference, this is a dict of `point id` to `price` for the BTR taxi + traderService.SubServices = new Dictionary(); + + // TODO: What is this used for? Maybe lightkeeper? + traderService.UniqueItems = new MongoID[] { }; + + // Convert our format to the backend settings format and store it + serviceData = new ServiceData(traderService); + servicesData[serviceData.ServiceType] = serviceData; + } + } + + _cachedTraders.Add(traderId); + } + + // Update service availability + foreach (var servicesDataPair in servicesData) + { + // Only update this trader's services + if (servicesDataPair.Value.TraderId != traderId) + { + continue; + } + + // TODO: We should probably actually calculate this? + var CanAfford = true; + + // Check whether we've purchased this service yet + var traderService = servicesDataPair.Key; + var WasPurchasedInThisRaid = IsServicePurchased(traderService, traderId); + traderInfo.SetServiceAvailability(traderService, CanAfford, WasPurchasedInThisRaid); + } + } + + public void AfterPurchaseTraderService(ETraderServiceType serviceType, AbstractQuestControllerClass questController, string subServiceId = null) + { + GameWorld gameWorld = Singleton.Instance; + Player player = gameWorld?.MainPlayer; + + if (gameWorld == null || player == null) + { + Debug.LogError("TryPurchaseTraderService - Error fetching game objects"); + return; + } + + // Service doesn't exist + if (!Singleton.Instance.ServicesData.TryGetValue(serviceType, out var serviceData)) + { + return; + } + + SetServicePurchased(serviceType, serviceData.TraderId); + } + + public void SetServicePurchased(ETraderServiceType serviceType, string traderId) + { + if (_servicePurchased.TryGetValue(serviceType, out var traderDict)) + { + traderDict[traderId] = true; + } + else + { + _servicePurchased[serviceType] = new Dictionary(); + _servicePurchased[serviceType][traderId] = true; + } + } + + public bool IsServicePurchased(ETraderServiceType serviceType, string traderId) + { + if (_servicePurchased.TryGetValue(serviceType, out var traderDict)) + { + if (traderDict.TryGetValue(traderId, out var result)) + { + return result; + } + } + + return false; + } + } +}