Helper methods extensions (#431)

* Remove unused model

* Start moving methods to extensions, cleanup code
This commit is contained in:
Jesse
2025-06-28 13:14:50 +02:00
committed by GitHub
parent a3dbd3176e
commit 22c71bee5b
20 changed files with 248 additions and 276 deletions
@@ -1145,7 +1145,7 @@ public class RagfairController
};
}
_itemHelper.FixItemStackCount(rootItem);
rootItem.FixItemStackCount();
itemsToReturn.Add(
_itemHelper.FindAndReturnChildrenAsItems(pmcData.Inventory.Items, itemId)
@@ -223,5 +223,58 @@ namespace SPTarkov.Server.Core.Extensions
return list;
}
/// Check if the passed in item has buy count restrictions
/// </summary>
/// <param name="itemToCheck">Item to check</param>
/// <returns>true if it has buy restrictions</returns>
public static bool HasBuyRestrictions(this Item itemToCheck)
{
return itemToCheck.Upd?.BuyRestrictionCurrent is not null
&& itemToCheck.Upd?.BuyRestrictionMax is not null;
}
/// <summary>
/// Gets the identifier for a child using slotId, locationX and locationY.
/// </summary>
/// <param name="item">Item.</param>
/// <returns>SlotId OR slotid, locationX, locationY.</returns>
public static string GetChildId(this Item item)
{
if (item.Location is null)
{
return item.SlotId;
}
var LocationTyped = (ItemLocation)item.Location;
return $"{item.SlotId},{LocationTyped.X},{LocationTyped.Y}";
}
public static bool IsVertical(this ItemLocation itemLocation)
{
var castValue = itemLocation.R.ToString();
return castValue == "1"
|| string.Equals(castValue, "vertical", StringComparison.OrdinalIgnoreCase)
|| string.Equals(
itemLocation.Rotation?.ToString(),
"vertical",
StringComparison.OrdinalIgnoreCase
);
}
/// <summary>
/// Update items upd.StackObjectsCount to be 1 if its upd is missing or StackObjectsCount is undefined
/// </summary>
/// <param name="item">Item to update</param>
/// <returns>Fixed item</returns>
public static void FixItemStackCount(this Item item)
{
// Ensure item has 'Upd' object
item.Upd ??= new Upd { StackObjectsCount = 1 };
// Ensure item has 'StackObjectsCount' property
item.Upd.StackObjectsCount ??= 1;
}
}
}
@@ -0,0 +1,49 @@
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Enums;
namespace SPTarkov.Server.Core.Extensions
{
public static class ProductionExtensions
{
private static readonly HashSet<string> _idCheck =
[
HideoutHelper.BitcoinFarm,
HideoutHelper.CultistCircleCraftId,
];
/// <summary>
/// Has the craft completed
/// Ignores bitcoin farm/cultist circle as they're continuous crafts
/// </summary>
/// <param name="craft">Craft to check</param>
/// <returns>True when craft is complete</returns>
public static bool IsCraftComplete(this Production craft)
{
return craft.Progress >= craft.ProductionTime && !_idCheck.Contains(craft.RecipeId);
}
/// <summary>
/// Is a craft from a particular hideout area
/// </summary>
/// <param name="craft">Craft to check</param>
/// <param name="hideoutType">Type to check craft against</param>
/// <returns>True if it is from that area</returns>
public static bool IsCraftOfType(this Production craft, HideoutAreas hideoutType)
{
switch (hideoutType)
{
case HideoutAreas.WaterCollector:
return craft.RecipeId == HideoutHelper.WaterCollector;
case HideoutAreas.BitcoinFarm:
return craft.RecipeId == HideoutHelper.BitcoinFarm;
case HideoutAreas.ScavCase:
return craft.SptIsScavCase ?? false;
case HideoutAreas.CircleOfCultists:
return craft.SptIsCultistCircle ?? false;
default:
return false;
}
}
}
}
@@ -110,5 +110,59 @@ namespace SPTarkov.Server.Core.Extensions
Points = 0,
};
}
/// <summary>
/// Recursively checks if the given item is
/// inside the stash, that is it has the stash as
/// ancestor with slotId=hideout
/// </summary>
/// <param name="pmcData">Player profile</param>
/// <param name="itemToCheck">Item to look for</param>
/// <returns>True if item exists inside stash</returns>
public static bool IsItemInStash(this PmcData pmcData, Item itemToCheck)
{
// Start recursive check
return pmcData.IsParentInStash(itemToCheck.Id);
}
public static bool IsParentInStash(this PmcData pmcData, string itemId)
{
// Item not found / has no parent
var item = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemId);
if (item?.ParentId is null)
{
return false;
}
// Root level. Items parent is the stash with slotId "hideout"
if (item.ParentId == pmcData.Inventory.Stash && item.SlotId == "hideout")
{
return true;
}
// Recursive case: Check the items parent
return IsParentInStash(pmcData, item.ParentId);
}
/// <summary>
/// Iterate over all bonuses and sum up all bonuses of desired type in provided profile
/// </summary>
/// <param name="pmcProfile">Player profile</param>
/// <param name="desiredBonus">Bonus to sum up</param>
/// <returns>Summed bonus value or 0 if no bonus found</returns>
public static double GetBonusValueFromProfile(
this PmcData pmcProfile,
BonusType desiredBonus
)
{
var bonuses = pmcProfile?.Bonuses?.Where(b => b.Type == desiredBonus);
if (!bonuses.Any())
{
return 0;
}
// Sum all bonuses found above
return bonuses?.Sum(bonus => bonus?.Value ?? 0) ?? 0;
}
}
}
@@ -0,0 +1,41 @@
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
namespace SPTarkov.Server.Core.Extensions
{
public static class TemplateItemExtensions
{
public static IEnumerable<TemplateItem> OfClass(
this Dictionary<string, TemplateItem> templates,
params string[] baseClasses
)
{
return templates.Where(x => baseClasses.Contains(x.Value.Parent)).Select(x => x.Value);
}
public static IEnumerable<TemplateItem> OfClass(
this Dictionary<string, TemplateItem> templates,
Func<TemplateItem, bool> pred,
params string[] baseClasses
)
{
return templates
.Where(x => baseClasses.Contains(x.Value.Parent) && pred(x.Value))
.Select(x => x.Value);
}
/// <summary>
/// Check if item is quest item
/// </summary>
/// <param name="tpl">Items tpl to check quest status of</param>
/// <returns>true if item is flagged as quest item</returns>
public static bool IsQuestItem(this TemplateItem templateItem)
{
if (templateItem.Properties.QuestItem.GetValueOrDefault(false))
{
return true;
}
return false;
}
}
}
@@ -0,0 +1,13 @@
namespace SPTarkov.Server.Core.Extensions
{
public static class UtilityExtensions
{
public static List<T> IntersectWith<T>(this List<T> first, List<T> second)
{
//a.Intersect(x => b.Contains(x)).ToList();
// gives error Delegate type could not be infered
return first.Where(x => second.Contains(x)).ToList();
}
}
}
@@ -23,7 +23,6 @@ public class BotEquipmentModGenerator(
ISptLogger<BotEquipmentModGenerator> _logger,
HashUtil _hashUtil,
RandomUtil _randomUtil,
ProbabilityHelper _probabilityHelper,
DatabaseService _databaseService,
ItemHelper _itemHelper,
BotEquipmentFilterService _botEquipmentFilterService,
@@ -1103,7 +1102,7 @@ public class BotEquipmentModGenerator(
return ModSpawn.SPAWN;
}
var spawnMod = _probabilityHelper.RollChance(
var spawnMod = _randomUtil.RollChance(
modSpawnChances.GetValueOrDefault(modSlotName.ToLower())
);
if (
@@ -1,6 +1,7 @@
using System.Collections.Frozen;
using System.Text.RegularExpressions;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Helpers.Dialog.Commando.SptCommands;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Eft.Dialog;
@@ -323,7 +324,7 @@ public class GiveSptCommand(
protected bool IsItemAllowed(TemplateItem templateItem)
{
return templateItem.Type != "Node"
&& !_itemHelper.IsQuestItem(templateItem.Id)
&& !templateItem.IsQuestItem()
&& !_itemFilterService.IsItemBlacklisted(templateItem.Id)
&& (templateItem.Properties?.Prefab?.Path ?? "") != ""
&& !_itemHelper.IsOfBaseclasses(
@@ -1,6 +0,0 @@
using SPTarkov.DI.Annotations;
namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class GameEventHelper { }
@@ -35,7 +35,6 @@ public class HideoutHelper(
public const string BitcoinProductionId = "5d5c205bd582a50d042a3c0e";
public const string WaterCollector = "5d5589c1f934db045e6c5492";
public const int MaxSkillPoint = 5000;
protected HashSet<string> _idCheck = [BitcoinFarm, CultistCircleCraftId];
/// <summary>
/// Add production to profiles' Hideout.Production array
@@ -341,20 +340,20 @@ public class HideoutHelper(
}
// Skip processing (Don't skip continuous crafts like bitcoin farm or cultist circle)
if (IsCraftComplete(craft))
if (craft.IsCraftComplete())
{
continue;
}
// Special handling required
if (IsCraftOfType(craft, HideoutAreas.ScavCase))
if (craft.IsCraftOfType(HideoutAreas.ScavCase))
{
UpdateScavCaseProductionTimer(pmcData, prodId.Key);
continue;
}
if (IsCraftOfType(craft, HideoutAreas.WaterCollector))
if (craft.IsCraftOfType(HideoutAreas.WaterCollector))
{
UpdateWaterCollectorProductionTimer(pmcData, prodId.Key, hideoutProperties);
@@ -362,7 +361,7 @@ public class HideoutHelper(
}
// Continuous craft
if (IsCraftOfType(craft, HideoutAreas.BitcoinFarm))
if (craft.IsCraftOfType(HideoutAreas.BitcoinFarm))
{
UpdateBitcoinFarm(
pmcData,
@@ -375,7 +374,7 @@ public class HideoutHelper(
}
// No recipe, needs special handling
if (IsCraftOfType(craft, HideoutAreas.CircleOfCultists))
if (craft.IsCraftOfType(HideoutAreas.CircleOfCultists))
{
UpdateCultistCircleCraftProgress(pmcData, prodId.Key);
@@ -397,43 +396,6 @@ public class HideoutHelper(
}
}
/// <summary>
/// Is a craft from a particular hideout area
/// </summary>
/// <param name="craft">Craft to check</param>
/// <param name="hideoutType">Type to check craft against</param>
/// <returns>True if it is from that area</returns>
protected bool IsCraftOfType(Production craft, HideoutAreas hideoutType)
{
switch (hideoutType)
{
case HideoutAreas.WaterCollector:
return craft.RecipeId == WaterCollector;
case HideoutAreas.BitcoinFarm:
return craft.RecipeId == BitcoinFarm;
case HideoutAreas.ScavCase:
return craft.SptIsScavCase ?? false;
case HideoutAreas.CircleOfCultists:
return craft.SptIsCultistCircle ?? false;
default:
_logger.Error(
$"Unhandled hideout area: {hideoutType}, assuming craft: {craft.RecipeId} is not of this type"
);
return false;
}
}
/// <summary>
/// Has the craft completed
/// Ignores bitcoin farm/cultist circle as they're continuous crafts
/// </summary>
/// <param name="craft">Craft to check</param>
/// <returns>True when craft is complete</returns>
protected bool IsCraftComplete(Production craft)
{
return craft.Progress >= craft.ProductionTime && !_idCheck.Contains(craft.RecipeId);
}
/// <summary>
/// Update progress timer for water collector
/// </summary>
@@ -512,7 +512,7 @@ public class InventoryHelper(
profile.Inventory.Items,
itemId
);
if (itemAndChildrenToRemove.Count == 0)
if (!itemAndChildrenToRemove.Any())
{
if (_logger.IsLogEnabled(LogLevel.Debug))
{
@@ -924,8 +924,8 @@ public class InventoryHelper(
var tmpSize = GetSizeByInventoryItemHash(item.Template, item.Id, inventoryItemHash);
var iW = tmpSize[0]; // x
var iH = tmpSize[1]; // y
var fH = IsVertical(itemLocation) ? iW : iH;
var fW = IsVertical(itemLocation) ? iH : iW;
var fH = itemLocation.IsVertical() ? iW : iH;
var fW = itemLocation.IsVertical() ? iH : iW;
for (var y = 0; y < fH; y++)
{
@@ -958,18 +958,6 @@ public class InventoryHelper(
return containerYX;
}
protected bool IsVertical(ItemLocation itemLocation)
{
var castValue = itemLocation.R.ToString();
return castValue == "1"
|| string.Equals(castValue, "vertical", StringComparison.OrdinalIgnoreCase)
|| string.Equals(
itemLocation.Rotation?.ToString(),
"vertical",
StringComparison.OrdinalIgnoreCase
);
}
protected InventoryItemHash GetInventoryItemHash(List<Item> inventoryItems)
{
var inventoryItemHash = new InventoryItemHash
@@ -1370,39 +1358,6 @@ public class InventoryHelper(
return _inventoryConfig;
}
/// <summary>
/// Recursively checks if the given item is
/// inside the stash, that is it has the stash as
/// ancestor with slotId=hideout
/// </summary>
/// <param name="pmcData">Player profile</param>
/// <param name="itemToCheck">Item to look for</param>
/// <returns>True if item exists inside stash</returns>
public bool IsItemInStash(PmcData pmcData, Item itemToCheck)
{
// Start recursive check
return IsParentInStash(itemToCheck.Id, pmcData);
}
protected static bool IsParentInStash(string itemId, PmcData pmcData)
{
// Item not found / has no parent
var item = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemId);
if (item?.ParentId is null)
{
return false;
}
// Root level. Items parent is the stash with slotId "hideout"
if (item.ParentId == pmcData.Inventory.Stash && item.SlotId == "hideout")
{
return true;
}
// Recursive case: Check the items parent
return IsParentInStash(item.ParentId, pmcData);
}
public void ValidateInventoryUsesMongoIds(List<Item> itemsToValidate)
{
var errors = itemsToValidate
@@ -499,22 +499,6 @@ public class ItemHelper(
return null;
}
/// <summary>
/// Update items upd.StackObjectsCount to be 1 if its upd is missing or StackObjectsCount is undefined
/// </summary>
/// <param name="item">Item to update</param>
/// <returns>Fixed item</returns>
public Item FixItemStackCount(Item item)
{
// Ensure item has 'Upd' object
item.Upd ??= new Upd { StackObjectsCount = 1 };
// Ensure item has 'StackObjectsCount' property
item.Upd.StackObjectsCount ??= 1;
return item;
}
/// <summary>
/// Get cloned copy of all item data from items.json
/// </summary>
@@ -807,17 +791,6 @@ public class ItemHelper(
return list;
}
/// <summary>
/// Check if the passed in item has buy count restrictions
/// </summary>
/// <param name="itemToCheck">Item to check</param>
/// <returns>true if it has buy restrictions</returns>
public bool HasBuyRestrictions(Item itemToCheck)
{
return itemToCheck.Upd?.BuyRestrictionCurrent is not null
&& itemToCheck.Upd?.BuyRestrictionMax is not null;
}
/// <summary>
/// Checks if the passed template id is a dog tag.
/// </summary>
@@ -828,23 +801,6 @@ public class ItemHelper(
return _dogTagTpls.Contains(tpl);
}
/// <summary>
/// Gets the identifier for a child using slotId, locationX and locationY.
/// </summary>
/// <param name="item">Item.</param>
/// <returns>SlotId OR slotid, locationX, locationY.</returns>
public string GetChildId(Item item)
{
if (item.Location is null)
{
return item.SlotId;
}
var LocationTyped = (ItemLocation)item.Location;
return $"{item.SlotId},{LocationTyped.X},{LocationTyped.Y}";
}
/// <summary>
/// Checks if the passed item can be stacked.
/// </summary>
@@ -1305,22 +1261,6 @@ public class ItemHelper(
return DoesItemOrParentsIdMatch(item.Parent, tplsToCheck);
}
/// <summary>
/// Check if item is quest item
/// </summary>
/// <param name="tpl">Items tpl to check quest status of</param>
/// <returns>true if item is flagged as quest item</returns>
public bool IsQuestItem(string tpl)
{
var itemDetails = GetItem(tpl);
if (itemDetails.Key && itemDetails.Value.Properties.QuestItem.GetValueOrDefault(false))
{
return true;
}
return false;
}
/// <summary>
/// Checks to see if the item is *actually* moddable in-raid. Checks include the items existence in the database, the
/// parent items existence in the database, the existence (and value) of the items RaidModdable property, and that
@@ -1,20 +0,0 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Utils;
namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class ProbabilityHelper(ISptLogger<ProbabilityHelper> _logger, RandomUtil _randomUtil)
{
/// <summary>
/// Chance to roll a number out of 100
/// </summary>
/// <param name="chance">Percentage chance roll should success</param>
/// <param name="scale">scale of chance to allow support of numbers > 1-100</param>
/// <returns>true if success</returns>
public bool RollChance(double chance, double scale = 1)
{
return _randomUtil.GetInt(1, (int)(100 * scale)) / (1 * scale) <= chance;
}
}
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Eft.Ragfair;
using SPTarkov.Server.Core.Models.Enums;
@@ -16,7 +17,6 @@ public class RagfairHelper(
HandbookHelper handbookHelper,
ItemHelper itemHelper,
RagfairLinkedItemService ragfairLinkedItemService,
UtilityHelper utilityHelper,
ConfigServer configServer,
ICloner cloner
)
@@ -86,7 +86,7 @@ public class RagfairHelper(
if (!string.IsNullOrEmpty(request.HandbookId))
{
var handbook = GetCategoryList(request.HandbookId);
result = result?.Count > 0 ? utilityHelper.ArrayIntersect(result, handbook) : handbook;
result = result?.Count > 0 ? result.IntersectWith(handbook) : handbook;
}
return result;
@@ -157,25 +157,25 @@ public class RagfairHelper(
foreach (var item in items)
{
var itemFixed = itemHelper.FixItemStackCount(item);
item.FixItemStackCount();
var isChild = items.Any(it => it.Id == itemFixed.ParentId);
var isChild = items.Any(it => it.Id == item.ParentId);
if (!isChild)
{
if (rootItem == null)
{
rootItem = cloner.Clone(itemFixed);
rootItem = cloner.Clone(item);
rootItem.Upd.OriginalStackObjectsCount = rootItem.Upd.StackObjectsCount;
}
else
{
rootItem.Upd.StackObjectsCount += itemFixed.Upd.StackObjectsCount;
list.Add(itemFixed);
rootItem.Upd.StackObjectsCount += item.Upd.StackObjectsCount;
list.Add(item);
}
}
else
{
list.Add(itemFixed);
list.Add(item);
}
}
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Enums;
using SPTarkov.Server.Core.Models.Spt.Config;
@@ -75,7 +76,7 @@ public class RagfairServerHelper(
}
// Skip quest items
if (blacklistConfig.EnableQuestList && itemHelper.IsQuestItem(itemDetails.Value.Id))
if (blacklistConfig.EnableQuestList && itemDetails.Value.IsQuestItem())
{
return false;
}
@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Eft.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Eft.Inventory;
@@ -76,8 +77,7 @@ public class TradeHelper(
var itemPurchased = offerWithItem.Items.FirstOrDefault();
// Ensure purchase does not exceed trader item limit
var assortHasBuyRestrictions = _itemHelper.HasBuyRestrictions(itemPurchased);
if (assortHasBuyRestrictions)
if (itemPurchased.HasBuyRestrictions())
{
CheckPurchaseIsWithinTraderItemLimit(
sessionID,
@@ -170,8 +170,7 @@ public class TradeHelper(
);
// Ensure purchase does not exceed trader item limit
var assortHasBuyRestrictions = _itemHelper.HasBuyRestrictions(itemPurchased);
if (assortHasBuyRestrictions)
if (itemPurchased.HasBuyRestrictions())
// Will throw error if check fails
{
CheckPurchaseIsWithinTraderItemLimit(
@@ -195,7 +194,7 @@ public class TradeHelper(
// Decrement trader item count
itemPurchased.Upd.StackObjectsCount -= buyCount;
if (assortHasBuyRestrictions)
if (itemPurchased.HasBuyRestrictions())
{
var itemPurchaseDat = new PurchaseDetails
{
@@ -1,15 +0,0 @@
using SPTarkov.DI.Annotations;
namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class UtilityHelper
{
public List<T> ArrayIntersect<T>(List<T> a, List<T> b)
{
//a.Intersect(x => b.Contains(x)).ToList();
// gives error Delegate type could not be infered
return a.Where(x => b.Contains(x)).ToList();
}
}
@@ -1,65 +0,0 @@
using System.Text.Json.Serialization;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
namespace SPTarkov.Server.Core.Models.Eft.Player;
public record PlayerIncrementSkillLevelRequestData
{
[JsonExtensionData]
public Dictionary<string, object>? ExtensionData { get; set; }
[JsonPropertyName("_id")]
public string? Id { get; set; }
[JsonPropertyName("experience")]
public int? Experience { get; set; }
[JsonPropertyName("quests")]
public List<object>? Quests { get; set; }
[JsonPropertyName("ragFairOffers")]
public List<object>? RagFairOffers { get; set; }
[JsonPropertyName("builds")]
public List<object>? Builds { get; set; }
[JsonPropertyName("items")]
public Items? Items { get; set; }
[JsonPropertyName("production")]
public Production? Production { get; set; }
[JsonPropertyName("skills")]
public Skills? Skills { get; set; }
[JsonPropertyName("traderRelations")]
public TraderRelations? TraderRelations { get; set; }
}
// TODO: These are all lists of objects.
public record Items
{
[JsonExtensionData]
public Dictionary<string, object>? ExtensionData { get; set; }
[JsonPropertyName("new")]
public List<object>? NewItems { get; set; }
[JsonPropertyName("change")]
public List<object>? ChangedItems { get; set; }
[JsonPropertyName("del")]
public List<object>? DeletedItems { get; set; }
}
public record Production
{
[JsonExtensionData]
public Dictionary<string, object>? ExtensionData { get; set; }
}
public record TraderRelations
{
[JsonExtensionData]
public Dictionary<string, object>? ExtensionData { get; set; }
}
@@ -238,7 +238,7 @@ public class PaymentService(
}
// Item is not in the stash
if (!_inventoryHelper.IsItemInStash(pmcData, item))
if (!pmcData.IsItemInStash(item))
{
continue;
}
@@ -461,4 +461,15 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
{
return GetCollectionValue(list);
}
/// <summary>
/// Chance to roll a number out of 100
/// </summary>
/// <param name="chance">Percentage chance roll should success</param>
/// <param name="scale">scale of chance to allow support of numbers > 1-100</param>
/// <returns>true if success</returns>
public bool RollChance(double chance, double scale = 1)
{
return GetInt(1, (int)(100 * scale)) / (1 * scale) <= chance;
}
}