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 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) internal static float GetItemAttribute(this Item? item, EItemAttributeId attributeId)
{ {
if (item == null) if (item == null)
@@ -24,19 +58,34 @@ namespace LootValueEX.Extensions
return -1f; 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) if (item == null)
return string.Empty; return string.Empty;
StringBuilder prehashString = new StringBuilder(); 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; 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 #if DEBUG
internal static string AttributesToString(this Item? item) internal static string AttributesToString(this Item? item)
{ {
@@ -47,27 +96,51 @@ namespace LootValueEX.Extensions
return String.Empty; return String.Empty;
StringBuilder sb = new StringBuilder(); 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(); return sb.ToString();
} }
#endif #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) internal static float GetDurability(this Item? item)
{ {
if (item == null) if (item == null)
return -1f; return -1f;
switch (item.Template) switch (item.Template)
{ {
case ArmoredRigTemplate armoredRig: case ArmoredRigTemplate armoredRig:
case ArmorTemplate armor: case ArmorTemplate armor:
return Utils.ItemUtils.GetArmorDurability(item.GetItemComponentsInChildren<RepairableComponent>(true)); return Utils.ItemUtils.GetArmorDurability(
item.GetItemComponentsInChildren<RepairableComponent>(true));
default: default:
return item.GetItemAttribute(EItemAttributeId.Durability); 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) internal static float GetUses(this Item? item)
{ {
if (item == null) if (item == null)
return -1f; return -1f;
switch (item.Template) switch (item.Template)
{ {
case KeycardTemplate: case KeycardTemplate:
@@ -77,6 +150,12 @@ namespace LootValueEX.Extensions
if (item.TryGetItemComponent(out MedKitComponent medKitComponent)) if (item.TryGetItemComponent(out MedKitComponent medKitComponent))
return medKitComponent.HpResource; return medKitComponent.HpResource;
return -1f; 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: default:
return -1f; 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 SPT.Reflection.Utils;
using System.Reflection; using System.Reflection;
namespace LootValueEX.Patches namespace LootValueEX.Patches.Tooltips
{ {
/// <summary> /// <summary>
/// This patch will affect the following screens: Stash, Weapon Preset Builder, Character Gear, Character Preset Selector, New Ragfair Offer, Message Items, Loot /// 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 SPT.Reflection.Utils;
using System.Reflection; using System.Reflection;
namespace LootValueEX.Patches namespace LootValueEX.Patches.Tooltips
{ {
/// <summary> /// <summary>
/// This patch will affect the following screens: Stash, Weapon Preset Builder, Character Gear, Character Preset Selector, New Ragfair Offer, Message Items, Loot /// 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); protected override MethodBase GetTargetMethod() => typeof(GridItemView).GetMethod("ShowTooltip", BindingFlags.Instance | BindingFlags.Public);
internal static bool PatchTooltip { get; private set; } = false; 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] [PatchPrefix]
static void EnableTooltipPatch(GridItemView __instance) static void EnableTooltipPatch(GridItemView __instance)

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LootValueEX.Patches namespace LootValueEX.Patches.Tooltips
{ {
internal class TooltipPatch : ModulePatch internal class TooltipPatch : ModulePatch
{ {
@@ -19,12 +19,10 @@ namespace LootValueEX.Patches
[PatchPostfix] [PatchPostfix]
public static void AlterText(SimpleTooltip __instance, string text) 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) 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>"; Structs.TradeOfferStruct tradeOffer = Utils.ItemUtils.GetBestTraderValue(GridItemTooltipPatch.HoveredItem);
Plugin.Log.LogDebug(GridItemTooltipPatch.HoveredItem?.AttributesToString()); 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) if (InsuranceSlotPatch.PatchTooltip)
{ {

View File

@@ -3,7 +3,7 @@ using SPT.Reflection.Patching;
using SPT.Reflection.Utils; using SPT.Reflection.Utils;
using System.Reflection; using System.Reflection;
namespace LootValueEX.Patches namespace LootValueEX.Patches.Tooltips
{ {
class TradingItemPatch : ModulePatch 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; Log = base.Logger;
new Patches.GridItemTooltipPatch().Enable(); new Patches.TraderClassPatch().Enable();
new Patches.TooltipPatch().Enable();
new Patches.InsuranceGridPatch().Enable(); new Patches.Tooltips.GridItemTooltipPatch().Enable();
new Patches.InsuranceSlotPatch().Enable(); new Patches.Tooltips.TooltipPatch().Enable();
new Patches.ItemPricePatch().Enable(); new Patches.Tooltips.InsuranceGridPatch().Enable();
new Patches.TradingItemPatch().Enable(); new Patches.Tooltips.InsuranceSlotPatch().Enable();
new Patches.HandbookPatching().Enable(); new Patches.Tooltips.ItemPricePatch().Enable();
new Patches.BarterItemPatch().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 namespace LootValueEX.Utils
{ {
internal class HashingUtils internal static class HashingUtils
{ {
internal static string ConvertToSha256(string value) internal static string ConvertToSha256(string value)
{ {

View File

@@ -1,17 +1,29 @@
using EFT.InventoryLogic; using EFT.InventoryLogic;
using LootValueEX.Extensions;
using SPT.Reflection.Utils;
namespace LootValueEX.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; Structs.TradeOfferStruct bestOffer = new();
foreach (RepairableComponent component in repairableComponents) 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;
} }
} }