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:
@@ -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
|
||||
)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user