diff --git a/Core/Helpers/BotGeneratorHelper.cs b/Core/Helpers/BotGeneratorHelper.cs index d316beff..cd3b4a26 100644 --- a/Core/Helpers/BotGeneratorHelper.cs +++ b/Core/Helpers/BotGeneratorHelper.cs @@ -325,7 +325,158 @@ public class BotGeneratorHelper /// false if no incompatibilities, also has incompatibility reason public ChooseRandomCompatibleModResult IsItemIncompatibleWithCurrentItems(List itemsEquipped, string tplToCheck, string equipmentSlot) { - throw new NotImplementedException(); + // Skip slots that have no incompatibilities + List slotsToCheck = ["Scabbard", "Backpack", "SecureContainer", "Holster", "ArmBand"]; + if (slotsToCheck.Contains(equipmentSlot)) + { + return new() { Incompatible = false, Found = false, Reason = "" }; + } + + // TODO: Can probably be optimized to cache itemTemplates as items are added to inventory + var equippedItemsDb = itemsEquipped.Select((equippedItem) => _itemHelper.GetItem(equippedItem.Template).Value); + var itemToEquipDb = _itemHelper.GetItem(tplToCheck); + var itemToEquip = itemToEquipDb.Value; + + if (!itemToEquipDb.Key) + { + _logger.Warning( + _localisationService.GetText( + "bot-invalid_item_compatibility_check", + new + { + itemTpl = tplToCheck, + slot = equipmentSlot, + } + ) + ); + + return new() { Incompatible = true, Found = false, Reason = $"item: {tplToCheck} does not exist in the database" }; + } + + if (itemToEquip.Properties is null) + { + _logger.Warning( + _localisationService.GetText( + "bot-compatibility_check_missing_props", + new + { + id = itemToEquip.Id, + name = itemToEquip.Name, + slot = equipmentSlot, + } + ) + ); + + return new() { Incompatible = true, Found = false, Reason = $"item: {tplToCheck} does not have a _props field" }; + } + + // Does an equipped item have a property that blocks the desired item - check for prop "BlocksX" .e.g BlocksEarpiece / BlocksFaceCover + var blockingItem = equippedItemsDb.FirstOrDefault( + (item) => item.Properties.GetType().GetProperties().FirstOrDefault(x => x.Name.ToLower() == $"blocks{equipmentSlot}").GetValue(item) is not null + ); + if (blockingItem is not null) + { + // this.logger.warning(`1 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._name} - ${equipmentSlot}`); + + return new() + { + Incompatible = true, Found = false, + Reason = $"{tplToCheck} {itemToEquip.Name} in slot: {equipmentSlot} blocked by: {blockingItem.Id} {blockingItem.Name}", SlotBlocked = true + }; + } + + // Check if any of the current inventory templates have the incoming item defined as incompatible + blockingItem = equippedItemsDb.FirstOrDefault((x) => x.Properties.ConflictingItems?.Contains(tplToCheck) ?? false); + if (blockingItem is not null) + { + // this.logger.warning(`2 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._props.Name} - ${equipmentSlot}`); + return new() + { + Incompatible = true, + Found = false, + Reason = $"{tplToCheck} {itemToEquip.Name} in slot: {equipmentSlot} blocked by: {blockingItem.Id} {blockingItem.Name}", + SlotBlocked = true + }; + } + + // Does item being checked get blocked/block existing item + if (itemToEquip.Properties.BlocksHeadwear ?? false) + { + var existingHeadwear = itemsEquipped.FirstOrDefault((x) => x.SlotId == "Headwear"); + if (existingHeadwear is not null) + { + return new() + { + Incompatible = true, + Found = false, + Reason = $"{tplToCheck} {itemToEquip.Name} is blocked by: {existingHeadwear.Template} in slot: {existingHeadwear.SlotId}", + SlotBlocked = true + }; + } + } + + // Does item being checked get blocked/block existing item + if (itemToEquip.Properties.BlocksFaceCover ?? false) + { + var existingFaceCover = itemsEquipped.FirstOrDefault((item) => item.SlotId == "FaceCover"); + if (existingFaceCover is not null) + { + return new() + { + Incompatible = true, + Found = false, + Reason = $"{tplToCheck} {itemToEquip.Name} is blocked by: {existingFaceCover.Template} in slot: {existingFaceCover.SlotId}", + SlotBlocked = true, + }; + } + } + + // Does item being checked get blocked/block existing item + if (itemToEquip.Properties.BlocksEarpiece ?? false) + { + var existingEarpiece = itemsEquipped.FirstOrDefault((item) => item.SlotId == "Earpiece"); + if (existingEarpiece is not null) + { + return new() + { + Incompatible = true, + Found = false, + Reason = $"{tplToCheck} {itemToEquip.Name} is blocked by: {existingEarpiece.Template} in slot: {existingEarpiece.SlotId}", + SlotBlocked = true, + }; + } + } + + // Does item being checked get blocked/block existing item + if (itemToEquip.Properties.BlocksArmorVest is not null) + { + var existingArmorVest = itemsEquipped.FirstOrDefault((item) => item.SlotId == "ArmorVest"); + if (existingArmorVest is not null) + { + return new() + { + Incompatible = true, + Found = false, + Reason = $"{tplToCheck} {itemToEquip.Name} is blocked by: {existingArmorVest.Template} in slot: {existingArmorVest.SlotId}", + SlotBlocked = true, + }; + } + } + + // Check if the incoming item has any inventory items defined as incompatible + var blockingInventoryItem = itemsEquipped.FirstOrDefault((x) => itemToEquip.Properties.ConflictingItems?.Contains(x.Template) ?? false); + if (blockingInventoryItem is not null) + { + // this.logger.warning(`3 incompatibility found between - ${itemToEquip[1]._name} and ${blockingInventoryItem._tpl} - ${equipmentSlot}`) + return new() + { + Incompatible = true, + Found = false, + Reason = $"{tplToCheck} blocks existing item {blockingInventoryItem.Template} in slot {blockingInventoryItem.SlotId}", + }; + } + + return new() { Incompatible = false, Reason = "" }; } /// @@ -360,7 +511,143 @@ public class BotGeneratorHelper BotBaseInventory inventory, HashSet containersIdFull = null) { - throw new NotImplementedException(); + /** Track how many containers are unable to be found */ + var missingContainerCount = 0; + foreach (var equipmentSlotId in equipmentSlots) + { + if (containersIdFull?.Contains(equipmentSlotId) ?? false) + { + continue; + } + + // Get container to put item into + var container = inventory.Items.FirstOrDefault((item) => item.SlotId == equipmentSlotId); + if (container is null) + { + missingContainerCount++; + if (missingContainerCount == equipmentSlots.Count) + { + // Bot doesnt have any containers we want to add item to + _logger.Debug( + $"Unable to add item: {itemWithChildren[0].Template} to bot as it lacks the following containers: {string.Join(",", equipmentSlots)}" + ); + + return ItemAddedResult.NO_CONTAINERS; + } + + // No container of desired type found, skip to next container type + continue; + } + + // Get container details from db + var containerTemplate = _itemHelper.GetItem(container.Template); + if (!containerTemplate.Key) + { + _logger.Warning(_localisationService.GetText("bot-missing_container_with_tpl", container.Template)); + + // Bad item, skip + continue; + } + + if (!containerTemplate.Value.Properties.Grids?.Any() ?? false) + { + // Container has no slots to hold items + continue; + } + + // Get x/y grid size of item + var itemSize = _inventoryHelper.GetItemSize(rootItemTplId, rootItemId, itemWithChildren); + + // Iterate over each grid in the container and look for a big enough space for the item to be placed in + var currentGridCount = 1; + var totalSlotGridCount = containerTemplate.Value.Properties.Grids.Count; + foreach (var slotGrid in containerTemplate.Value.Properties.Grids) + { + // Grid is empty, skip or item size is bigger than grid + if (slotGrid.Props.CellsH == 0 || slotGrid.Props.CellsV == 0 || itemSize[0] * itemSize[1] > slotGrid.Props.CellsV * slotGrid.Props.CellsH) + continue; + + // Can't put item type in grid, skip all grids as we're assuming they have the same rules + if (!ItemAllowedInContainer(slotGrid, rootItemTplId)) + { + // Multiple containers, maybe next one allows item, only break out of loop for this containers grids + break; + } + + // Get all root items in found container + var existingContainerItems = inventory.Items.Where( + (item) => item.ParentId == container.Id && item.SlotId == slotGrid.Name + ) + .ToList(); + + // Get root items in container we can iterate over to find out what space is free + var containerItemsToCheck = existingContainerItems.Where((x) => x.SlotId == slotGrid.Name); + foreach (var item in containerItemsToCheck) + { + // Look for children on items, insert into array if found + // (used later when figuring out how much space weapon takes up) + var itemWithChildrens = _itemHelper.FindAndReturnChildrenAsItems(inventory.Items, item.Id); + if (itemWithChildrens.Count > 1) + { + existingContainerItems.Remove(item); + existingContainerItems.AddRange(itemWithChildrens); + } + } + + // Get rid of items free/used spots in current grid + var slotGridMap = _inventoryHelper.GetContainerMap( + slotGrid.Props.CellsH.GetValueOrDefault(), + slotGrid.Props.CellsV.GetValueOrDefault(), + existingContainerItems, + container.Id + ); + + // Try to fit item into grid + var findSlotResult = _containerHelper.FindSlotForItem(slotGridMap, itemSize[0], itemSize[1]); + + // Open slot found, add item to inventory + if (findSlotResult.Success ?? false) + { + var parentItem = itemWithChildren.FirstOrDefault((i) => i.Id == rootItemId); + + // Set items parent to container id + parentItem.ParentId = container.Id; + parentItem.SlotId = slotGrid.Name; + parentItem.Location = new ItemLocation() + { + X = findSlotResult.X, + Y = findSlotResult.Y, + R = (findSlotResult.Rotation ?? false) ? 1 : 0, + } + ; + + inventory.Items.AddRange(itemWithChildren); + + return ItemAddedResult.SUCCESS; + } + + // If we've checked all grids in container and reached this point, there's no space for item + if (currentGridCount >= totalSlotGridCount) + { + break; + } + + currentGridCount++; + // No space in this grid, move to next container grid and try again + } + + // if we got to this point, the item couldnt be placed on the container + if (containersIdFull is not null) + { + // if the item was a one by one, we know it must be full. Or if the maps cant find a slot for a one by one + if (itemSize[0] == 1 && itemSize[1] == 1) + { + containersIdFull.Add(equipmentSlotId); + } + } + } + + return ItemAddedResult.NO_SPACE; } /// @@ -371,6 +658,40 @@ public class BotGeneratorHelper /// True if allowed protected bool ItemAllowedInContainer(Grid slotGrid, string itemTpl) { - throw new NotImplementedException(); + var propFilters = slotGrid.Props.Filters; + var excludedFilter = propFilters[0]?.ExcludedFilter ?? []; + var filter = propFilters[0]?.Filter ?? []; + + if (propFilters.Count == 0) + { + // no filters, item is fine to add + return true; + } + + // Check if item base type is excluded + if (excludedFilter is not null || filter is not null) + { + var itemDetails = _itemHelper.GetItem(itemTpl).Value; + + // if item to add is found in exclude filter, not allowed + if (excludedFilter?.Contains(itemDetails.Parent) ?? false) + { + return false; + } + + // If Filter array only contains 1 filter and its for basetype 'item', allow it + if (filter?.Count == 1 && filter.Contains(BaseClasses.ITEM)) + { + return true; + } + + // If allowed filter has something in it + filter doesnt have basetype 'item', not allowed + if (filter?.Count > 0 && !filter.Contains(itemDetails.Parent)) + { + return false; + } + } + + return true; } } diff --git a/Core/Helpers/InventoryHelper.cs b/Core/Helpers/InventoryHelper.cs index f728f2cb..6724b9f1 100644 --- a/Core/Helpers/InventoryHelper.cs +++ b/Core/Helpers/InventoryHelper.cs @@ -215,7 +215,7 @@ public class InventoryHelper /// Players inventory items /// Id of the container /// Two-dimensional representation of container - public List> GetContainerMap(int containerH, int containerV, List itemList, string containerId) + public List> GetContainerMap(double containerH, double containerV, List itemList, string containerId) { throw new NotImplementedException(); } diff --git a/Core/Models/Eft/Common/Tables/Item.cs b/Core/Models/Eft/Common/Tables/Item.cs index 4eb0ea60..6688a0d5 100644 --- a/Core/Models/Eft/Common/Tables/Item.cs +++ b/Core/Models/Eft/Common/Tables/Item.cs @@ -28,8 +28,8 @@ public class Item public class ItemLocation { - public float? X { get; set; } - public float? Y { get; set; } + public double? X { get; set; } + public double? Y { get; set; } public object? R { get; set; } // TODO: Can be string or number public bool? IsSearched { get; set; }