460 lines
17 KiB
C#
460 lines
17 KiB
C#
using System.Collections.Concurrent;
|
|
using SPTarkov.DI.Annotations;
|
|
using SPTarkov.Server.Core.Extensions;
|
|
using SPTarkov.Server.Core.Helpers;
|
|
using SPTarkov.Server.Core.Models.Common;
|
|
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
|
using SPTarkov.Server.Core.Models.Enums;
|
|
using SPTarkov.Server.Core.Models.Utils;
|
|
|
|
namespace SPTarkov.Server.Core.Services;
|
|
|
|
/// <summary>
|
|
/// Service for keeping track of items and their exact position inside a bots container
|
|
/// </summary>
|
|
[Injectable]
|
|
public class BotInventoryContainerService(ISptLogger<BotGeneratorHelper> logger, ItemHelper itemHelper)
|
|
{
|
|
// botId/containerName
|
|
private readonly ConcurrentDictionary<MongoId, Dictionary<EquipmentSlots, ContainerDetails>> _botContainers = new();
|
|
|
|
/// <summary>
|
|
/// Add a container + details to a bots cache ready to accept loot
|
|
/// </summary>
|
|
/// <param name="botId">Unique identifier of bot</param>
|
|
/// <param name="containerName">name of container e.g. "Backpack"</param>
|
|
/// <param name="containerInventoryItem">Inventory item loot will be linked to in bots inventory</param>
|
|
public void AddEmptyContainerToBot(MongoId botId, EquipmentSlots containerName, Item containerInventoryItem)
|
|
{
|
|
// Add bot to dict if it doesn't exist
|
|
_botContainers.TryAdd(botId, new());
|
|
|
|
// Get the bots' currently cached containers
|
|
var containers = GetOrCreateBotContainerDictionary(botId);
|
|
|
|
// Add container to bot
|
|
if (!containers.ContainsKey(containerName))
|
|
{
|
|
var containerDbItem = itemHelper.GetItem(containerInventoryItem.Template);
|
|
containers.Add(containerName, new ContainerDetails(containerDbItem.Value, containerInventoryItem));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to add an item + children to a container
|
|
/// </summary>
|
|
/// <param name="botId">Bots unique id</param>
|
|
/// <param name="containerName">Name of container to add to e.g. "Backpack"</param>
|
|
/// <param name="itemAndChildren">Item and its children to add to container</param>
|
|
/// <param name="botInventory">Inventory to add Item+children to</param>
|
|
/// <param name="itemWidth">Width of item with its children</param>
|
|
/// <param name="itemHeight">Height of item with its children</param>
|
|
/// <returns>ItemAddedResult</returns>
|
|
public ItemAddedResult TryAddItemToBotContainer(
|
|
MongoId botId,
|
|
EquipmentSlots containerName,
|
|
List<Item> itemAndChildren,
|
|
BotBaseInventory botInventory,
|
|
int itemWidth,
|
|
int itemHeight
|
|
)
|
|
{
|
|
if (itemAndChildren.Count == 0)
|
|
{
|
|
return ItemAddedResult.INCOMPATIBLE_ITEM;
|
|
}
|
|
|
|
var addResult = ItemAddedResult.UNKNOWN;
|
|
|
|
// Find bot and the container we will attempt to add into
|
|
if (
|
|
!GetOrCreateBotContainerDictionary(botId).TryGetValue(containerName, out var containerDetails)
|
|
|| containerDetails.ContainerGridDetails.Count == 0
|
|
)
|
|
{
|
|
// No grids, cannot add item
|
|
return ItemAddedResult.NO_CONTAINERS;
|
|
}
|
|
|
|
if (!ItemAllowedInContainer(containerDetails, itemAndChildren))
|
|
// Multiple containers, maybe next one allows item, only break out of loop for the containers grids
|
|
{
|
|
return ItemAddedResult.INCOMPATIBLE_ITEM;
|
|
}
|
|
|
|
// Try to fit item into one of the containers' grids
|
|
var rootItem = itemAndChildren.First();
|
|
var gridIndex = -1; // start at -1 as we increment index first thing each grid we iterate over
|
|
foreach (var gridDb in containerDetails.ContainerDbItem.Properties.Grids)
|
|
{
|
|
gridIndex++;
|
|
|
|
var gridDetails = containerDetails.ContainerGridDetails[gridIndex];
|
|
if (gridDetails.GridFull)
|
|
{
|
|
// Skip to next grid
|
|
continue;
|
|
}
|
|
|
|
if (IsItemBiggerThanGrid(gridDetails.GridMap, itemWidth, itemHeight))
|
|
{
|
|
// Skip to next grid
|
|
continue;
|
|
}
|
|
|
|
// Look for a slot in the grid to place item
|
|
var findSlotResult = gridDetails.GridMap.FindSlotForItem(itemWidth, itemHeight);
|
|
if (findSlotResult.Success.GetValueOrDefault(false))
|
|
{
|
|
// It Fits!
|
|
|
|
// Set items parent to Id of container
|
|
rootItem.ParentId = containerDetails.ContainerInventoryItem.Id;
|
|
rootItem.SlotId = gridDb.Name; // Can be name of container e.g. "Backpack" OR "2/3/4/5" depending on which grid of a container item is added to
|
|
rootItem.Location = new ItemLocation
|
|
{
|
|
X = findSlotResult.X,
|
|
Y = findSlotResult.Y,
|
|
R = findSlotResult.Rotation ?? false ? ItemRotation.Vertical : ItemRotation.Horizontal,
|
|
};
|
|
|
|
// Flag result as success to report to caller
|
|
addResult = ItemAddedResult.SUCCESS;
|
|
|
|
// Update grid with slots taken up by above item
|
|
FillGridRegion(
|
|
gridDetails.GridMap,
|
|
findSlotResult.X.Value,
|
|
findSlotResult.Y.Value,
|
|
findSlotResult.Rotation.GetValueOrDefault() ? itemHeight : itemWidth,
|
|
findSlotResult.Rotation.GetValueOrDefault() ? itemWidth : itemHeight
|
|
);
|
|
|
|
// Add item into bots inventory
|
|
botInventory.Items.AddRange(itemAndChildren);
|
|
|
|
// Exit loop, we've found a slot for item
|
|
break;
|
|
}
|
|
|
|
// Didn't fit, flag as no space, hopefully next grid has space
|
|
addResult = ItemAddedResult.NO_SPACE;
|
|
|
|
FlagGridIfFull(gridDetails, itemWidth, itemHeight);
|
|
}
|
|
|
|
return addResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to add an item + children to a container at a specific x/y grid position
|
|
/// </summary>
|
|
/// <param name="botId">Bots unique id</param>
|
|
/// <param name="containerName">Name of container to add to e.g. "Backpack"</param>
|
|
/// <param name="itemAndChildren">Item and its children to add to container</param>
|
|
/// <param name="botInventory">Inventory to add Item+children to</param>
|
|
/// <param name="itemWidth">Width of item with its children</param>
|
|
/// <param name="itemHeight">Height of item with its children</param>
|
|
/// <param name="fixedLocation">Details for where to place item in container grid</param>
|
|
/// <returns>ItemAddedResult</returns>
|
|
public ItemAddedResult AddItemToBotContainerFixedPosition(
|
|
MongoId botId,
|
|
EquipmentSlots containerName,
|
|
List<Item> itemAndChildren,
|
|
BotBaseInventory botInventory,
|
|
int itemWidth,
|
|
int itemHeight,
|
|
ItemLocation fixedLocation
|
|
)
|
|
{
|
|
if (itemAndChildren.Count == 0)
|
|
{
|
|
return ItemAddedResult.INCOMPATIBLE_ITEM;
|
|
}
|
|
|
|
// Default result
|
|
var addResult = ItemAddedResult.UNKNOWN;
|
|
|
|
// Find bot and the container we are attempting to store item in
|
|
var botContainers = GetOrCreateBotContainerDictionary(botId);
|
|
if (!botContainers.TryGetValue(containerName, out var containerDetails) || containerDetails.ContainerGridDetails.Count == 0)
|
|
{
|
|
// No grids, cannot add item
|
|
return ItemAddedResult.NO_CONTAINERS;
|
|
}
|
|
|
|
if (!ItemAllowedInContainer(containerDetails, itemAndChildren))
|
|
// Multiple containers, maybe next one allows item, only break out of loop for the containers grids
|
|
{
|
|
return ItemAddedResult.INCOMPATIBLE_ITEM;
|
|
}
|
|
|
|
// Try to fit item into one of the containers' grids
|
|
var rootItem = itemAndChildren.FirstOrDefault();
|
|
if (rootItem is null)
|
|
{
|
|
return ItemAddedResult.UNKNOWN;
|
|
}
|
|
foreach (var gridDetails in containerDetails.ContainerGridDetails)
|
|
{
|
|
if (gridDetails.GridFull)
|
|
{
|
|
// No space, skip early
|
|
continue;
|
|
}
|
|
|
|
if (IsItemBiggerThanGrid(gridDetails.GridMap, itemWidth, itemHeight))
|
|
{
|
|
// Skip early
|
|
continue;
|
|
}
|
|
|
|
// Look for a slot in the grid to place item
|
|
var result = gridDetails.GridMap.TryFillContainerMapWithItem(
|
|
fixedLocation.X.Value,
|
|
fixedLocation.Y.Value,
|
|
itemWidth,
|
|
itemHeight,
|
|
fixedLocation.R == ItemRotation.Vertical,
|
|
out _
|
|
);
|
|
if (result)
|
|
{
|
|
// It Fits!
|
|
|
|
// Parent root item to container
|
|
rootItem.ParentId = containerDetails.ContainerInventoryItem.Id;
|
|
rootItem.SlotId = containerName.ToString();
|
|
rootItem.Location = new ItemLocation
|
|
{
|
|
X = fixedLocation.X.Value,
|
|
Y = fixedLocation.Y.Value,
|
|
R = fixedLocation.R,
|
|
};
|
|
|
|
// Flag result as success to report to caller
|
|
addResult = ItemAddedResult.SUCCESS;
|
|
|
|
// Update internal grid with slots taken up by above item
|
|
FillGridRegion(
|
|
gridDetails.GridMap,
|
|
fixedLocation.X.Value,
|
|
fixedLocation.Y.Value,
|
|
fixedLocation.R == ItemRotation.Vertical ? itemHeight : itemWidth,
|
|
fixedLocation.R == ItemRotation.Vertical ? itemWidth : itemHeight
|
|
);
|
|
|
|
// Item fits + Added to layout grid, add item and children
|
|
//containerDetails.ItemsAndChildrenInContainer.AddRange(itemAndChildren);
|
|
|
|
// Add item into bots inventory
|
|
botInventory.Items.AddRange(itemAndChildren);
|
|
|
|
// Exit loop, we've found a position for item and can stop
|
|
break;
|
|
}
|
|
|
|
// Didn't fit, flag as no space, hopefully next grid has space
|
|
addResult = ItemAddedResult.NO_SPACE;
|
|
|
|
FlagGridIfFull(gridDetails, itemWidth, itemHeight);
|
|
}
|
|
|
|
return addResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper - Get the bot-specific container details, create if data doesn't exist
|
|
/// </summary>
|
|
/// <param name="botId">Bot unique identifier</param>
|
|
/// <returns>Dictionary</returns>
|
|
protected Dictionary<EquipmentSlots, ContainerDetails> GetOrCreateBotContainerDictionary(MongoId botId)
|
|
{
|
|
if (!_botContainers.TryGetValue(botId, out var botContainers))
|
|
{
|
|
// Create blank dict ready for containers to be added
|
|
botContainers = new();
|
|
}
|
|
|
|
return botContainers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fill region of a 2D array
|
|
/// </summary>
|
|
/// <param name="grid">The 2D grid array to modify</param>
|
|
/// <param name="x">The starting column index (left)</param>
|
|
/// <param name="y">The starting row index (top)</param>
|
|
/// <param name="itemWidth">The number of cells to update horizontally</param>
|
|
/// <param name="itemHeight">The number of cells to update vertically</param>
|
|
protected void FillGridRegion(int[,] grid, int x, int y, int itemWidth, int itemHeight)
|
|
{
|
|
// Outer loop iterates through rows (from starting y position)
|
|
for (var row = y; row < y + itemHeight; row++)
|
|
{
|
|
// Inner loop iterates through columns (from starting x position)
|
|
for (var col = x; col < x + itemWidth; col++)
|
|
{
|
|
grid[row, col] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flag a container grid as full if a 1x1 item cannot fit or there are no spaces left in the 2d array
|
|
/// </summary>
|
|
/// <param name="gridDetails"></param>
|
|
/// <param name="itemWidth"></param>
|
|
/// <param name="itemHeight"></param>
|
|
protected static void FlagGridIfFull(ContainerMapDetails gridDetails, int itemWidth, int itemHeight)
|
|
{
|
|
// If item is 1x1 and it failed to fit, grid must be full
|
|
if (itemHeight == 1 && itemWidth == 1)
|
|
{
|
|
gridDetails.GridFull = true; // Flag now so later items can skip grid
|
|
|
|
return;
|
|
}
|
|
|
|
// Check if grid is full and flag
|
|
if (gridDetails.GridMap.ContainerIsFull())
|
|
{
|
|
gridDetails.GridFull = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the items subtype allowed inside this container / is it excluded from this container
|
|
/// </summary>
|
|
/// <param name="containerDetails">Details on the container we want to add item into</param>
|
|
/// <param name="itemAndChildren">Item+children we want to add into container</param>
|
|
/// <returns>true = item is allowed</returns>
|
|
private bool ItemAllowedInContainer(ContainerDetails containerDetails, List<Item>? itemAndChildren)
|
|
{
|
|
// Assume all grids have same limitations
|
|
var firstSlotGrid = containerDetails.ContainerDbItem.Properties.Grids.FirstOrDefault();
|
|
var propFilters = firstSlotGrid?.Props?.Filters;
|
|
if (propFilters is null || !propFilters.Any())
|
|
// No filters, item is fine to add
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check if item base type is excluded
|
|
var itemDetails = itemHelper.GetItem(itemAndChildren.FirstOrDefault().Template).Value;
|
|
|
|
// if item to add is found in exclude filter, not allowed
|
|
var excludedFilter = propFilters.FirstOrDefault()?.ExcludedFilter ?? [];
|
|
if (excludedFilter.Contains(itemDetails?.Parent ?? string.Empty))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If Filter array only contains 1 filter and it is for basetype 'item', allow it
|
|
var filter = propFilters.FirstOrDefault()?.Filter ?? [];
|
|
if (filter.Count == 1 && filter.Contains(BaseClasses.ITEM))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// If allowed filter has something in it + filter doesn't have basetype 'item', not allowed
|
|
if (filter.Count > 0 && !filter.Contains(itemDetails?.Parent ?? string.Empty))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the items edge length bigger than the grid trying to hold it
|
|
/// </summary>
|
|
/// <param name="grid">Container grid</param>
|
|
/// <param name="itemWidth">Width of item</param>
|
|
/// <param name="itemHeight">Height of item</param>
|
|
/// <returns>true = item bigger than grid</returns>
|
|
private bool IsItemBiggerThanGrid(int[,] grid, int itemWidth, int itemHeight)
|
|
{
|
|
var gridHeight = grid.GetLength(0);
|
|
var gridWidth = grid.GetLength(1);
|
|
|
|
// Check if it can fit in either orientation
|
|
var fitsNormally = itemWidth <= gridWidth && itemHeight <= gridHeight;
|
|
var fitsRotated = itemHeight <= gridWidth && itemWidth <= gridHeight;
|
|
|
|
// Fails both checks
|
|
return !fitsNormally && !fitsRotated;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a bots container details from cache by its id
|
|
/// </summary>
|
|
/// <param name="botId">Identifier of bot to get details of</param>
|
|
/// <returns>Dictionary of containers and their details</returns>
|
|
public Dictionary<EquipmentSlots, ContainerDetails>? GetBotContainer(MongoId botId)
|
|
{
|
|
return GetOrCreateBotContainerDictionary(botId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear the cache of all bot containers
|
|
/// </summary>
|
|
public void ClearCache()
|
|
{
|
|
_botContainers.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear specific bot container details from cache
|
|
/// </summary>
|
|
/// <param name="botId">Bot identifier</param>
|
|
public void ClearCache(MongoId botId)
|
|
{
|
|
_botContainers.Remove(botId, out _);
|
|
}
|
|
|
|
public record ContainerDetails
|
|
{
|
|
public ContainerDetails(TemplateItem containerDbItem, Item containerInventoryItem)
|
|
{
|
|
ContainerDbItem = containerDbItem;
|
|
ContainerInventoryItem = containerInventoryItem;
|
|
// Add all grids for this container
|
|
foreach (var grid in containerDbItem.Properties.Grids)
|
|
{
|
|
ContainerGridDetails.Add(
|
|
new ContainerMapDetails
|
|
{
|
|
GridMap = new int[grid.Props.CellsV.GetValueOrDefault(), grid.Props.CellsH.GetValueOrDefault()],
|
|
GridFull = false,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grid layout and flag if grid is full
|
|
/// </summary>
|
|
public List<ContainerMapDetails> ContainerGridDetails { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Db record for the container holding items
|
|
/// </summary>
|
|
public TemplateItem ContainerDbItem { get; set; }
|
|
|
|
/// <summary>
|
|
/// Inventory item representing the container
|
|
/// </summary>
|
|
public Item ContainerInventoryItem { get; set; }
|
|
|
|
// TODO: implement this + add checks inside AddItemToBotContainer for perf improvement
|
|
public bool ContainerFull { get; set; } = false;
|
|
}
|
|
|
|
public record ContainerMapDetails
|
|
{
|
|
public int[,] GridMap { get; init; }
|
|
public bool GridFull { get; set; }
|
|
}
|
|
}
|