Compare commits

...

8 Commits

Author SHA1 Message Date
Yui
7919daa086 feat: added TaskCache class 2024-11-26 02:36:36 -03:00
Yui
cabd008204 feat: replaced ItemUtils.GetArmorDurability() loop with Sum() function. 2024-11-26 02:35:35 -03:00
Yui
7734090feb chore: changed the access level of some functions 2024-11-22 21:30:48 -03:00
Yui
db4003caa7 feat: get best trader offer for items
TODO: Make a better conversion for USD/EUR to RUB
2024-11-08 17:17:10 -03:00
Yui
a86213d3a6 feat: async inventory processing
TODO: Do !!NOT!! process containers twice!
2024-11-06 17:45:58 -03:00
Yui
fa0821686b feat: added inventory patch, in-raid and stash 2024-11-04 01:37:34 -03:00
Yui
ac5140ce24 docs: added documentation on ItemExtensions.cs 2024-10-29 19:32:48 -03:00
Yui
313763cfc4 fix: fixed ammo count on magazines 2024-10-29 19:19:16 -03:00
17 changed files with 351 additions and 40 deletions

View File

@@ -0,0 +1,72 @@
using System.Collections.Concurrent;
namespace LootValueEX.Common;
internal class TaskCache
{
private readonly SemaphoreSlim _semaphore;
private readonly ConcurrentDictionary<Guid, Task> _taskDict;
internal TaskCache(int maxTasks = 10)
{
_semaphore = new SemaphoreSlim(maxTasks);
_taskDict = new ConcurrentDictionary<Guid, Task>();
}
private bool IsTaskCancelled(Guid taskGuid) =>
_taskDict.TryGetValue(taskGuid, out Task task) && task.Status == TaskStatus.Canceled;
internal Guid AddTask(Func<CancellationToken, Task> taskFactory)
{
Guid taskGuid = Guid.NewGuid();
CancellationTokenSource cts = new CancellationTokenSource();
Task task = taskFactory(cts.Token);
_taskDict[taskGuid] = task;
return taskGuid;
}
internal bool RemoveTask(Guid taskGuid)
{
if (_taskDict.TryGetValue(taskGuid, out Task task))
{
if (_taskDict.TryRemove(taskGuid, out _))
{
task.Dispose();
_semaphore.Release();
return true;
}
}
return false;
}
internal async Task<Task> RunTaskByGuidAsync(Guid taskGuid)
{
await _semaphore.WaitAsync();
try
{
if ((_taskDict.TryGetValue(taskGuid, out Task task)))
{
if (!task.IsCompleted && !task.Status.Equals(TaskStatus.Running))
await task;
return task;
}
Plugin.Log.LogError($"Task {taskGuid.ToString()} not found.");
}
catch (OperationCanceledException)
{
RemoveTask(taskGuid);
Plugin.Log.LogDebug($"Task {taskGuid.ToString()} got cancelled.");
}
return Task.FromResult(false);
}
internal async Task<Task> AddAndRunTaskAsync(Func<CancellationToken, Task> taskFactory)
{
Guid taskGuid = AddTask(taskFactory);
return await RunTaskByGuidAsync(taskGuid);
}
}

View File

