0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 06:30:43 -05:00

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: SPT-AKI/Modules#61
Co-authored-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
Co-committed-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
This commit is contained in:
DrakiaXYZ 2024-01-15 09:09:31 +00:00 committed by chomp
parent f3141db764
commit 4dee774efc
10 changed files with 237 additions and 138 deletions

View File

@ -29,6 +29,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" /> <ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" /> <ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
<ProjectReference Include="..\Aki.SinglePlayer\Aki.SinglePlayer.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,4 +1,5 @@
using Aki.Debugging.BTR.Utils; using Aki.Debugging.BTR.Utils;
using Aki.SinglePlayer.Utils.TraderServices;
using Comfort.Common; using Comfort.Common;
using EFT; using EFT;
using EFT.InventoryLogic; using EFT.InventoryLogic;
@ -220,7 +221,7 @@ namespace Aki.Debugging.BTR
btrMachineGunWeapon = BTRUtil.CreateItem(BTRUtil.BTRMachineGunWeaponTplId); btrMachineGunWeapon = BTRUtil.CreateItem(BTRUtil.BTRMachineGunWeaponTplId);
// Pull services data for the BTR from the server // Pull services data for the BTR from the server
BTRUtil.PopulateTraderServicesData(BTRUtil.BTRTraderId); TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId);
} }
/** /**

View File

@ -1,5 +1,6 @@
using Aki.Debugging.BTR.Utils; using Aki.Debugging.BTR.Utils;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.SinglePlayer.Utils.TraderServices;
using EFT; using EFT;
using EFT.Console.Core; using EFT.Console.Core;
using EFT.UI; using EFT.UI;
@ -42,7 +43,7 @@ namespace Aki.Debugging.BTR.Patches
internal static void PatchPrefix(Profile.ETraderServiceSource traderServiceSourceType, ref bool useDebugData) internal static void PatchPrefix(Profile.ETraderServiceSource traderServiceSourceType, ref bool useDebugData)
{ {
useDebugData = false; useDebugData = false;
BTRUtil.PopulateTraderServicesData(Profile.TraderInfo.TraderServiceToId[traderServiceSourceType]); TraderServicesManager.Instance.GetTraderServicesDataFromServer(Profile.TraderInfo.TraderServiceToId[traderServiceSourceType]);
} }
} }
} }

View File

@ -1,11 +1,11 @@
using Aki.Debugging.BTR.Utils; using Aki.Debugging.BTR.Utils;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.SinglePlayer.Utils.TraderServices;
using Comfort.Common; using Comfort.Common;
using EFT; using EFT;
using EFT.UI; using EFT.UI;
using HarmonyLib; using HarmonyLib;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
namespace Aki.Debugging.BTR.Patches namespace Aki.Debugging.BTR.Patches
{ {
@ -26,18 +26,8 @@ namespace Aki.Debugging.BTR.Patches
return; return;
} }
GameWorld gameWorld = Singleton<GameWorld>.Instance;
var player = gameWorld?.MainPlayer;
var btrManager = gameWorld?.GetComponent<BTRManager>();
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 // Update the trader services information now that we've used this service
btrManager.SetServicePurchased(ETraderServiceType.BtrItemsDelivery, BTRUtil.BTRTraderId); TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId);
BTRUtil.UpdateTraderServices(BTRUtil.BTRTraderId);
} }
} }
} }

View File

@ -1,5 +1,4 @@
using Aki.Common.Http; using Aki.Common.Http;
using Aki.Debugging.BTR.Models;
using Comfort.Common; using Comfort.Common;
using EFT; using EFT;
using EFT.InventoryLogic; using EFT.InventoryLogic;
@ -29,127 +28,6 @@ namespace Aki.Debugging.BTR.Utils
_ = nameof(TraderServiceClass.WasPurchasedInThisRaid); _ = 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<List<TraderServiceModel>>(json);
Dictionary<ETraderServiceType, ServiceData> servicesData = Singleton<BackendConfigSettingsClass>.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<MongoID, int>();
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<string, int>();
// 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<ETraderServiceType>;
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<GameWorld>.Instance;
btrManager = gameWorld?.GetComponent<BTRManager>();
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;
}
/// <summary> /// <summary>
/// Used to create an instance of the item in-raid. /// Used to create an instance of the item in-raid.
/// </summary> /// </summary>

View File

@ -6,6 +6,7 @@ using Aki.SinglePlayer.Patches.Progression;
using Aki.SinglePlayer.Patches.Quests; using Aki.SinglePlayer.Patches.Quests;
using Aki.SinglePlayer.Patches.RaidFix; using Aki.SinglePlayer.Patches.RaidFix;
using Aki.SinglePlayer.Patches.ScavMode; using Aki.SinglePlayer.Patches.ScavMode;
using Aki.SinglePlayer.Patches.TraderServices;
using BepInEx; using BepInEx;
namespace Aki.SinglePlayer namespace Aki.SinglePlayer
@ -57,6 +58,8 @@ namespace Aki.SinglePlayer
new LabsKeycardRemovalPatch().Enable(); new LabsKeycardRemovalPatch().Enable();
new ScavLateStartPatch().Enable(); new ScavLateStartPatch().Enable();
new MidRaidAchievementChangePatch().Enable(); new MidRaidAchievementChangePatch().Enable();
new GetTraderServicesPatch().Enable();
new PurchaseTraderServicePatch().Enable();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -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;
}
}
}

View File

@ -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<bool> __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}");
}
}
}
}

View File

@ -2,7 +2,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
namespace Aki.Debugging.BTR.Models namespace Aki.SinglePlayer.Utils.TraderServices
{ {
public class TraderServiceModel public class TraderServiceModel
{ {

View File

@ -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<ETraderServiceType, Dictionary<string, bool>> _servicePurchased { get; set; }
private HashSet<string> _cachedTraders = new HashSet<string>();
public TraderServicesManager()
{
_servicePurchased = new Dictionary<ETraderServiceType, Dictionary<string, bool>>();
}
public void GetTraderServicesDataFromServer(string traderId)
{
Dictionary<ETraderServiceType, ServiceData> servicesData = Singleton<BackendConfigSettingsClass>.Instance.ServicesData;
var gameWorld = Singleton<GameWorld>.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<List<TraderServiceModel>>(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<MongoID, int>();
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<string, int>();
// 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<GameWorld>.Instance;
Player player = gameWorld?.MainPlayer;
if (gameWorld == null || player == null)
{
Debug.LogError("TryPurchaseTraderService - Error fetching game objects");
return;
}
// Service doesn't exist
if (!Singleton<BackendConfigSettingsClass>.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<string, bool>();
_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;
}
}
}