Rewrote container item space system to use 2 dimensional arrays (#442)

* Rewrote container item space system to use 2 dimensional arrays

* Moved container helper code into extension methods

* Reduced amount of parameters passed into `RowIsFull`

* Skip root trader items

* Remove debug

---------

Co-authored-by: Chomp <dev@dev.sp-tarkov.com>
This commit is contained in:
Chomp
2025-07-03 16:36:13 +01:00
committed by GitHub
parent a9918f9e1c
commit 4e2d4dc708
12 changed files with 416 additions and 393 deletions
@@ -0,0 +1,239 @@
using SPTarkov.Server.Core.Models.Spt.Inventory;
namespace SPTarkov.Server.Core.Extensions
{
public static class ContainerExtensions
{
/// <summary>
/// Finds a slot for an item in a given 2D container map
/// </summary>
/// <param name="container2D">List of container with positions filled/free</param>
/// <param name="itemX">Width of item</param>
/// <param name="itemY">Height of item</param>
/// <returns>Location to place item in container</returns>
public static FindSlotResult FindSlotForItem(
this int[,] container2D,
int? itemX,
int? itemY
)
{
// Assume not rotated
var rotation = false;
// Find the min volume the item will take up
var minVolume = (itemX < itemY ? itemX : itemY) - 1;
var containerY = container2D.GetLength(0); // rows
var containerX = container2D.GetLength(1); // columns
var limitY = containerY - minVolume;
var limitX = containerX - minVolume;
// Every x+y slot taken up in container, exit
if (ContainerIsFull(container2D))
{
return new FindSlotResult(false);
}
// Down = y, iterate over rows
for (var y = 0; y < limitY; y++)
{
if (RowIsFull(container2D, y))
{
continue;
}
// Left to right across columns, look for free position
for (var x = 0; x < limitX; x++)
{
// Does item fit
if (
CanItemBePlacedInContainerAtPosition(
container2D,
x,
y,
itemX.Value,
itemY.Value
)
)
{
// Success, found a spot it fits
return new FindSlotResult(true, x, y, rotation);
}
if (!ItemBiggerThan1X1(itemX.Value, itemY.Value))
{
// Doesn't fit AND rotating won't help
continue;
}
// Rotate item by swapping x and y item values
if (
CanItemBePlacedInContainerAtPosition(
container2D,
x,
y,
itemY.Value, // Swapped
itemX.Value // Swapped
)
)
{
// Found a position for the item when rotated
rotation = true;
return new FindSlotResult(true, x, y, rotation);
}
}
}
// Tried all possible positions, nothing big enough for item
return new FindSlotResult(false);
}
/// <summary>
/// Find a free slot for an item to be placed at
/// </summary>
/// <param name="container2D">Container to place item in</param>
/// <param name="x">Container x size</param>
/// <param name="y">Container y size</param>
/// <param name="itemXWidth">Items width</param>
/// <param name="itemYHeight">Items height</param>
/// <param name="isRotated">is item rotated</param>
public static void FillContainerMapWithItem(
this int[,] container2D,
int x,
int y,
int? itemXWidth,
int? itemYHeight,
bool isRotated
)
{
// Swap height/width if item needs to be rotated to fit
var itemWidth = isRotated ? itemYHeight : itemXWidth;
var itemHeight = isRotated ? itemXWidth : itemYHeight;
for (var tmpY = y; tmpY < y + itemHeight; tmpY++)
{
for (var tmpX = x; tmpX < x + itemWidth; tmpX++)
{
if (container2D[tmpY, tmpX] == 0)
{
// Flag slot as used
container2D[tmpY, tmpX] = 1;
}
else
{
throw new Exception(
$"Slot at({x}, {y}) is already filled. Cannot fit a {itemXWidth} by {itemYHeight} item"
);
}
}
}
}
/// <summary>
/// Is the requested row full
/// </summary>
/// <param name="container2D">Container to check</param>
/// <param name="rowIndex">Index of row to check</param>
/// <returns>True = full</returns>
private static bool RowIsFull(int[,] container2D, int rowIndex)
{
var rowFull = true;
var containerColumnCount = container2D.GetLength(0); // rows
for (var col = 0; col < containerColumnCount; col++)
{
if (container2D[rowIndex, col] == 0)
{
rowFull = false;
break;
}
}
return rowFull;
}
/// <summary>
/// Is every slot in container full
/// </summary>
/// <param name="container2D">Container to check</param>
/// <returns>True = full</returns>
private static bool ContainerIsFull(int[,] container2D)
{
var containerY = container2D.GetLength(0); // rows
var containerX = container2D.GetLength(1); // columns
var containerFull = true;
for (var y = 0; y < containerY; y++)
{
for (var x = 0; x < containerX; x++)
{
if (container2D[y, x] == 0)
{
containerFull = false;
break;
}
}
if (!containerFull)
{
break;
}
}
return containerFull;
}
/// <summary>
/// Is the item size values passed in bigger than 1x1
/// </summary>
/// <param name="itemWidth">Width of item</param>
/// <param name="itemHeight">Height of item</param>
/// <returns>True = bigger than 1x1</returns>
private static bool ItemBiggerThan1X1(int itemWidth, int itemHeight)
{
return itemWidth + itemHeight > 2;
}
/// <summary>
/// Can an item of specified size be placed inside a 2d container at a specific position
/// </summary>
/// <param name="container">Container to find space in</param>
/// <param name="startXPos">Starting x position for item</param>
/// <param name="startYPos">Starting y position for item</param>
/// <param name="itemXWidth">Items width</param>
/// <param name="itemYHeight">Items height</param>
/// <returns>True - slot found</returns>
private static bool CanItemBePlacedInContainerAtPosition(
int[,] container,
int startXPos,
int startYPos,
int itemXWidth,
int itemYHeight
)
{
var containerHeight = container.GetLength(0); // Rows
var containerWidth = container.GetLength(1); // Columns
// Check item isn't bigger than container when at position
if (
startXPos + itemXWidth > containerWidth
|| startYPos + itemYHeight > containerHeight
)
{
// Item is bigger than container, will never fit
return false;
}
// Check each slot, is any filled
for (var checkY = startYPos; checkY < startYPos + itemYHeight; checkY++)
{
for (var checkX = startXPos; checkX < startXPos + itemXWidth; checkX++)
{
if (container[checkY, checkX] == 1)
{
// Occupied by something
return false;
}
}
}
return true; // Slot is free
}
}
}
@@ -1,4 +1,6 @@
using SPTarkov.Server.Core.Models.Common;
using System.Text.Json;
using SPTarkov.Common.Extensions;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
@@ -342,5 +344,42 @@ namespace SPTarkov.Server.Core.Extensions
ExtensionData = item.ExtensionData,
};
}
public static ItemLocation? GetParsedLocation(this Item item)
{
if (item.Location is null)
{
return null;
}
if (item.Location is JsonElement element)
{
// TODO: when is this true
return element.ToObject<ItemLocation>();
}
return (ItemLocation)item.Location;
}
/// <summary>
/// Get a list of the item IDs (NOT tpls) inside a secure container
/// </summary>
/// <param name="items">Inventory items to look for secure container in</param>
/// <returns>List of ids</returns>
public static List<string> GetSecureContainerItems(this List<Item> items)
{
var secureContainer = items.First(x => x.SlotId == "SecuredContainer");
// No container found, drop out
if (secureContainer is null)
{
return [];
}
var itemsInSecureContainer = items.FindAndReturnChildrenByItems(secureContainer.Id);
// Return all items returned and exclude the secure container item itself
return itemsInSecureContainer.Where(x => x != secureContainer.Id).ToList();
}
}
}
@@ -21,10 +21,8 @@ namespace SPTarkov.Server.Core.Generators;
public class LocationLootGenerator(
ISptLogger<LocationLootGenerator> _logger,
RandomUtil _randomUtil,
HashUtil _hashUtil,
ItemHelper _itemHelper,
DatabaseService _databaseService,
ContainerHelper _containerHelper,
PresetHelper _presetHelper,
ServerLocalisationService _serverLocalisationService,
SeasonalEventService _seasonalEventService,
@@ -600,8 +598,7 @@ public class LocationLootGenerator(
: chosenItemWithChildren.Items;
// look for open slot to put chosen item into
var result = _containerHelper.FindSlotForItem(
containerMap,
var result = containerMap.FindSlotForItem(
chosenItemWithChildren.Width,
chosenItemWithChildren.Height
);
@@ -620,8 +617,7 @@ public class LocationLootGenerator(
}
// Find somewhere for item inside container
_containerHelper.FillContainerMapWithItem(
containerMap,
containerMap.FillContainerMapWithItem(
result.X.Value,
result.Y.Value,
chosenItemWithChildren.Width,
@@ -21,7 +21,6 @@ public class BotGeneratorHelper(
DurabilityLimitsHelper _durabilityLimitsHelper,
ItemHelper _itemHelper,
InventoryHelper _inventoryHelper,
ContainerHelper _containerHelper,
ProfileActivityService _profileActivityService,
ServerLocalisationService _serverLocalisationService,
ConfigServer _configServer
@@ -665,7 +664,7 @@ public class BotGeneratorHelper(
}
// Get x/y grid size of item
var itemSize = _inventoryHelper.GetItemSize(
var (width, height) = _inventoryHelper.GetItemSize(
rootItemTplId,
rootItemId,
itemWithChildren
@@ -680,7 +679,7 @@ public class BotGeneratorHelper(
if (
slotGrid.Props?.CellsH == 0
|| slotGrid.Props?.CellsV == 0
|| itemSize[0] * itemSize[1] > slotGrid.Props?.CellsV * slotGrid.Props?.CellsH
|| width * height > slotGrid.Props?.CellsV * slotGrid.Props?.CellsH
)
{
continue;
@@ -718,11 +717,7 @@ public class BotGeneratorHelper(
);
// Try to fit item into grid
var findSlotResult = _containerHelper.FindSlotForItem(
slotGridMap,
itemSize[0],
itemSize[1]
);
var findSlotResult = slotGridMap.FindSlotForItem(width, height);
// Free slot found, add item
if (findSlotResult.Success ?? false)
@@ -768,7 +763,7 @@ public class BotGeneratorHelper(
}
// 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)
if (width == 1 && height == 1)
{
containersIdFull.Add(equipmentSlotId.ToString());
}
@@ -1,211 +0,0 @@
using System.Text.Json.Serialization;
using SPTarkov.DI.Annotations;
namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class ContainerHelper
{
/// <summary>
/// Finds a slot for an item in a given 2D container map
/// </summary>
/// <param name="container2D">List of container with positions filled/free</param>
/// <param name="itemX">Width of item</param>
/// <param name="itemY">Height of item</param>
/// <returns>Location to place item in container</returns>
public FindSlotResult FindSlotForItem(int[][] container2D, int? itemX, int? itemY)
{
// Assume not rotated
var rotation = false;
var minVolume = (itemX < itemY ? itemX : itemY) - 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 = y
for (var y = 0; y < limitY; y++)
{
if (container2D[y].All(x => x == 1))
// Every item in row is full, skip row
{
continue;
}
// Go left to right across x-axis looking for free position
for (var x = 0; x < limitX; x++)
{
if (
CanItemBePlacedInContainerAtPosition(
container2D,
containerX,
containerY,
x,
y,
itemX!.Value,
itemY!.Value
)
)
{
// Success, return result
return new FindSlotResult(true, x, y, rotation);
}
if (ItemBiggerThan1X1(itemX!.Value, itemY!.Value))
{
// Pointless rotating a 1x1, try next position across
continue;
}
// Bigger than 1x1, try rotating by swapping x and y values
if (
!CanItemBePlacedInContainerAtPosition(
container2D,
containerX,
containerY,
x,
y,
itemY!.Value,
itemX!.Value
)
)
{
continue;
}
// Found a position for item when rotated
rotation = true;
return new FindSlotResult(true, x, y, rotation);
}
}
// Tried all possible positions, nothing big enough for item
return new FindSlotResult(false);
}
protected static bool ItemBiggerThan1X1(int itemWidth, int itemHeight)
{
return itemWidth + itemHeight > 2;
}
/// <summary>
/// Can an item of specified size be placed inside a 2d container at a specific position
/// </summary>
/// <param name="container">Container to find space in</param>
/// <param name="containerWidth">Container x size</param>
/// <param name="containerHeight">Container y size</param>
/// <param name="startXPos">Starting x position for item</param>
/// <param name="startYPos">Starting y position for item</param>
/// <param name="itemWidth">Items width</param>
/// <param name="itemHeight">Items height</param>
/// <returns>True - slot found</returns>
protected bool CanItemBePlacedInContainerAtPosition(
int[][] container,
int containerWidth,
int containerHeight,
int startXPos,
int startYPos,
int itemWidth,
int itemHeight
)
{
// Check item isn't bigger than container when at position
if (startXPos + itemWidth > containerWidth || startYPos + itemHeight > containerHeight)
{
return false;
}
// Check each position item will take up in container, go across and then down
for (var itemY = startYPos; itemY < startYPos + itemHeight; itemY++)
{
for (var itemX = startXPos; itemX < startXPos + itemWidth; itemX++)
{
// e,g for a 2x2 item; [0,0] then [0,1] then [1,0] then [1,1]
if (container[itemY][itemX] != 0)
{
// x,y Position blocked, can't place
return false;
}
}
}
return true;
}
/// <summary>
/// Find a free slot for an item to be placed at
/// </summary>
/// <param name="container2D">Container to place item in</param>
/// <param name="x">Container x size</param>
/// <param name="y">Container y size</param>
/// <param name="itemW">Items width</param>
/// <param name="itemH">Items height</param>
/// <param name="rotate">is item rotated</param>
public void FillContainerMapWithItem(
int[][] container2D,
int x,
int y,
int? itemW,
int? itemH,
bool rotate
)
{
// Swap height/width if we want to fit it in rotated
var itemWidth = rotate ? itemH : itemW;
var itemHeight = rotate ? itemW : itemH;
for (var tmpY = y; tmpY < y + itemHeight; tmpY++)
for (var tmpX = x; tmpX < x + itemWidth; tmpX++)
{
if (container2D[tmpY][tmpX] == 0)
// Flag slot as used
{
container2D[tmpY][tmpX] = 1;
}
else
{
throw new Exception(
$"Slot at({x}, {y}) is already filled. Cannot fit a {itemW} by {itemH} item"
);
}
}
}
}
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 int? X { get; set; }
[JsonPropertyName("y")]
public int? Y { get; set; }
[JsonPropertyName("rotation")]
public bool? Rotation { get; set; }
}
@@ -1,7 +1,5 @@
using System.Collections.Frozen;
using System.Text.Json;
using System.Text.Json.Serialization;
using SPTarkov.Common.Extensions;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Common;
@@ -25,10 +23,8 @@ namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class InventoryHelper(
ISptLogger<InventoryHelper> _logger,
HashUtil _hashUtil,
HttpResponseUtil _httpResponseUtil,
DialogueHelper _dialogueHelper,
ContainerHelper _containerHelper,
EventOutputHolder _eventOutputHolder,
ProfileHelper _profileHelper,
ItemHelper _itemHelper,
@@ -146,10 +142,7 @@ public class InventoryHelper(
// Run callback
try
{
if (request.Callback is not null)
{
request.Callback((int)(itemWithModsToAddClone[0].Upd.StackObjectsCount ?? 0));
}
request.Callback?.Invoke((int)(itemWithModsToAddClone[0].Upd.StackObjectsCount ?? 0));
}
catch (Exception ex)
{
@@ -169,7 +162,7 @@ public class InventoryHelper(
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug(
$"Added {itemWithModsToAddClone[0].Upd?.StackObjectsCount ?? 1} item: {itemWithModsToAddClone[0].Template} with: {itemWithModsToAddClone.Count - 1} mods to inventory"
$"Added: {itemWithModsToAddClone[0].Upd?.StackObjectsCount ?? 1} item: {itemWithModsToAddClone[0].Template} with: {itemWithModsToAddClone.Count - 1} mods to inventory"
);
}
}
@@ -245,7 +238,7 @@ public class InventoryHelper(
/// <param name="containerFS2D">Container grid to fit items into</param>
/// <param name="itemsWithChildren">Items to try and fit into grid</param>
/// <returns>True all fit</returns>
public bool CanPlaceItemsInContainer(int[][] containerFS2D, List<List<Item>> itemsWithChildren)
public bool CanPlaceItemsInContainer(int[,] containerFS2D, List<List<Item>> itemsWithChildren)
{
return itemsWithChildren.All(itemWithChildren =>
CanPlaceItemInContainer(containerFS2D, itemWithChildren)
@@ -258,28 +251,23 @@ public class InventoryHelper(
/// <param name="containerFS2D">Container grid</param>
/// <param name="itemWithChildren">Item to check fits</param>
/// <returns>True it fits</returns>
public bool CanPlaceItemInContainer(int[][] containerFS2D, List<Item> itemWithChildren)
public bool CanPlaceItemInContainer(int[,] containerFS2D, List<Item> itemWithChildren)
{
// Get x/y size of item
var rootItem = itemWithChildren[0];
var itemSize = GetItemSize(rootItem.Template, rootItem.Id, itemWithChildren);
var (sizeX, sizeY) = GetItemSize(rootItem.Template, rootItem.Id, itemWithChildren);
// Look for a place to slot item into
var findSlotResult = _containerHelper.FindSlotForItem(
containerFS2D,
itemSize[0],
itemSize[1]
);
var findSlotResult = containerFS2D.FindSlotForItem(sizeX, sizeY);
if (findSlotResult.Success.GetValueOrDefault(false))
{
try
{
_containerHelper.FillContainerMapWithItem(
containerFS2D,
containerFS2D.FillContainerMapWithItem(
findSlotResult.X.Value,
findSlotResult.Y.Value,
itemSize[0],
itemSize[1],
sizeX,
sizeY,
findSlotResult.Rotation.Value
);
}
@@ -310,7 +298,7 @@ public class InventoryHelper(
/// <param name="containerId">Id of the container we're fitting item into</param>
/// <param name="desiredSlotId">Slot id value to use, default is "hideout"</param>
public void PlaceItemInContainer(
int[][] containerFS2D,
int[,] containerFS2D,
List<Item> itemWithChildren,
string containerId,
string desiredSlotId = "hideout"
@@ -318,24 +306,23 @@ public class InventoryHelper(
{
// Get x/y size of item
var rootItemAdded = itemWithChildren[0];
var itemSize = GetItemSize(rootItemAdded.Template, rootItemAdded.Id, itemWithChildren);
var (sizeX, sizeY) = GetItemSize(
rootItemAdded.Template,
rootItemAdded.Id,
itemWithChildren
);
// Look for a place to slot item into
var findSlotResult = _containerHelper.FindSlotForItem(
containerFS2D,
itemSize[0],
itemSize[1]
);
var findSlotResult = containerFS2D.FindSlotForItem(sizeX, sizeY);
if (findSlotResult.Success.GetValueOrDefault(false))
{
try
{
_containerHelper.FillContainerMapWithItem(
containerFS2D,
containerFS2D.FillContainerMapWithItem(
findSlotResult.X.Value,
findSlotResult.Y.Value,
itemSize[0],
itemSize[1],
sizeX,
sizeY,
findSlotResult.Rotation.Value
);
}
@@ -378,8 +365,8 @@ public class InventoryHelper(
/// <param name="useSortingTable">Should sorting table to be used if main stash has no space</param>
/// <param name="output">Output to send back to client</param>
protected void PlaceItemInInventory(
int[][] stashFS2D,
int[][] sortingTableFS2D,
int[,] stashFS2D,
int[,] sortingTableFS2D,
List<Item> itemWithChildren,
BotBaseInventory playerInventory,
bool useSortingTable,
@@ -388,20 +375,19 @@ public class InventoryHelper(
{
// Get x/y size of item
var rootItem = itemWithChildren[0];
var itemSize = GetItemSize(rootItem.Template, rootItem.Id, itemWithChildren);
var (sizeX, sizeY) = GetItemSize(rootItem.Template, rootItem.Id, itemWithChildren);
// Look for a place to slot item into
var findSlotResult = _containerHelper.FindSlotForItem(stashFS2D, itemSize[0], itemSize[1]);
var findSlotResult = stashFS2D.FindSlotForItem(sizeX, sizeY);
if (findSlotResult.Success.Value)
{
try
{
_containerHelper.FillContainerMapWithItem(
stashFS2D,
stashFS2D.FillContainerMapWithItem(
findSlotResult.X.Value,
findSlotResult.Y.Value,
itemSize[0],
itemSize[1],
sizeX,
sizeY,
findSlotResult.Rotation.Value
);
}
@@ -412,7 +398,7 @@ public class InventoryHelper(
return;
}
// Store details for object, incuding container item will be placed in
// Store details for object, including container item will be placed in
rootItem.ParentId = playerInventory.Stash;
rootItem.SlotId = "hideout";
rootItem.Location = new ItemLocation
@@ -430,20 +416,15 @@ public class InventoryHelper(
// Space not found in main stash, use sorting table
if (useSortingTable)
{
var findSortingSlotResult = _containerHelper.FindSlotForItem(
sortingTableFS2D,
itemSize[0],
itemSize[1]
);
var findSortingSlotResult = sortingTableFS2D.FindSlotForItem(sizeX, sizeY);
try
{
_containerHelper.FillContainerMapWithItem(
sortingTableFS2D,
sortingTableFS2D.FillContainerMapWithItem(
findSortingSlotResult.X.Value,
findSortingSlotResult.Y.Value,
itemSize[0],
itemSize[1],
sizeX,
sizeY,
findSortingSlotResult.Rotation.Value
);
}
@@ -687,7 +668,7 @@ public class InventoryHelper(
/// <param name="itemId">Items id to get size of</param>
/// <param name="inventoryItems"></param>
/// <returns>[width, height]</returns>
public List<int> GetItemSize(string? itemTpl, string itemId, List<Item> inventoryItems)
public (int, int) GetItemSize(string? itemTpl, string itemId, List<Item> inventoryItems)
{
// -> Prepares item Width and height returns [sizeX, sizeY]
return GetSizeByInventoryItemHash(itemTpl, itemId, GetInventoryItemHash(inventoryItems));
@@ -701,9 +682,9 @@ public class InventoryHelper(
/// <param name="itemId">Items id</param>
/// <param name="inventoryItemHash">Hashmap of inventory items</param>
/// <returns>An array representing the [width, height] of the item</returns>
protected List<int> GetSizeByInventoryItemHash(
string itemTpl,
string itemId,
protected (int, int) GetSizeByInventoryItemHash(
MongoId itemTpl,
MongoId itemId,
InventoryItemHash inventoryItemHash
)
{
@@ -736,7 +717,7 @@ public class InventoryHelper(
_serverLocalisationService.GetText("inventory-return_default_size", itemTpl)
);
return [1, 1]; // Invalid input data, return defaults
return (1, 1); // Invalid input data, return defaults
}
if (!inventoryItemHash.ByItemId.TryGetValue(itemId, out var rootItem))
@@ -745,7 +726,7 @@ public class InventoryHelper(
$"Unable to get root item with Id: {itemId} from player inventory. Defaulting to 1x1"
);
return [1, 1]; // Invalid input data, return defaults
return (1, 1); // Invalid input data, return defaults
}
// Does root item support being folded
@@ -867,11 +848,10 @@ public class InventoryHelper(
}
}
return
[
return (
outX.Value + sizeLeft + sizeRight + forcedLeft + forcedRight,
outY.Value + sizeUp + sizeDown + forcedUp + forcedDown,
];
outY.Value + sizeUp + sizeDown + forcedUp + forcedDown
);
}
/// <summary>
@@ -882,7 +862,7 @@ public class InventoryHelper(
/// <param name="itemList">Players inventory items</param>
/// <param name="containerId">Id of the container</param>
/// <returns>Two-dimensional representation of container</returns>
public int[][] GetContainerMap(int sizeX, int sizeY, List<Item> itemList, string containerId)
public int[,] GetContainerMap(int sizeX, int sizeY, List<Item> itemList, string containerId)
{
// Create blank 2d map of container
var containerYX = _itemHelper.GetBlankContainerMap(sizeY, sizeX);
@@ -892,66 +872,63 @@ public class InventoryHelper(
// Get subset of items that belong to the desired container
if (!inventoryItemHash.ByParentId.TryGetValue(containerId, out var rootItemsInContainer))
// No items in container, exit early
{
// No items in container, exit early and return the blank container map
return containerYX;
}
// Check each item in container
foreach (var item in rootItemsInContainer)
// Add every root items size (with mods attached) found in container
foreach (var rootItem in rootItemsInContainer)
{
ItemLocation? itemLocation;
if (item.Location is JsonElement element)
{
// TODO: is this ever true?
itemLocation = element.ToObject<ItemLocation>();
}
else
{
itemLocation = (ItemLocation?)item.Location;
}
var itemLocation = rootItem.GetParsedLocation();
if (itemLocation is null)
{
// Item has no location property
_logger.Error(
$"Unable to find 'location' property on item with id: {item.Id}, skipping"
$"Unable to find 'location' property on item with id: {rootItem.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 = itemLocation.IsVertical() ? iW : iH;
var fW = itemLocation.IsVertical() ? iH : iW;
var (xSize, ySize) = GetSizeByInventoryItemHash(
rootItem.Template,
rootItem.Id,
inventoryItemHash
);
var itemHSize = itemLocation.IsVertical() ? xSize : ySize;
var itemWSize = itemLocation.IsVertical() ? ySize : xSize;
for (var y = 0; y < fH; y++)
for (var yOffset = 0; yOffset < itemHSize; yOffset++)
{
try
for (var xOffset = 0; xOffset < itemWSize; xOffset++)
{
var rowIndex = itemLocation.Y + y;
var containerX = containerYX.ElementAtOrDefault(rowIndex.Value);
if (containerX is null)
{
_logger.Error(
$"Unable to find container: {containerId} row line: {itemLocation.Y + y}"
);
}
var currentY = itemLocation.Y.Value + yOffset;
var currentX = itemLocation.X.Value + xOffset;
// Fill the corresponding cells in the container map to show the slot is taken
Array.Fill(containerX, 1, itemLocation.X.Value, fW);
}
catch (Exception ex)
{
_logger.Error(
_serverLocalisationService.GetText(
"inventory-unable_to_fill_container",
new { id = item.Id, error = $"{ex.Message} {ex.StackTrace}" }
)
);
// Check still in containers bounds
if (currentY >= 0 && currentY < sizeY && currentX >= 0 && currentX < sizeX)
{
// mark slot used
containerYX[currentY, currentX] = 1;
}
else
{
// Out of bounds
var message =
$"Item: {rootItem.Id} at: {itemLocation.X}, {itemLocation.Y} size: {itemHSize}x{itemWSize} extends outside the containers bounds";
_logger.Error(
_serverLocalisationService.GetText(
"inventory-unable_to_fill_container",
new { id = rootItem.Id, error = $"{message}" }
)
);
// Stop and try next row
break;
}
}
}
}
@@ -975,6 +952,11 @@ public class InventoryHelper(
continue;
}
if (item.ParentId == "hideout")
{
continue;
}
if (!inventoryItemHash.ByParentId.ContainsKey(item.ParentId))
{
inventoryItemHash.ByParentId[item.ParentId] = [];
@@ -1057,7 +1039,7 @@ public class InventoryHelper(
/// </summary>
/// <param name="pmcData">Player profile</param>
/// <returns>2-dimensional array</returns>
protected int[][] GetStashSlotMap(PmcData pmcData)
protected int[,] GetStashSlotMap(PmcData pmcData)
{
var (horizontal, vertical) = GetPlayerStashSize(pmcData);
return GetContainerMap(
@@ -1073,7 +1055,7 @@ public class InventoryHelper(
/// </summary>
/// <param name="containerTpl">Container to get data for</param>
/// <returns>blank two-dimensional array</returns>
public int[][] GetContainerSlotMap(string containerTpl)
public int[,] GetContainerSlotMap(string containerTpl)
{
var containerTemplate = _itemHelper.GetItem(containerTpl).Value;
@@ -1089,7 +1071,7 @@ public class InventoryHelper(
/// </summary>
/// <param name="pmcData">Player profile</param>
/// <returns>two-dimensional array</returns>
protected int[][] GetSortingTableSlotMap(PmcData pmcData)
protected int[,] GetSortingTableSlotMap(PmcData pmcData)
{
return GetContainerMap(10, 45, pmcData.Inventory.Items, pmcData.Inventory.SortingTable);
}
@@ -2057,7 +2057,7 @@ public class ItemHelper(
/// Get a 2D grid of a container's item slots
/// </summary>
/// <param name="containerTpl">Tpl id of the container</param>
public int[][] GetContainerMapping(string containerTpl)
public int[,] GetContainerMapping(string containerTpl)
{
// Get template from db
var containerTemplate = GetItem(containerTpl).Value;
@@ -2075,16 +2075,8 @@ public class ItemHelper(
/// <param name="containerY">Horizontal size of container</param>
/// <param name="containerX">Vertical size of container</param>
/// <returns>Two-dimensional representation of container</returns>
public int[][] GetBlankContainerMap(int containerY, int containerX)
public int[,] GetBlankContainerMap(int containerY, int containerX)
{
//var x = new int[containerY][];
//for (int i = 0; i < containerY; i++)
//{
// x[i] = new int[containerH];
//}
//return x;
return Enumerable.Range(0, containerY).Select(_ => new int[containerX]).ToArray();
return new int[containerX, containerY];
}
}
@@ -1,30 +0,0 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class SecureContainerHelper(ItemHelper _itemHelper)
{
/// <summary>
/// Get a list of the item IDs (NOT tpls) inside a secure container
/// </summary>
/// <param name="items">Inventory items to look for secure container in</param>
/// <returns>List of ids</returns>
public List<string> GetSecureContainerItems(List<Item> items)
{
var secureContainer = items.First(x => x.SlotId == "SecuredContainer");
// No container found, drop out
if (secureContainer is null)
{
return [];
}
var itemsInSecureContainer = items.FindAndReturnChildrenByItems(secureContainer.Id);
// Return all items returned and exclude the secure container item itself
return itemsInSecureContainer.Where(x => x != secureContainer.Id).ToList();
}
}
@@ -23,11 +23,6 @@ public readonly struct MongoId : IEquatable<MongoId>
return;
}
if (id == "hideout")
{
throw new Exception("wtf");
}
if (id.Length != 24)
{
// TODO: Items.json root item has an empty parentId property
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace SPTarkov.Server.Core.Models.Spt.Inventory;
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 int? X { get; set; }
[JsonPropertyName("y")]
public int? Y { get; set; }
[JsonPropertyName("rotation")]
public bool? Rotation { get; set; }
}
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Generators;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Common;
@@ -9,7 +10,6 @@ using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Spt.Services;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Utils;
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
namespace SPTarkov.Server.Core.Services;
@@ -19,9 +19,7 @@ public class AirdropService(
ConfigServer configServer,
ISptLogger<AirdropService> _logger,
LootGenerator _lootGenerator,
HashUtil _hashUtil,
WeightedRandomHelper _weightedRandomHelper,
ContainerHelper _containerHelper,
ServerLocalisationService _serverLocalisationService,
ItemFilterService _itemFilterService,
ItemHelper _itemHelper
@@ -135,19 +133,14 @@ public class AirdropService(
var itemSize = _itemHelper.GetItemSize(itemAndChildren, itemAndChildren[0].Id);
// Look for open slot to put chosen item into
var result = _containerHelper.FindSlotForItem(
containerMap,
itemSize.Width,
itemSize.Height
);
var result = containerMap.FindSlotForItem(itemSize.Width, itemSize.Height);
if (result.Success.GetValueOrDefault(false))
{
// It Fits, add item + children
lootResult.AddRange(itemAndChildren);
// Update container with item we just added
_containerHelper.FillContainerMapWithItem(
containerMap,
containerMap.FillContainerMapWithItem(
result.X.Value,
result.Y.Value,
itemSize.Width,
@@ -137,7 +137,7 @@ public class CircleOfCultistService(
_hideoutConfig.CultistCircle
),
rewardAmountRoubles,
cultistCircleStashId,
cultistCircleStashId.Value,
_hideoutConfig.CultistCircle
);
@@ -153,7 +153,7 @@ public class CircleOfCultistService(
pmcData,
rewards,
containerGrid,
cultistCircleStashId,
cultistCircleStashId.Value,
output
);
@@ -352,7 +352,7 @@ public class CircleOfCultistService(
protected List<List<Item>> GetRewardsWithinBudget(
List<string> rewardItemTplPool,
double rewardBudget,
string cultistCircleStashId,
MongoId cultistCircleStashId,
CultistCircleSettings circleConfig
)
{
@@ -1014,8 +1014,8 @@ public class CircleOfCultistService(
string sessionId,
PmcData pmcData,
List<List<Item>> rewards,
int[][] containerGrid,
string cultistCircleStashId,
int[,] containerGrid,
MongoId cultistCircleStashId,
ItemEventRouterResponse output
)
{