@@ -10,7 +10,41 @@ namespace LootValueEX.Extensions
{
internal static class ItemExtensions
{
internal static bool IsExamined(this Item? item) => item != null && ClientAppUtils.GetMainApp().GetClientBackEndSession().Profile.Examined(item);
/// <summary>
/// Checks if an item has been examined by the player.
/// </summary>
/// <param name="item">The item to check.</param>
/// <returns>True if the item has been examined, false otherwise.</returns>
internal static bool IsExamined(this Item? item) => item != null &&
ClientAppUtils.GetMainApp().GetClientBackEndSession()
.Profile.Examined(item);
internal static bool IsStacked(this Item? item) =>
item != null && (item.StackObjectsCount > 1 || item.UnlimitedCount);
internal static string PrehashTemplate(this Item? item) =>
string.Format("{0}|{1}|{2}", item?.TemplateId, item.GetDurability(), item.GetUses());
internal static Item? UnstackItem(this Item? item)
{
if (item == null)
return null;
if (!item.IsStacked())
return item;
Item? itemClone = item.CloneItem();
itemClone.StackObjectsCount = 1;
itemClone.UnlimitedCount = false;
return itemClone;
}
/// <summary>
/// Retrieves the value of a specific attribute from an item.
/// </summary>
/// <param name="item">The item to retrieve the attribute from.</param>
/// <param name="attributeId">The ID of the attribute to retrieve.</param>
/// <returns>The value of the attribute, or -1f if the item is null or the attribute is not found.</returns>
internal static float GetItemAttribute(this Item? item, EItemAttributeId attributeId)
{
if (item == null)
@@ -24,19 +58,34 @@ namespace LootValueEX.Extensions
return -1f;
}
}
internal static string GetCustomHash(this Item? item)
/// <summary>
/// Generates a custom hash for the given item.
///
/// This function takes an optional Item as a parameter and returns its custom hash.
/// If the item is null, it returns an empty string. The custom hash is determined by the item's template, durability, and uses.
/// </summary>
/// <param name="item">The item to retrieve the custom hash for.</param>
/// <returns>The custom hash of the item, or an empty string if the item is null.</returns>
internal static async Task<string> GetCustomHashAsync(this Item? item)
{
if (item == null)
return string.Empty;
StringBuilder prehashString = new StringBuilder();
item.GetAllItems().Where(prop => !prop.Equals(item)).ExecuteForEach(prop => prehashString.Append(prop.GetCustomHash()));
if (item.Template.Equals(typeof(MagazineTemplate))){
item.GetAllItems().Where(i => !i.Equals(item)).DoMap(i => prehashString.Append(i.PrehashTemplate()));
if (item.Template.Equals(typeof(MagazineTemplate)))
{
MagazineTemplate magTemplate = (MagazineTemplate)item.Template;
magTemplate.Cartridges.ExecuteForEach(prop => prop.Items.ExecuteForEach(ammo => prehashString.Append(ammo.GetCustomHash())));
magTemplate.Cartridges.DoMap(s => s.Items.DoMap(i => prehashString.Append(i.PrehashTemplate())));
}
string itemHashTemplate = string.Format("{0}|{1}|{2}|{3}", prehashString.ToString(), item.TemplateId, item.GetDurability(), item.GetUses());
return Utils.HashingUtils.ConvertToSha256(itemHashTemplate);
prehashString.Append(item.PrehashTemplate());
return await Task.Run(() => Utils.HashingUtils.ConvertToSha256(prehashString.ToString()));
}
internal static string GetCustomHash(this Item? item) =>
Task.Run(() => item?.GetCustomHashAsync()).Result ?? string.Empty;
#if DEBUG
internal static string AttributesToString(this Item? item)
{
@@ -47,27 +96,51 @@ namespace LootValueEX.Extensions
return String.Empty;
StringBuilder sb = new StringBuilder();
item.Attributes.ForEach(attr => sb.Append($"\n{attr.Id}\n\tName: {attr.Name}\n\tBase Value: {attr.Base()}\n\tString value: {attr.StringValue()}\n\tDisplay Type: {attr.DisplayType()}"));
item.Attributes.ForEach(attr =>
sb.Append(
$"\n{attr.Id}\n\tName: {attr.Name}\n\tBase Value: {attr.Base()}\n\tString value: {attr.StringValue()}\n\tDisplay Type: {attr.DisplayType()}"));
return sb.ToString();
}
#endif
/// <summary>
/// Retrieves the durability of a given item.
///
/// This function takes an optional Item as a parameter and returns its durability.
/// If the item is null, it returns -1f. The durability is determined by the item's template.
/// </summary>
/// <param name="item">The item to retrieve the durability for.</param>
/// <returns>The durability of the item, or -1f if the item is null.</returns>
internal static float GetDurability(this Item? item)
{
if (item == null)
return -1f;
switch (item.Template)
{
case ArmoredRigTemplate armoredRig:
case ArmorTemplate armor:
return Utils.ItemUtils.GetArmorDurability(item.GetItemComponentsInChildren<RepairableComponent>(true));
return Utils.ItemUtils.GetArmorDurability(
item.GetItemComponentsInChildren<RepairableComponent>(true));
default:
return item.GetItemAttribute(EItemAttributeId.Durability);
}
}
/// <summary>
/// Retrieves the number of uses remaining for a given item.
///
/// This function takes an optional Item as a parameter and returns the number of uses remaining.
/// If the item is null, it returns -1f. The number of uses is determined by the item's template.
/// </summary>
/// <param name="item">The item to retrieve the number of uses for.</param>
/// <returns>The number of uses remaining for the item, or -1f if the item is null.</returns>
internal static float GetUses(this Item? item)
{
if (item == null)
return -1f;
switch (item.Template)
{
case KeycardTemplate:
@@ -77,6 +150,12 @@ namespace LootValueEX.Extensions
if (item.TryGetItemComponent(out MedKitComponent medKitComponent))
return medKitComponent.HpResource;
return -1f;
case MagazineTemplate:
MagazineClass? magazineClass = item as MagazineClass;
return magazineClass?.Count ?? -1f;
case AmmoBoxTemplate:
AmmoBox? ammoBox = item as AmmoBox;
return ammoBox?.Count ?? -1f;
default:
return -1f;
}

View File

@@ -0,0 +1,57 @@
using System.Diagnostics;
using System.Reflection;
using EFT;
using EFT.HealthSystem;
using EFT.UI;
using LootValueEX.Extensions;
using SPT.Reflection.Patching;
// ReSharper disable InconsistentNaming
namespace LootValueEX.Patches.Screens
{
internal class InventoryScreenPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() => typeof(InventoryScreen).GetMethods().SingleOrDefault(method => method.Name == "Show" && method.GetParameters()[0].ParameterType == typeof(IHealthController));
[PatchPostfix]
private static void PatchPostfix(ref Profile ___profile_0, ref LootItemClass ___lootItemClass)
{
Profile profile = ___profile_0;
TaskCompletionSource<bool> tcsInventory = new();
CancellationTokenSource ctsInventory = new CancellationTokenSource(5000);
Task<bool> taskInventory = tcsInventory.Task;
Task.Factory.StartNew(async () =>
{
Stopwatch sw = Stopwatch.StartNew();
foreach(EFT.InventoryLogic.Item item in profile.Inventory.GetPlayerItems(EFT.InventoryLogic.EPlayerItems.Equipment))
{
//Plugin.Log.LogDebug($"Equip Process: {item.LocalizedName()} ({item.TemplateId}): {await item.GetCustomHashAsync()}");
continue;
}
sw.Stop();
Plugin.Log.LogDebug($"Equipment processing finished in {sw.ElapsedMilliseconds}ms");
tcsInventory.SetResult(true);
}, ctsInventory.Token);
if(___lootItemClass != null)
{
LootItemClass lootItemClass = ___lootItemClass;
TaskCompletionSource<bool> tcsLoot = new();
CancellationTokenSource ctsLoot = new CancellationTokenSource(5000);
Task<bool> taskLoot = tcsLoot.Task;
Task.Factory.StartNew(async () =>
{
Stopwatch sw = Stopwatch.StartNew();
foreach (EFT.InventoryLogic.Item item in lootItemClass.GetAllItems())
{
//Plugin.Log.LogDebug($"LootItemClass process: {item.LocalizedName()} ({item.TemplateId}): {await item.GetCustomHashAsync()}");
continue;
}
Plugin.Log.LogDebug($"LootItemClass processing finished in {sw.ElapsedMilliseconds}ms");
tcsLoot.SetResult(true);
}, ctsLoot.Token);
}
}
}
}

View File

@@ -5,7 +5,7 @@ using SPT.Reflection.Patching;
using SPT.Reflection.Utils;
using System.Reflection;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
/// <summary>
/// This patch will affect the following screens: Stash, Weapon Preset Builder, Character Gear, Character Preset Selector, New Ragfair Offer, Message Items, Loot

View File

@@ -5,7 +5,7 @@ using SPT.Reflection.Patching;
using SPT.Reflection.Utils;
using System.Reflection;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
/// <summary>
/// This patch will affect the following screens: Stash, Weapon Preset Builder, Character Gear, Character Preset Selector, New Ragfair Offer, Message Items, Loot
@@ -14,7 +14,7 @@ namespace LootValueEX.Patches
{
protected override MethodBase GetTargetMethod() => typeof(GridItemView).GetMethod("ShowTooltip", BindingFlags.Instance | BindingFlags.Public);
internal static bool PatchTooltip { get; private set; } = false;
internal static EFT.InventoryLogic.Item? HoveredItem { get; private set; }
internal static Item? HoveredItem { get; private set; }
[PatchPrefix]
static void EnableTooltipPatch(GridItemView __instance)

View File

@@ -7,7 +7,7 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
internal class HandbookPatching : ModulePatch
{

View File

@@ -2,7 +2,7 @@
using SPT.Reflection.Patching;
using System.Reflection;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
class InsuranceGridPatch : ModulePatch
{

View File

@@ -2,7 +2,7 @@
using SPT.Reflection.Patching;
using System.Reflection;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
class InsuranceSlotPatch : ModulePatch
{

View File

@@ -1,7 +1,7 @@
using SPT.Reflection.Patching;
using System.Reflection;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
internal class ItemPricePatch : ModulePatch
{

View File

@@ -10,7 +10,7 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
internal class TooltipPatch : ModulePatch
{
@@ -19,12 +19,10 @@ namespace LootValueEX.Patches
[PatchPostfix]
public static void AlterText(SimpleTooltip __instance, string text)
{
StackTrace stackTrace = new StackTrace();
Plugin.Log.LogDebug("Stacktrace of tooltip call: \n" + stackTrace.ToString());
if (GridItemTooltipPatch.PatchTooltip)
{
text += $"<br>TemplateID: {GridItemTooltipPatch.HoveredItem?.TemplateId}<br>Template: {GridItemTooltipPatch.HoveredItem?.Template}<br>Item hashsum: {GridItemTooltipPatch.HoveredItem?.GetHashSum()}<br>Custom hash: {GridItemTooltipPatch.HoveredItem?.GetCustomHash()}<br>Item durability: {GridItemTooltipPatch.HoveredItem?.GetDurability()}<br>Item uses: {GridItemTooltipPatch.HoveredItem?.GetUses()}<br><color=#ff0fff><b>GridItemView</b></color>";
Plugin.Log.LogDebug(GridItemTooltipPatch.HoveredItem?.AttributesToString());
Structs.TradeOfferStruct tradeOffer = Utils.ItemUtils.GetBestTraderValue(GridItemTooltipPatch.HoveredItem);
text += $"<br>Hash: {GridItemTooltipPatch.HoveredItem?.GetCustomHash()}<br>Trader: {tradeOffer.TraderID}<br>Value: {tradeOffer.Price}<br>Currency: {tradeOffer.CurrencyID}<br>Price in rubles: {tradeOffer.PriceInRouble}<br><color=#ff0fff><b>GridItemView</b></color>";
}
if (InsuranceSlotPatch.PatchTooltip)
{

View File

@@ -3,7 +3,7 @@ using SPT.Reflection.Patching;
using SPT.Reflection.Utils;
using System.Reflection;
namespace LootValueEX.Patches
namespace LootValueEX.Patches.Tooltips
{
class TradingItemPatch : ModulePatch
{

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using SPT.Reflection.Patching;
namespace LootValueEX.Patches
{
internal class TraderClassPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() => typeof(TraderClass).GetConstructors().First();
[PatchPostfix]
private static void GenerateSupplyData(ref TraderClass __instance)
{
Plugin.Log.LogDebug($"Generating Assortment Data for trader {__instance.Id}");
__instance.RefreshAssortment(true, true);
}
}
}

View File

@@ -11,14 +11,18 @@ namespace LootValueEX
{
Log = base.Logger;
new Patches.GridItemTooltipPatch().Enable();
new Patches.TooltipPatch().Enable();
new Patches.InsuranceGridPatch().Enable();
new Patches.InsuranceSlotPatch().Enable();
new Patches.ItemPricePatch().Enable();
new Patches.TradingItemPatch().Enable();
new Patches.HandbookPatching().Enable();
new Patches.BarterItemPatch().Enable();
new Patches.TraderClassPatch().Enable();
new Patches.Tooltips.GridItemTooltipPatch().Enable();
new Patches.Tooltips.TooltipPatch().Enable();
new Patches.Tooltips.InsuranceGridPatch().Enable();
new Patches.Tooltips.InsuranceSlotPatch().Enable();
new Patches.Tooltips.ItemPricePatch().Enable();
new Patches.Tooltips.TradingItemPatch().Enable();
new Patches.Tooltips.HandbookPatching().Enable();
new Patches.Tooltips.BarterItemPatch().Enable();
new Patches.Screens.InventoryScreenPatch().Enable();
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LootValueEX.Structs
{
internal readonly struct TradeOfferStruct
{
internal readonly string TraderID;
internal readonly string CurrencyID;
internal readonly int Price;
internal readonly float PriceInRouble;
internal TradeOfferStruct(string traderId, string currencyId, int price, float priceInRouble)
{
TraderID = traderId;
CurrencyID = currencyId;
Price = price;
PriceInRouble = priceInRouble;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Comfort.Common;
using EFT.InventoryLogic;
using SPT.Reflection.Utils;
using EFTCurrencyUtils = GClass2531;
namespace LootValueEX.Utils
{
internal static class EconomyUtils
{
internal static float ConvertToRuble(string id, float amount)
{
if (!EFTCurrencyUtils.TryGetCurrencyType(id, out ECurrencyType currencyType))
return 0f;
if (currencyType.Equals(ECurrencyType.RUB))
return amount;
return amount * (float)Singleton<HandbookClass>.Instance.GetBasePrice(id);
}
internal static Structs.TradeOfferStruct GetTraderItemOffer(TraderClass trader, Item item)
{
Plugin.Log.LogDebug($"GetTraderItemOffer: {item.LocalizedName()} - {trader.LocalizedName}");
TraderClass.GStruct244? tradeOffer = trader.GetUserItemPrice(item);
if (tradeOffer == null)
{
Plugin.Log.LogDebug("GetTraderItemOffer: tradeOffer == null");
return default;
}
if(tradeOffer.Value.Amount > 0)
{
Plugin.Log.LogDebug($"{trader.LocalizedName}\n\tCurrencyID: {tradeOffer.Value.CurrencyId}\n\tValue: {tradeOffer.Value.Amount}\n\tPrice in RB: {EconomyUtils.ConvertToRuble(tradeOffer.Value.CurrencyId, tradeOffer.Value.Amount)}");
return new Structs.TradeOfferStruct(trader.Id, tradeOffer.Value.CurrencyId, tradeOffer.Value.Amount, EconomyUtils.ConvertToRuble(tradeOffer.Value.CurrencyId, tradeOffer.Value.Amount));
}
Plugin.Log.LogDebug("GetTraderItemOffer: no value");
return default;
}
}
}

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace LootValueEX.Utils
{
internal class HashingUtils
internal static class HashingUtils
{
internal static string ConvertToSha256(string value)
{

View File

@@ -1,17 +1,29 @@
using EFT.InventoryLogic;
using LootValueEX.Extensions;
using SPT.Reflection.Utils;
namespace LootValueEX.Utils
{
internal class ItemUtils
internal static class ItemUtils
{
public static float GetArmorDurability(IEnumerable<RepairableComponent> repairableComponents)
internal static float GetArmorDurability(IEnumerable<RepairableComponent> repairableComponents) => repairableComponents.Sum(x => x.Durability);
internal static Task<Structs.TradeOfferStruct> GetBestTraderValueAsync(Item item)
{
float totalDurability = 0;
foreach (RepairableComponent component in repairableComponents)
Structs.TradeOfferStruct bestOffer = new();
item = item.UnstackItem();
foreach (TraderClass trader in ClientAppUtils.GetMainApp().GetClientBackEndSession().DisplayableTraders)
{
totalDurability += component.Durability;
Structs.TradeOfferStruct currentOffer = EconomyUtils.GetTraderItemOffer(trader, item);
if (currentOffer.Equals(default) || currentOffer.Price <= 0)
continue;
if (currentOffer.PriceInRouble > bestOffer.PriceInRouble)
bestOffer = currentOffer;
}
return totalDurability;
return Task.FromResult(bestOffer);
}
public static Structs.TradeOfferStruct GetBestTraderValue(Item item) => Task.Run(() => GetBestTraderValueAsync(item)).Result;
}
}