diff --git a/Libraries/Core/Generators/BotEquipmentModGenerator.cs b/Libraries/Core/Generators/BotEquipmentModGenerator.cs index 95736e79..ba1bdf71 100644 --- a/Libraries/Core/Generators/BotEquipmentModGenerator.cs +++ b/Libraries/Core/Generators/BotEquipmentModGenerator.cs @@ -61,7 +61,7 @@ public class BotEquipmentModGenerator( } // Iterate over mod pool and choose mods to add to item - foreach (var (modSlotName, modPool) in compatibleModsPool) + foreach (var (modSlotName, modPool) in compatibleModsPool ?? []) { // Get the templates slot object from db var itemSlotTemplate = GetModItemSlotFromDb(modSlotName, parentTemplate); diff --git a/Libraries/Core/Generators/BotInventoryGenerator.cs b/Libraries/Core/Generators/BotInventoryGenerator.cs index 49345186..cf5be899 100644 --- a/Libraries/Core/Generators/BotInventoryGenerator.cs +++ b/Libraries/Core/Generators/BotInventoryGenerator.cs @@ -393,7 +393,7 @@ public class BotInventoryGenerator( // Roll dice on equipment item var shouldSpawn = _randomUtil.GetChance100(spawnChance ?? 0); - if (shouldSpawn && !settings.RootEquipmentPool.Any()) + if (shouldSpawn && settings.RootEquipmentPool.Any()) { var pickedItemDb = new TemplateItem(); var found = false; @@ -469,7 +469,8 @@ public class BotInventoryGenerator( ); // Edge case: Filter the armor items mod pool if bot exists in config dict + config has armor slot - if (_botConfig.Equipment[settings.BotData.EquipmentRole] is not null && + if (_botConfig.Equipment.ContainsKey(settings.BotData.EquipmentRole) && + settings.RandomisationDetails is not null && settings.RandomisationDetails.RandomisedArmorSlots.Contains(settings.RootEquipmentSlot.ToString())) { // Filter out mods from relevant blacklist @@ -480,7 +481,8 @@ public class BotInventoryGenerator( } // Does item have slots for sub-mods to be inserted into - if (pickedItemDb.Properties.Slots.Any() && settings.GenerateModsBlacklist.Contains(pickedItemDb.Id)) + if (pickedItemDb.Properties?.Slots?.Count > 0 + && settings.GenerateModsBlacklist.Contains(pickedItemDb.Id)) { var childItemsToAdd = _botEquipmentModGenerator.GenerateModsForEquipment( [item], diff --git a/Libraries/Core/Helpers/BotGeneratorHelper.cs b/Libraries/Core/Helpers/BotGeneratorHelper.cs index 563638d6..3d4adb69 100644 --- a/Libraries/Core/Helpers/BotGeneratorHelper.cs +++ b/Libraries/Core/Helpers/BotGeneratorHelper.cs @@ -474,13 +474,13 @@ public class BotGeneratorHelper( } // Get container to put item into - var container = (inventory.Items ?? []).FirstOrDefault(item => item.SlotId == equipmentSlotId.ToString()); + var container = (inventory.Items).FirstOrDefault(item => item.SlotId == equipmentSlotId.ToString()); if (container is null) { missingContainerCount++; if (missingContainerCount == equipmentSlots.Count) { - // Bot doesnt have any containers we want to add item to + // Bot doesn't have any containers we want to add item to _logger.Debug( $"Unable to add item: {itemWithChildren.FirstOrDefault()?.Template} to bot as it lacks the following containers: {string.Join(",", equipmentSlots)}" ); diff --git a/Libraries/Core/Helpers/ContainerHelper.cs b/Libraries/Core/Helpers/ContainerHelper.cs index 8942a56e..04050f83 100644 --- a/Libraries/Core/Helpers/ContainerHelper.cs +++ b/Libraries/Core/Helpers/ContainerHelper.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using SptCommon.Annotations; namespace Core.Helpers; @@ -13,9 +13,59 @@ public class ContainerHelper /// Width of item /// Height of item /// Location to place item in container - public FindSlotResult FindSlotForItem(List> container2D, int itemWidth, int itemHeight) + public FindSlotResult FindSlotForItem(int[][] container2D, int itemWidth, int itemHeight) { - throw new NotImplementedException(); + var rotation = false; + var minVolume = (itemWidth < itemHeight ? itemWidth : itemHeight) - 1; + var containerY = container2D.Length; + var containerX = container2D[0].Length; + var limitY = containerY - minVolume; + var limitX = containerX - minVolume; + + // Every x+y slot taken up in container, exit + if (container2D.All((x) => x.All((y) =>y == 1))) + { + return new FindSlotResult(false); + } + + // Down + for (var y = 0; y < limitY; y++) + { + // Across + if (container2D[y].All((x) => x == 1)) + { + // Every item in row is full, skip row + continue; + } + + for (var x = 0; x < limitX; x++) + { + var foundSlot = LocateSlot(container2D, containerX, containerY, x, y, itemWidth, itemHeight); + + // Failed to find slot, rotate item and try again + if (!foundSlot && itemWidth * itemHeight > 1) + { + // Bigger than 1x1 + foundSlot = LocateSlot(container2D, containerX, containerY, x, y, itemHeight, itemWidth); // Height/Width swapped + if (foundSlot) + { + // Found a slot for it when rotated + rotation = true; + } + } + + if (!foundSlot) + { + // Didn't fit this hole, try again + continue; + } + + return new FindSlotResult(true, x, y, rotation); + } + } + + // Tried all possible holes, nothing big enough for the item + return new FindSlotResult(false); } /// @@ -30,7 +80,7 @@ public class ContainerHelper /// Items height /// True - slot found protected bool LocateSlot( - List> container2D, + int[][] container2D, int containerX, int containerY, int x, @@ -51,7 +101,7 @@ public class ContainerHelper /// Items height /// is item rotated public void FillContainerMapWithItem( - List> container2D, + int[][] container2D, int x, int y, int itemW, @@ -64,14 +114,31 @@ public class ContainerHelper public class FindSlotResult { + public FindSlotResult(bool success) + { + Success = success; + } + + public FindSlotResult(bool success, int x, int y, bool rotation) + { + Success = success; + X = x; + Y = y; + Rotation = rotation; + } + + public FindSlotResult() + { + } + [JsonPropertyName("success")] public bool? Success { get; set; } [JsonPropertyName("x")] - public double? X { get; set; } + public int? X { get; set; } [JsonPropertyName("y")] - public double? Y { get; set; } + public int? Y { get; set; } [JsonPropertyName("rotation")] public bool? Rotation { get; set; } diff --git a/Libraries/Core/Helpers/InventoryHelper.cs b/Libraries/Core/Helpers/InventoryHelper.cs index 829c3898..1ce5272e 100644 --- a/Libraries/Core/Helpers/InventoryHelper.cs +++ b/Libraries/Core/Helpers/InventoryHelper.cs @@ -8,6 +8,12 @@ using Core.Models.Spt.Config; using Core.Models.Spt.Inventory; using Core.Models.Utils; using Core.Services; +using Core.Models.Eft.Player; +using System.ComponentModel; +using Core.Models.Eft.Hideout; +using Core.Models.Enums; +using Core.Models.Spt.Bots; +using Core.Models.Spt.Services; namespace Core.Helpers; @@ -16,6 +22,7 @@ public class InventoryHelper( ISptLogger _logger, ProfileHelper _profileHelper, DialogueHelper _dialogueHelper, + ItemHelper _itemHelper, LocalisationService _localisationService ) { @@ -187,7 +194,8 @@ public class InventoryHelper( /// [width, height] public List GetItemSize(string? itemTpl, string itemId, List inventoryItems) { - throw new NotImplementedException(); + // -> Prepares item Width and height returns [sizeX, sizeY] + return GetSizeByInventoryItemHash(itemTpl, itemId, GetInventoryItemHash(inventoryItems)); } /// @@ -198,9 +206,126 @@ public class InventoryHelper( /// Items id /// Hashmap of inventory items /// An array representing the [width, height] of the item - protected List GetSizeByInventoryItemHash(string itemTpl, string itemId, InventoryItemHash inventoryItemHash) + protected List GetSizeByInventoryItemHash(string itemTpl, string itemID, InventoryItemHash inventoryItemHash) { - throw new NotImplementedException(); + var toDo = new List { itemID }; + var result = _itemHelper.GetItem(itemTpl); + var tmpItem = result.Value; + + // Invalid item + if (!result.Key) + { + _logger.Error(_localisationService.GetText("inventory-invalid_item_missing_from_db", itemTpl)); + } + + // Item found but no _props property + if (tmpItem is not null && tmpItem.Properties is null) + { + _localisationService.GetText("inventory-item_missing_props_property", new { + itemTpl = itemTpl, + itemName = tmpItem?.Name + }); + } + + // No item object or getItem() returned false + if (tmpItem is null && result.Value is null) + { + // return default size of 1x1 + _logger.Error(_localisationService.GetText("inventory-return_default_size", itemTpl)); + + return [1, 1]; // Invalid input data, return defaults + } + + var rootItem = inventoryItemHash.ByItemId[itemID]; + var foldableWeapon = tmpItem.Properties.Foldable; + var foldedSlot = tmpItem.Properties.FoldedSlot; + + var sizeUp = 0; + var sizeDown = 0; + var sizeLeft = 0; + var sizeRight = 0; + + var forcedUp = 0; + var forcedDown = 0; + var forcedLeft = 0; + var forcedRight = 0; + var outX = (int)tmpItem.Properties.Width; + var outY = (int)tmpItem.Properties.Height; + + // Item types to ignore + var skipThisItems = new List { BaseClasses.BACKPACK, BaseClasses.SEARCHABLE_ITEM, BaseClasses.SIMPLE_CONTAINER }; + + var rootFolded = rootItem?.Upd?.Foldable?.Folded == true; + + // The item itself is collapsible + if (foldableWeapon is not null && string.IsNullOrEmpty(foldedSlot) && rootFolded) + { + outX -= tmpItem.Properties.SizeReduceRight.Value; + } + + // Calculate size contribution from child items/attachments + if (!skipThisItems.Contains(tmpItem.Parent)) + { + while (toDo.Count > 0) + { + if (inventoryItemHash.ByParentId.ContainsKey(toDo[0])) { + foreach (var item in inventoryItemHash.ByParentId[toDo[0]]) { + // Filtering child items outside of mod slots, such as those inside containers, without counting their ExtraSize attribute + if (item.SlotId.IndexOf("mod_") < 0) + { + continue; + } + + toDo.Add(item.Id); + + // If the barrel is folded the space in the barrel is not counted + var itemResult = _itemHelper.GetItem(item.Template); + if (!itemResult.Key) + { + _logger.Error( + _localisationService.GetText("inventory-get_item_size_item_not_found_by_tpl", item.Template)); + } + + var itm = itemResult.Value; + var childFoldable = itm.Properties.Foldable.GetValueOrDefault(false); + var childFolded = item.Upd?.Foldable is not null && item.Upd.Foldable.Folded == true; + + if (foldableWeapon is true && foldedSlot == item.SlotId && (rootFolded || childFolded)) + { + continue; + } + + if (childFoldable && rootFolded && childFolded) + { + continue; + } + + // Calculating child ExtraSize + if (itm.Properties.ExtraSizeForceAdd == true) + { + forcedUp += itm.Properties.ExtraSizeUp.Value; + forcedDown += itm.Properties.ExtraSizeDown.Value; + forcedLeft += itm.Properties.ExtraSizeLeft.Value; + forcedRight += itm.Properties.ExtraSizeRight.Value; + } + else + { + sizeUp = sizeUp < itm.Properties.ExtraSizeUp ? itm.Properties.ExtraSizeUp.Value : sizeUp; + sizeDown = sizeDown < itm.Properties.ExtraSizeDown ? itm.Properties.ExtraSizeDown.Value : sizeDown; + sizeLeft = sizeLeft < itm.Properties.ExtraSizeLeft ? itm.Properties.ExtraSizeLeft.Value : sizeLeft; + sizeRight = sizeRight < itm.Properties.ExtraSizeRight ? itm.Properties.ExtraSizeRight.Value : sizeRight; + } + } + } + + toDo.RemoveAt(0); + } + } + + return [ + outX + sizeLeft + sizeRight + forcedLeft + forcedRight, + outY + sizeUp + sizeDown + forcedUp + forcedDown, + ]; } /// @@ -209,9 +334,9 @@ public class InventoryHelper( /// Horizontal size of container /// Vertical size of container /// Two-dimensional representation of container - protected List> GetBlankContainerMap(int containerH, int containerY) + protected int[][] GetBlankContainerMap(int containerH, int containerY) { - throw new NotImplementedException(); + return Enumerable.Repeat(Enumerable.Repeat(0, containerH).ToArray(), containerY).ToArray(); } /// @@ -222,9 +347,68 @@ public class InventoryHelper( /// Players inventory items /// Id of the container /// Two-dimensional representation of container - public List> GetContainerMap(double containerH, double containerV, List itemList, string containerId) + public int[][] GetContainerMap(int containerH, int containerV, List itemList, string containerId) { - throw new NotImplementedException(); + // Create blank 2d map of container + var container2D = GetBlankContainerMap(containerH, containerV); + + // Get all items in players inventory keyed by their parentId and by ItemId + var inventoryItemHash = GetInventoryItemHash(itemList); + + // Get subset of items that belong to the desired container + if (!inventoryItemHash.ByParentId.TryGetValue(containerId, out List containerItemHash)) + { + // No items in container, exit early + return container2D; + } + + // Check each item in container + foreach (var item in containerItemHash) { + var itemLocation = item?.Location as ItemLocation; + if (itemLocation is null) + { + // item has no location property + _logger.Error("Unable to find 'location' property on item with id: ${ item._id}, skipping"); + + continue; + } + + // Get x/y size of item + 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; + + // Find the ending x coord of container + var fillTo = itemLocation.X + fW; + + for (var y = 0; y < fH; y++) + { + try + { + var rowIndex = itemLocation.Y + y; + var containerRow = container2D[rowIndex.Value]; + if (containerRow is null) + { + _logger.Error("Unable to find container: { containerId} row line: { itemLocation.y + y}"); + } + + // Fill the corresponding cells in the container map to show the slot is taken + Array.Fill(containerRow, 1, itemLocation.X.Value, fillTo.Value); + } catch (Exception ex) { + _logger.Error( + _localisationService.GetText("inventory-unable_to_fill_container", new { + id = item.Id, + error = ex.Message + }) + ); + } + + } + } + + return container2D; } protected bool IsVertical(ItemLocation itemLocation) @@ -234,7 +418,25 @@ public class InventoryHelper( protected InventoryItemHash GetInventoryItemHash(List inventoryItems) { - throw new NotImplementedException(); + var inventoryItemHash = new InventoryItemHash + { + ByItemId = new(), + ByParentId = new() + }; + foreach (var item in inventoryItems) + { + inventoryItemHash.ByItemId.TryAdd(item.Id, item); + + if (item.ParentId is null) { + continue; + } + + if (!inventoryItemHash.ByParentId.ContainsKey(item.ParentId)) { + inventoryItemHash.ByParentId[item.ParentId] = []; + } + inventoryItemHash.ByParentId[item.ParentId].Add(item); + } + return inventoryItemHash; } /// diff --git a/Libraries/Core/Models/Eft/Common/Tables/Item.cs b/Libraries/Core/Models/Eft/Common/Tables/Item.cs index 605126df..25170621 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/Item.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/Item.cs @@ -29,8 +29,8 @@ public record Item public record ItemLocation { - public double? X { get; set; } - public double? Y { get; set; } + public int? X { get; set; } + public int? Y { get; set; } public object? R { get; set; } // TODO: Can be string or number public bool? IsSearched { get; set; } diff --git a/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs b/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs index 13b4461e..812940ae 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs @@ -153,19 +153,19 @@ public record Props public double? RepairSpeed { get; set; } [JsonPropertyName("ExtraSizeLeft")] - public double? ExtraSizeLeft { get; set; } + public int? ExtraSizeLeft { get; set; } [JsonPropertyName("ExtraSizeRight")] - public double? ExtraSizeRight { get; set; } + public int? ExtraSizeRight { get; set; } [JsonPropertyName("ExtraSizeUp")] - public double? ExtraSizeUp { get; set; } + public int? ExtraSizeUp { get; set; } [JsonPropertyName("FlareTypes")] public List? FlareTypes { get; set; } [JsonPropertyName("ExtraSizeDown")] - public double? ExtraSizeDown { get; set; } + public int? ExtraSizeDown { get; set; } [JsonPropertyName("ExtraSizeForceAdd")] public bool? ExtraSizeForceAdd { get; set; } @@ -474,7 +474,7 @@ public record Props public bool? Retractable { get; set; } [JsonPropertyName("SizeReduceRight")] - public double? SizeReduceRight { get; set; } + public int? SizeReduceRight { get; set; } [JsonPropertyName("CenterOfImpact")] public double? CenterOfImpact { get; set; } @@ -1512,10 +1512,10 @@ public record GridProps public List? Filters { get; set; } [JsonPropertyName("cellsH")] - public double? CellsH { get; set; } + public int? CellsH { get; set; } [JsonPropertyName("cellsV")] - public double? CellsV { get; set; } + public int? CellsV { get; set; } [JsonPropertyName("minCount")] public double? MinCount { get; set; }