diff --git a/Libraries/SPTarkov.Server.Core/Exceptions/Items/ItemHelperException.cs b/Libraries/SPTarkov.Server.Core/Exceptions/Items/ItemHelperException.cs new file mode 100644 index 00000000..b0f3b065 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Exceptions/Items/ItemHelperException.cs @@ -0,0 +1,10 @@ +namespace SPTarkov.Server.Core.Exceptions.Items; + +public class ItemHelperException : Exception +{ + public ItemHelperException(string message) + : base(message) { } + + public ItemHelperException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs index 4056a842..0344752f 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using SPTarkov.DI.Annotations; +using SPTarkov.Server.Core.Exceptions.Items; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; @@ -128,7 +129,7 @@ public class ItemHelper( /// Item tpl to find /// OPTIONAL - slotId of desired item /// Item or null if no item found - public Item GetItemFromPoolByTpl(IEnumerable itemPool, MongoId tpl, string slotId = "") + public Item? GetItemFromPoolByTpl(IEnumerable itemPool, MongoId tpl, string slotId = "") { // Filter the pool by slotId if provided var filteredPool = string.IsNullOrEmpty(slotId) @@ -182,7 +183,7 @@ public class ItemHelper( Upd itemProperties = new(); // Armors, etc - if (itemTemplate.Properties.MaxDurability is not null) + if (itemTemplate.Properties?.MaxDurability is not null) { itemProperties.Repairable = new UpdRepairable { @@ -191,43 +192,34 @@ public class ItemHelper( }; } - if (itemTemplate.Properties.HasHinge ?? false) + if (itemTemplate.Properties?.HasHinge ?? false) { itemProperties.Togglable = new UpdTogglable { On = true }; } - if (itemTemplate.Properties.Foldable ?? false) + if (itemTemplate.Properties?.Foldable ?? false) { itemProperties.Foldable = new UpdFoldable { Folded = false }; } - if (itemTemplate.Properties.WeapFireType?.Any() ?? false) + if (itemTemplate.Properties?.WeapFireType?.Count == 0) { - if (itemTemplate.Properties.WeapFireType.Contains("fullauto")) - { - itemProperties.FireMode = new UpdFireMode { FireMode = "fullauto" }; - } - else - { - itemProperties.FireMode = new UpdFireMode { FireMode = randomUtil.GetArrayValue(itemTemplate.Properties.WeapFireType) }; - } + itemProperties.FireMode = itemTemplate.Properties.WeapFireType.Contains("fullauto") + ? new UpdFireMode { FireMode = "fullauto" } + : new UpdFireMode { FireMode = randomUtil.GetArrayValue(itemTemplate.Properties.WeapFireType) }; } - if (itemTemplate.Properties.MaxHpResource is not null) + if (itemTemplate.Properties?.MaxHpResource is not null) { itemProperties.MedKit = new UpdMedKit { HpResource = itemTemplate.Properties.MaxHpResource }; } - if (itemTemplate.Properties.MaxResource is not null && itemTemplate.Properties.FoodUseTime is not null) + if (itemTemplate.Properties?.MaxResource is not null && itemTemplate.Properties.FoodUseTime is not null) { itemProperties.FoodDrink = new UpdFoodDrink { HpPercent = itemTemplate.Properties.MaxResource }; } - if (itemTemplate.Parent == BaseClasses.FLASHLIGHT) - { - itemProperties.Light = new UpdLight { IsActive = false, SelectedMode = 0 }; - } - else if (itemTemplate.Parent == BaseClasses.TACTICAL_COMBO) + if (itemTemplate.Parent == BaseClasses.FLASHLIGHT || itemTemplate.Parent == BaseClasses.TACTICAL_COMBO) { itemProperties.Light = new UpdLight { IsActive = false, SelectedMode = 0 }; } @@ -238,7 +230,7 @@ public class ItemHelper( } // Toggleable face shield - if ((itemTemplate.Properties.HasHinge ?? false) && (itemTemplate.Properties.FaceShieldComponent ?? false)) + if ((itemTemplate.Properties?.HasHinge ?? false) && (itemTemplate.Properties.FaceShieldComponent ?? false)) { itemProperties.Togglable = new UpdTogglable { On = false }; } @@ -248,11 +240,17 @@ public class ItemHelper( /// /// Checks if a tpl is a valid item. Valid meaning that it's an item that can be stored in stash + ///

/// Valid means: + ///
/// Not quest item + ///
/// 'Item' type + ///
/// Not on the invalid base types array + ///
/// Price above 0 roubles + ///
///
/// Template id to check /// OPTIONAL - Base types deemed invalid @@ -262,6 +260,11 @@ public class ItemHelper( var baseTypes = invalidBaseTypes ?? _defaultInvalidBaseTypes; var itemDetails = GetItem(tpl); + if (itemDetails.Value is null) + { + return false; + } + return itemDetails.Key && IsValidItem(itemDetails.Value, baseTypes); } @@ -280,7 +283,7 @@ public class ItemHelper( { var baseTypes = invalidBaseTypes ?? _defaultInvalidBaseTypes; - return !(item.Properties.QuestItem ?? false) + return !(item.Properties?.QuestItem ?? false) && string.Equals(item.Type, "Item", StringComparison.OrdinalIgnoreCase) && GetItemPrice(item.Id) > 0 && !itemFilterService.IsItemBlacklisted(item.Id) @@ -346,7 +349,9 @@ public class ItemHelper( var itemTemplate = GetItem(itemTpl); return itemTemplate.Value?.Properties?.Slots is not null - && itemTemplate.Value.Properties.Slots.Any(slot => _removablePlateSlotIds.Contains(slot.Name.ToLowerInvariant())); + && itemTemplate.Value.Properties.Slots.Any(slot => + _removablePlateSlotIds.Contains(slot.Name?.ToLowerInvariant() ?? string.Empty) + ); } /// @@ -370,18 +375,13 @@ public class ItemHelper( } // Has no slots - if (!(itemDbDetails.Value.Properties.Slots ?? []).Any()) + if (!(itemDbDetails.Value?.Properties?.Slots ?? []).Any()) { return false; } // Check if item has slots that match soft insert name ids - if (itemDbDetails.Value.Properties.Slots.Any(slot => IsSoftInsertId(slot.Name.ToLowerInvariant()))) - { - return true; - } - - return false; + return itemDbDetails.Value?.Properties?.Slots?.Any(slot => IsSoftInsertId(slot.Name?.ToLowerInvariant() ?? string.Empty)) ?? false; } /// @@ -481,7 +481,7 @@ public class ItemHelper( /// Get cloned copy of all item data from items.json /// /// List of TemplateItem objects - public List GetItemsClone() + public List? GetItemsClone() { return cloner.Clone(databaseService.GetItems().Values.ToList()); } @@ -588,7 +588,7 @@ public class ItemHelper( if ( skipArmorItemsWithoutDurability && IsOfBaseclass(item.Template, BaseClasses.ARMOR) - && itemDetails?.Properties?.MaxDurability == 0 + && itemDetails.Properties?.MaxDurability == 0 ) { return -1; @@ -599,7 +599,7 @@ public class ItemHelper( if (item.Upd.MedKit is not null) { // Meds - result = (item.Upd.MedKit.HpResource ?? 0) / (itemDetails.Properties.MaxHpResource ?? 0); + result = (item.Upd.MedKit.HpResource ?? 0) / (itemDetails.Properties?.MaxHpResource ?? 0); } else if (item.Upd.Repairable is not null) { @@ -607,9 +607,9 @@ public class ItemHelper( } else if (item.Upd.FoodDrink is not null) { - result = (item.Upd.FoodDrink.HpPercent ?? 0) / (itemDetails.Properties.MaxResource ?? 0); + result = (item.Upd.FoodDrink.HpPercent ?? 0) / (itemDetails.Properties?.MaxResource ?? 0); } - else if (item.Upd.Key?.NumberOfUsages > 0 && itemDetails.Properties.MaximumNumberOfUsage > 0) + else if (item.Upd.Key?.NumberOfUsages > 0 && itemDetails.Properties?.MaximumNumberOfUsage > 0) { // keys - keys count upwards, not down like everything else var maxNumOfUsages = itemDetails.Properties.MaximumNumberOfUsage; @@ -618,11 +618,11 @@ public class ItemHelper( else if (item.Upd.Resource?.UnitsConsumed > 0) { // E.g. fuel tank - result = (item.Upd.Resource.Value ?? 0) / (itemDetails.Properties.MaxResource ?? 0); + result = (item.Upd.Resource.Value ?? 0) / (itemDetails.Properties?.MaxResource ?? 0); } else if (item.Upd.RepairKit is not null) { - result = (item.Upd.RepairKit.Resource ?? 0) / (itemDetails.Properties.MaxRepairResource ?? 0); + result = (item.Upd.RepairKit.Resource ?? 0) / (itemDetails.Properties?.MaxRepairResource ?? 0); } if (result == 0) @@ -732,7 +732,7 @@ public class ItemHelper( return null; } - return item.Properties.StackMaxSize > 1; + return item.Properties?.StackMaxSize > 1; } /// @@ -742,12 +742,12 @@ public class ItemHelper( /// List of root item + children. public List SplitStack(Item itemToSplit) { - if (itemToSplit?.Upd?.StackObjectsCount is null) + if (itemToSplit.Upd?.StackObjectsCount is null) { return [itemToSplit]; } - var maxStackSize = GetItem(itemToSplit.Template).Value.Properties.StackMaxSize; + var maxStackSize = GetItem(itemToSplit.Template).Value?.Properties?.StackMaxSize; var remainingCount = itemToSplit.Upd.StackObjectsCount; List rootAndChildren = []; @@ -755,7 +755,7 @@ public class ItemHelper( // return the item as is. if (remainingCount <= maxStackSize) { - rootAndChildren.Add(cloner.Clone(itemToSplit)); + rootAndChildren.Add(cloner.Clone(itemToSplit)!); return rootAndChildren; } @@ -765,8 +765,8 @@ public class ItemHelper( var amount = Math.Min(remainingCount.Value, maxStackSize ?? 0); var newStackClone = cloner.Clone(itemToSplit); - newStackClone.Id = new MongoId(); - newStackClone.Upd.StackObjectsCount = amount; + newStackClone!.Id = new MongoId(); + newStackClone.Upd!.StackObjectsCount = amount; remainingCount -= amount; rootAndChildren.Add(newStackClone); } @@ -788,7 +788,7 @@ public class ItemHelper( return [itemWithChildren]; } - var maxStackSize = GetItem(originRootItem.Template).Value.Properties.StackMaxSize; + var maxStackSize = GetItem(originRootItem.Template).Value?.Properties?.StackMaxSize; var remainingCount = originRootItem.Upd.StackObjectsCount; List> result = []; @@ -804,11 +804,11 @@ public class ItemHelper( while (remainingCount > 0) { // Clone item and make IDs unique - var itemWithChildrenClone = cloner.Clone(itemWithChildren).ReplaceIDs().ToList(); + var itemWithChildrenClone = cloner.Clone(itemWithChildren)!.ReplaceIDs().ToList(); // Set stack count to new value var amount = Math.Min(remainingCount.Value, maxStackSize ?? 0); - itemWithChildrenClone[0].Upd.StackObjectsCount = amount; + itemWithChildrenClone[0].Upd!.StackObjectsCount = amount; remainingCount -= amount; result.Add(itemWithChildrenClone); } @@ -824,7 +824,7 @@ public class ItemHelper( public List> SplitStackIntoSeparateItems(Item itemToSplit) { var itemTemplate = GetItem(itemToSplit.Template).Value; - var itemMaxStackSize = itemTemplate.Properties.StackMaxSize ?? 1; + var itemMaxStackSize = itemTemplate?.Properties?.StackMaxSize ?? 1; // item already within bounds of stack size, return it if (itemToSplit.Upd?.StackObjectsCount <= itemMaxStackSize) @@ -837,14 +837,14 @@ public class ItemHelper( // Split items stack into chunks List> result = []; - var remainingCount = itemToSplit.Upd.StackObjectsCount; + var remainingCount = itemToSplit.Upd?.StackObjectsCount; while (remainingCount != 0) { var amount = Math.Min(remainingCount ?? 0, itemMaxStackSize); - var newItemClone = cloner.Clone(itemToSplit); + var newItemClone = cloner.Clone(itemToSplit)!; newItemClone.Id = new MongoId(); - newItemClone.Upd.StackObjectsCount = amount; + newItemClone.Upd!.StackObjectsCount = amount; remainingCount -= amount; result.Add([newItemClone]); } @@ -885,7 +885,7 @@ public class ItemHelper( /// /// Item with mods to update. /// New id to add on children of base item. - public void ReplaceRootItemID(IEnumerable itemWithChildren, MongoId newId) + public void ReplaceRootItemId(IEnumerable itemWithChildren, MongoId newId) { // original id on base item var oldId = itemWithChildren.First().Id; @@ -931,6 +931,11 @@ public class ItemHelper( itemIdBlacklist.UnionWith(insuredItems.Select(x => x.ItemId.Value)); } + if (inventory.Items is null) + { + return; + } + foreach (var item in inventory.Items) { if (itemIdBlacklist.Contains(item.Id)) @@ -948,7 +953,7 @@ public class ItemHelper( item.Id = newId; // Find all children of item and update their parent ids to match - var childItems = inventory.Items.Where(x => x.ParentId == originalId); + var childItems = inventory.Items.Where(x => x.ParentId is not null && x.ParentId == originalId); foreach (var childItem in childItems) { childItem.ParentId = newId; @@ -960,7 +965,7 @@ public class ItemHelper( continue; } - // Update quickslot id + // Update quick-slot id if (inventory.FastPanel.ContainsKey(originalId)) { inventory.FastPanel[originalId] = newId; @@ -975,14 +980,8 @@ public class ItemHelper( /// Items to adjust the IDs of /// Player profile /// Insured items that should not have their IDs replaced - /// Quick slot panel /// Items - public IEnumerable ReplaceIDs( - IEnumerable originalItems, - PmcData? pmcData, - IEnumerable? insuredItems = null, - Dictionary? fastPanel = null - ) + public IEnumerable ReplaceIDs(IEnumerable originalItems, PmcData? pmcData, IEnumerable? insuredItems = null) { // Blacklist var itemIdBlacklist = new HashSet(); @@ -1033,12 +1032,12 @@ public class ItemHelper( } // Also replace in quick slot if the old ID exists. - if (pmcData.Inventory.FastPanel is null) + if (pmcData?.Inventory?.FastPanel is null) { continue; } - // Update quickslot id + // Update quick-slot id // TODO: i dont think the fast panel key is a mongoid, it should be e.g. "Item4" if (pmcData.Inventory.FastPanel.ContainsKey(originalId)) { @@ -1184,7 +1183,7 @@ public class ItemHelper( while (currentItem != null && IsAttachmentAttached(currentItem)) { - currentItem = itemsMap.FirstOrDefault(x => x.Key == currentItem.ParentId).Value; + currentItem = itemsMap.FirstOrDefault(kvp => kvp.Key == currentItem.ParentId).Value; if (currentItem == null) { return null; @@ -1203,9 +1202,11 @@ public class ItemHelper( { HashSet check = ["hideout", "main"]; + var slotId = item.SlotId ?? string.Empty; + return !( - check.Contains(item.SlotId) // Is root item - || _slotsAsStrings.Contains(item.SlotId) // Is root item in equipment slot e.g. `Headwear` + check.Contains(slotId) // Is root item + || _slotsAsStrings.Contains(slotId) // Is root item in equipment slot e.g. `Headwear` || int.TryParse(item.SlotId, out _) ); // Has int as slotId, is inside container. e.g. cartridges } @@ -1228,9 +1229,9 @@ public class ItemHelper( { var currentItem = itemsMap.GetValueOrDefault(itemId); - while (currentItem is not null && !_slotsAsStrings.Contains(currentItem.SlotId)) + while (currentItem is not null && !_slotsAsStrings.Contains(currentItem.SlotId ?? string.Empty)) { - currentItem = itemsMap.GetValueOrDefault(currentItem.ParentId); + currentItem = itemsMap.GetValueOrDefault(currentItem.ParentId ?? string.Empty); if (currentItem is null) { return null; @@ -1246,11 +1247,22 @@ public class ItemHelper( /// Item with children /// The base items root id /// ItemSize object (width and height) - public ItemSize GetItemSize(ICollection items, MongoId rootItemId) + public ItemSize? GetItemSize(ICollection items, MongoId rootItemId) { - var rootTemplate = GetItem(items.FirstOrDefault(x => x.Id == rootItemId).Template).Value; - var width = rootTemplate.Properties.Width; - var height = rootTemplate.Properties.Height; + var itemTemplate = items.FirstOrDefault(x => x.Id == rootItemId)?.Template; + if (itemTemplate is null) + { + return null; + } + + var rootTemplate = GetItem(itemTemplate.Value).Value; + if (rootTemplate is null) + { + return null; + } + + var width = rootTemplate.Properties?.Width; + var height = rootTemplate.Properties?.Height; var sizeUp = 0; var sizeDown = 0; @@ -1268,7 +1280,7 @@ public class ItemHelper( var itemDbTemplate = GetItem(item.Template).Value; // Calculating child ExtraSize - if (itemDbTemplate.Properties.ExtraSizeForceAdd ?? false) + if (itemDbTemplate?.Properties?.ExtraSizeForceAdd ?? false) { forcedUp += itemDbTemplate.Properties.ExtraSizeUp.Value; forcedDown += itemDbTemplate.Properties.ExtraSizeDown.Value; @@ -1277,11 +1289,11 @@ public class ItemHelper( } else { - sizeUp = sizeUp < itemDbTemplate.Properties.ExtraSizeUp ? itemDbTemplate.Properties.ExtraSizeUp.Value : sizeUp; - sizeDown = sizeDown < itemDbTemplate.Properties.ExtraSizeDown ? itemDbTemplate.Properties.ExtraSizeDown.Value : sizeDown; - sizeLeft = sizeLeft < itemDbTemplate.Properties.ExtraSizeLeft ? itemDbTemplate.Properties.ExtraSizeLeft.Value : sizeLeft; + sizeUp = sizeUp < itemDbTemplate?.Properties?.ExtraSizeUp ? itemDbTemplate.Properties.ExtraSizeUp.Value : sizeUp; + sizeDown = sizeDown < itemDbTemplate?.Properties?.ExtraSizeDown ? itemDbTemplate.Properties.ExtraSizeDown.Value : sizeDown; + sizeLeft = sizeLeft < itemDbTemplate?.Properties?.ExtraSizeLeft ? itemDbTemplate.Properties.ExtraSizeLeft.Value : sizeLeft; sizeRight = - sizeRight < itemDbTemplate.Properties.ExtraSizeRight ? itemDbTemplate.Properties.ExtraSizeRight.Value : sizeRight; + sizeRight < itemDbTemplate?.Properties?.ExtraSizeRight ? itemDbTemplate.Properties.ExtraSizeRight.Value : sizeRight; } } @@ -1316,10 +1328,10 @@ public class ItemHelper( /// Item template from items db public void AddCartridgesToAmmoBox(List ammoBox, TemplateItem ammoBoxDetails) { - var ammoBoxMaxCartridgeCount = ammoBoxDetails.Properties.StackSlots.First().MaxCount; - var cartridgeTpl = ammoBoxDetails.Properties.StackSlots.First().Props.Filters.First().Filter.FirstOrDefault(); - var cartridgeDetails = GetItem(cartridgeTpl); - var cartridgeMaxStackSize = cartridgeDetails.Value.Properties.StackMaxSize; + var ammoBoxMaxCartridgeCount = ammoBoxDetails.Properties?.StackSlots?.First().MaxCount; + var cartridgeTpl = ammoBoxDetails.Properties?.StackSlots?.First().Props?.Filters?.First().Filter?.FirstOrDefault(); + var cartridgeDetails = GetItem(cartridgeTpl.Value); + var cartridgeMaxStackSize = cartridgeDetails.Value?.Properties?.StackMaxSize; // Exit early if ammo already exists in box if (ammoBox.Any(item => item.Template.Equals(cartridgeTpl))) @@ -1339,7 +1351,7 @@ public class ItemHelper( var cartridgeCountToAdd = remainingSpace < maxPerStack ? remainingSpace : maxPerStack; // Add cartridge item into items array - var cartridgeItemToAdd = CreateCartridges(ammoBox[0].Id, cartridgeTpl, (int)cartridgeCountToAdd, location); + var cartridgeItemToAdd = CreateCartridges(ammoBox[0].Id, cartridgeTpl.Value, (int)cartridgeCountToAdd, location); // In live no ammo box has the first cartridge item with a location if (location == 0) @@ -1387,11 +1399,14 @@ public class ItemHelper( ) { var chosenCaliber = caliber ?? GetRandomValidCaliber(magTemplate); - - // Edge case - Klin pp-9 has a typo in its ammo caliber - if (chosenCaliber == "Caliber9x18PMM") + switch (chosenCaliber) { - chosenCaliber = "Caliber9x18PM"; + case null: + throw new ItemHelperException("Chosen caliber is null when trying to fill magazine with random cartridge"); + // Edge case - Klin pp-9 has a typo in its ammo caliber + case "Caliber9x18PMM": + chosenCaliber = "Caliber9x18PM"; + break; } // Chose a randomly weighted cartridge that fits @@ -1405,7 +1420,7 @@ public class ItemHelper( { if (logger.IsLogEnabled(LogLevel.Debug)) { - logger.Debug($"Unable to fill item: {magazine.FirstOrDefault().Id} {magTemplate.Name} with cartridges, none found."); + logger.Debug($"Unable to fill item: {magazine.FirstOrDefault()?.Id} {magTemplate.Name} with cartridges, none found."); } return; @@ -1428,8 +1443,8 @@ public class ItemHelper( double minSizeMultiplier = 0.25 ) { - var isUBGL = IsOfBaseclass(magTemplate.Id, BaseClasses.LAUNCHER); - if (isUBGL) + var isUbgl = IsOfBaseclass(magTemplate.Id, BaseClasses.LAUNCHER); + if (isUbgl) // UBGL don't have mags { return; @@ -1509,8 +1524,13 @@ public class ItemHelper( /// Tpl of cartridge protected string? GetRandomValidCaliber(TemplateItem magTemplate) { - var ammoTpls = magTemplate.Properties.Cartridges.First().Props.Filters.First().Filter; - var calibers = ammoTpls.Where(x => GetItem(x).Key).Select(x => GetItem(x).Value.Properties.Caliber).ToList(); + var ammoTpls = magTemplate.Properties?.Cartridges?.First().Props?.Filters?.First().Filter; + var calibers = ammoTpls?.Where(x => GetItem(x).Key).Select(x => GetItem(x).Value?.Properties?.Caliber).ToList(); + + if (calibers is null) + { + throw new ItemHelperException("Calibers is null when trying to generate random valid caliber"); + } return randomUtil.DrawRandomFromList(calibers).FirstOrDefault(); } @@ -1548,16 +1568,22 @@ public class ItemHelper( } var ammoArray = new ProbabilityObjectArray(cloner); - foreach (var icd in ammos) + foreach (var ammoDetails in ammos) { + if (ammoDetails.Tpl is null) + { + logger.Error("Ammo details tpl is null when trying to draw ammo from pool"); + continue; + } + // Whitelist exists and tpl not inside it, skip // Fixes 9x18mm kedr issues - if (cartridgeWhitelist is not null && !cartridgeWhitelist.Contains(icd.Tpl.Value)) + if (cartridgeWhitelist is not null && !cartridgeWhitelist.Contains(ammoDetails.Tpl.Value)) { continue; } - ammoArray.Add(new ProbabilityObject(icd.Tpl.Value, (double)icd.RelativeProbability, null)); + ammoArray.Add(new ProbabilityObject(ammoDetails.Tpl.Value, (double)ammoDetails.RelativeProbability, null)); } return ammoArray.Draw().FirstOrDefault(); @@ -1635,7 +1661,7 @@ public class ItemHelper( { var result = itemToAdd; HashSet incompatibleModTpls = []; - foreach (var slot in itemToAddTemplate.Properties.Slots) + foreach (var slot in itemToAddTemplate.Properties?.Slots ?? []) { // If only required mods is requested, skip non-essential if (requiredOnly && !(slot.Required ?? false)) @@ -1647,16 +1673,16 @@ public class ItemHelper( if (modSpawnChanceDict is not null && !(slot.Required ?? false)) { // only roll chance to not include mod if dict exists and has value for this mod type (e.g. front_plate) - if (modSpawnChanceDict.ContainsKey(slot.Name.ToLowerInvariant())) + if (modSpawnChanceDict.TryGetValue(slot.Name?.ToLowerInvariant() ?? string.Empty, out var value)) { - if (!randomUtil.GetChance100(modSpawnChanceDict[slot.Name.ToLowerInvariant()])) + if (!randomUtil.GetChance100(value)) { continue; } } } - var itemPool = slot.Props.Filters.FirstOrDefault().Filter ?? []; + var itemPool = slot.Props?.Filters?.FirstOrDefault()?.Filter ?? []; if (itemPool.Count == 0) { if (logger.IsLogEnabled(LogLevel.Debug)) @@ -1695,6 +1721,10 @@ public class ItemHelper( result.Add(modItemToAdd); var modItemDbDetails = GetItem(modItemToAdd.Template).Value; + if (modItemDbDetails?.Properties?.ConflictingItems is null) + { + continue; + } // Include conflicting items of newly added mod in pool to be used for next mod choice incompatibleModTpls.UnionWith(modItemDbDetails.Properties.ConflictingItems); @@ -1842,8 +1872,13 @@ public class ItemHelper( var containerTemplate = GetItem(containerTpl).Value; // Get height/width - var height = containerTemplate.Properties.Grids.First().Props.CellsV; - var width = containerTemplate.Properties.Grids.First().Props.CellsH; + var height = containerTemplate?.Properties?.Grids?.First().Props?.CellsV; + var width = containerTemplate?.Properties?.Grids?.First().Props?.CellsH; + + if (height is null || width is null) + { + throw new ItemHelperException("Height or width is null when trying to calculate container mapping"); + } return GetBlankContainerMap(width.Value, height.Value); }