Apply enforced file scoped namespacing
This commit is contained in:
@@ -1,28 +1,27 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
|
||||
namespace Benchmarks
|
||||
namespace Benchmarks;
|
||||
|
||||
[SimpleJob(warmupCount: 10, iterationCount: 25)]
|
||||
[MemoryDiagnoser]
|
||||
public class MathUtilInterpBenchmarks
|
||||
{
|
||||
[SimpleJob(warmupCount: 10, iterationCount: 25)]
|
||||
[MemoryDiagnoser]
|
||||
public class MathUtilInterpBenchmarks
|
||||
private MathUtil _mathUtil;
|
||||
|
||||
private double input = 15d;
|
||||
private List<double> x = [1, 10, 20, 30, 40, 50, 60];
|
||||
private List<double> y = [11000, 20000, 32000, 45000, 58000, 70000, 82000];
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
private MathUtil _mathUtil;
|
||||
_mathUtil = new MathUtil();
|
||||
}
|
||||
|
||||
private double input = 15d;
|
||||
private List<double> x = [1, 10, 20, 30, 40, 50, 60];
|
||||
private List<double> y = [11000, 20000, 32000, 45000, 58000, 70000, 82000];
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_mathUtil = new MathUtil();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Interp()
|
||||
{
|
||||
_mathUtil.Interp1(input, x, y);
|
||||
}
|
||||
[Benchmark]
|
||||
public void Interp()
|
||||
{
|
||||
_mathUtil.Interp1(input, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
namespace SPTarkov.Reflection.Patching
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchPrefixAttribute : Attribute { }
|
||||
namespace SPTarkov.Reflection.Patching;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchPostfixAttribute : Attribute { }
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchPrefixAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchTranspilerAttribute : Attribute { }
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchPostfixAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchFinalizerAttribute : Attribute { }
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchTranspilerAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchIlManipulatorAttribute : Attribute { }
|
||||
}
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchFinalizerAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class PatchIlManipulatorAttribute : Attribute { }
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
namespace SPTarkov.Server.Core.DI
|
||||
{
|
||||
/// <summary>
|
||||
/// A service locator designed specifically for Harmony patches and other
|
||||
/// parts of the application that do not have direct access to the Dependency Injection (DI) system.
|
||||
///
|
||||
/// This should not be used at all when having direct access to DI.
|
||||
/// </summary>
|
||||
public static class ServiceLocator
|
||||
{
|
||||
public static IServiceProvider ServiceProvider { get; private set; }
|
||||
namespace SPTarkov.Server.Core.DI;
|
||||
|
||||
internal static void SetServiceProvider(IServiceProvider provider)
|
||||
{
|
||||
ServiceProvider = provider;
|
||||
}
|
||||
/// <summary>
|
||||
/// A service locator designed specifically for Harmony patches and other
|
||||
/// parts of the application that do not have direct access to the Dependency Injection (DI) system.
|
||||
///
|
||||
/// This should not be used at all when having direct access to DI.
|
||||
/// </summary>
|
||||
public static class ServiceLocator
|
||||
{
|
||||
public static IServiceProvider ServiceProvider { get; private set; }
|
||||
|
||||
internal static void SetServiceProvider(IServiceProvider provider)
|
||||
{
|
||||
ServiceProvider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,253 +1,252 @@
|
||||
using SPTarkov.Server.Core.Models.Spt.Inventory;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class ContainerExtensions
|
||||
{
|
||||
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="itemWidthX">Width of item</param>
|
||||
/// <param name="itemHeightY">Height of item</param>
|
||||
/// <returns>Location to place item in container</returns>
|
||||
public static FindSlotResult FindSlotForItem(this int[,] container2D, int? itemWidthX, int? itemHeightY)
|
||||
{
|
||||
/// <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="itemWidthX">Width of item</param>
|
||||
/// <param name="itemHeightY">Height of item</param>
|
||||
/// <returns>Location to place item in container</returns>
|
||||
public static FindSlotResult FindSlotForItem(this int[,] container2D, int? itemWidthX, int? itemHeightY)
|
||||
// Assume not rotated
|
||||
var rotation = false;
|
||||
|
||||
// Find the min volume the item will take up
|
||||
var minVolume = (itemWidthX < itemHeightY ? itemWidthX : itemHeightY) - 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))
|
||||
{
|
||||
// Assume not rotated
|
||||
var rotation = false;
|
||||
|
||||
// Find the min volume the item will take up
|
||||
var minVolume = (itemWidthX < itemHeightY ? itemWidthX : itemHeightY) - 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 row = 0; row < limitY; row++)
|
||||
{
|
||||
if (RowIsFull(container2D, row))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Left to right across columns, look for free position
|
||||
for (var column = 0; column < limitX; column++)
|
||||
{
|
||||
// Does item fit
|
||||
if (CanItemBePlacedInContainerAtPosition(container2D, row, column, itemWidthX.Value, itemHeightY.Value))
|
||||
{
|
||||
// Success, found a spot it fits
|
||||
return new FindSlotResult(true, column, row, rotation);
|
||||
}
|
||||
|
||||
if (!ItemBiggerThan1X1(itemWidthX.Value, itemHeightY.Value))
|
||||
{
|
||||
// Doesn't fit AND rotating won't help
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rotate item by swapping x and y item values
|
||||
if (
|
||||
CanItemBePlacedInContainerAtPosition(
|
||||
container2D,
|
||||
row,
|
||||
column,
|
||||
itemHeightY.Value, // Swapped
|
||||
itemWidthX.Value // Swapped
|
||||
)
|
||||
)
|
||||
{
|
||||
// Found a position for the item when rotated
|
||||
rotation = true;
|
||||
return new FindSlotResult(true, column, row, 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="columnStartPositionX">Container y size</param>
|
||||
/// <param name="rowStartPositionY">Container x 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 columnStartPositionX,
|
||||
int rowStartPositionY,
|
||||
int? itemXWidth,
|
||||
int? itemYHeight,
|
||||
bool isRotated
|
||||
)
|
||||
// Down = y, iterate over rows
|
||||
for (var row = 0; row < limitY; row++)
|
||||
{
|
||||
var containerY = container2D.GetLength(0); // rows
|
||||
var containerX = container2D.GetLength(1); // columns
|
||||
|
||||
// Swap height/width if item needs to be rotated to fit
|
||||
var itemWidth = isRotated ? itemYHeight : itemXWidth;
|
||||
var itemHeight = isRotated ? itemXWidth : itemYHeight;
|
||||
|
||||
var itemRowEndPosition = rowStartPositionY + (itemHeight - 1);
|
||||
var itemColumnEndPosition = columnStartPositionX + (itemWidth - 1);
|
||||
|
||||
//Item is a 1x1, flag slot as taken and exit early
|
||||
if (itemXWidth == 1 && itemYHeight == 1)
|
||||
if (RowIsFull(container2D, row))
|
||||
{
|
||||
container2D[rowStartPositionY, columnStartPositionX] = 1;
|
||||
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Loop over rows and columns and flag each as taken by item
|
||||
for (var y = rowStartPositionY; y <= itemRowEndPosition; y++)
|
||||
// Left to right across columns, look for free position
|
||||
for (var column = 0; column < limitX; column++)
|
||||
{
|
||||
for (var x = columnStartPositionX; x <= itemColumnEndPosition; x++)
|
||||
// Does item fit
|
||||
if (CanItemBePlacedInContainerAtPosition(container2D, row, column, itemWidthX.Value, itemHeightY.Value))
|
||||
{
|
||||
if (container2D[y, x] == 0)
|
||||
{
|
||||
// Flag slot as used
|
||||
container2D[y, x] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(
|
||||
$"Slot at: ({containerX}, {containerY}) is already filled. Cannot fit: {itemXWidth} by {itemYHeight} item"
|
||||
);
|
||||
}
|
||||
// Success, found a spot it fits
|
||||
return new FindSlotResult(true, column, row, rotation);
|
||||
}
|
||||
|
||||
if (!ItemBiggerThan1X1(itemWidthX.Value, itemHeightY.Value))
|
||||
{
|
||||
// Doesn't fit AND rotating won't help
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rotate item by swapping x and y item values
|
||||
if (
|
||||
CanItemBePlacedInContainerAtPosition(
|
||||
container2D,
|
||||
row,
|
||||
column,
|
||||
itemHeightY.Value, // Swapped
|
||||
itemWidthX.Value // Swapped
|
||||
)
|
||||
)
|
||||
{
|
||||
// Found a position for the item when rotated
|
||||
rotation = true;
|
||||
return new FindSlotResult(true, column, row, rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(1); // Column
|
||||
for (var col = 0; col < containerColumnCount; col++)
|
||||
{
|
||||
if (container2D[rowIndex, col] == 0)
|
||||
{
|
||||
rowFull = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Tried all possible positions, nothing big enough for item
|
||||
return new FindSlotResult(false);
|
||||
}
|
||||
|
||||
return rowFull;
|
||||
/// <summary>
|
||||
/// Find a free slot for an item to be placed at
|
||||
/// </summary>
|
||||
/// <param name="container2D">Container to place item in</param>
|
||||
/// <param name="columnStartPositionX">Container y size</param>
|
||||
/// <param name="rowStartPositionY">Container x 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 columnStartPositionX,
|
||||
int rowStartPositionY,
|
||||
int? itemXWidth,
|
||||
int? itemYHeight,
|
||||
bool isRotated
|
||||
)
|
||||
{
|
||||
var containerY = container2D.GetLength(0); // rows
|
||||
var containerX = container2D.GetLength(1); // columns
|
||||
|
||||
// Swap height/width if item needs to be rotated to fit
|
||||
var itemWidth = isRotated ? itemYHeight : itemXWidth;
|
||||
var itemHeight = isRotated ? itemXWidth : itemYHeight;
|
||||
|
||||
var itemRowEndPosition = rowStartPositionY + (itemHeight - 1);
|
||||
var itemColumnEndPosition = columnStartPositionX + (itemWidth - 1);
|
||||
|
||||
//Item is a 1x1, flag slot as taken and exit early
|
||||
if (itemXWidth == 1 && itemYHeight == 1)
|
||||
{
|
||||
container2D[rowStartPositionY, columnStartPositionX] = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
// Loop over rows and columns and flag each as taken by item
|
||||
for (var y = rowStartPositionY; y <= itemRowEndPosition; y++)
|
||||
{
|
||||
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 = columnStartPositionX; x <= itemColumnEndPosition; x++)
|
||||
{
|
||||
for (var x = 0; x < containerX; x++)
|
||||
if (container2D[y, x] == 0)
|
||||
{
|
||||
if (container2D[y, x] == 0)
|
||||
{
|
||||
containerFull = false;
|
||||
break;
|
||||
}
|
||||
// Flag slot as used
|
||||
container2D[y, x] = 1;
|
||||
}
|
||||
if (!containerFull)
|
||||
else
|
||||
{
|
||||
break;
|
||||
throw new Exception(
|
||||
$"Slot at: ({containerX}, {containerY}) is already filled. Cannot fit: {itemXWidth} by {itemYHeight} item"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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="itemStartVerticalPos">Starting y position for item</param>
|
||||
/// <param name="itemStartHorizontalPos">Starting x position for item</param>
|
||||
/// <param name="itemWidth">Items width (y)</param>
|
||||
/// <param name="itemHeight">Items height (x)</param>
|
||||
/// <returns>True - slot found</returns>
|
||||
public static bool CanItemBePlacedInContainerAtPosition(
|
||||
this int[,] container,
|
||||
int itemStartVerticalPos,
|
||||
int itemStartHorizontalPos,
|
||||
int itemWidth,
|
||||
int itemHeight
|
||||
)
|
||||
{
|
||||
var containerHeight = container.GetLength(0); // Rows
|
||||
var containerWidth = container.GetLength(1); // Columns
|
||||
|
||||
var itemEndColPosition = itemStartHorizontalPos + itemWidth - 1;
|
||||
var itemEndRowPosition = itemStartVerticalPos + itemHeight - 1;
|
||||
|
||||
// Check item isn't bigger than container when at position
|
||||
if (itemEndColPosition > containerWidth - 1 || itemEndRowPosition > containerHeight - 1)
|
||||
{
|
||||
// Item is bigger than container, will never fit
|
||||
return false;
|
||||
}
|
||||
|
||||
// Early exit if exact spot is taken
|
||||
if (container[itemStartVerticalPos, itemStartHorizontalPos] == 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Single slot item, do direct check
|
||||
if (itemWidth == 1 && itemHeight == 1)
|
||||
{
|
||||
return container[itemStartVerticalPos, itemStartHorizontalPos] == 0;
|
||||
}
|
||||
|
||||
for (var row = itemStartVerticalPos; row <= itemEndRowPosition; row++)
|
||||
{
|
||||
for (var column = itemStartHorizontalPos; column <= itemEndColPosition; column++)
|
||||
{
|
||||
if (container[row, column] == 1)
|
||||
{
|
||||
// Occupied by something
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Slot is free
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(1); // Column
|
||||
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="itemStartVerticalPos">Starting y position for item</param>
|
||||
/// <param name="itemStartHorizontalPos">Starting x position for item</param>
|
||||
/// <param name="itemWidth">Items width (y)</param>
|
||||
/// <param name="itemHeight">Items height (x)</param>
|
||||
/// <returns>True - slot found</returns>
|
||||
public static bool CanItemBePlacedInContainerAtPosition(
|
||||
this int[,] container,
|
||||
int itemStartVerticalPos,
|
||||
int itemStartHorizontalPos,
|
||||
int itemWidth,
|
||||
int itemHeight
|
||||
)
|
||||
{
|
||||
var containerHeight = container.GetLength(0); // Rows
|
||||
var containerWidth = container.GetLength(1); // Columns
|
||||
|
||||
var itemEndColPosition = itemStartHorizontalPos + itemWidth - 1;
|
||||
var itemEndRowPosition = itemStartVerticalPos + itemHeight - 1;
|
||||
|
||||
// Check item isn't bigger than container when at position
|
||||
if (itemEndColPosition > containerWidth - 1 || itemEndRowPosition > containerHeight - 1)
|
||||
{
|
||||
// Item is bigger than container, will never fit
|
||||
return false;
|
||||
}
|
||||
|
||||
// Early exit if exact spot is taken
|
||||
if (container[itemStartVerticalPos, itemStartHorizontalPos] == 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Single slot item, do direct check
|
||||
if (itemWidth == 1 && itemHeight == 1)
|
||||
{
|
||||
return container[itemStartVerticalPos, itemStartHorizontalPos] == 0;
|
||||
}
|
||||
|
||||
for (var row = itemStartVerticalPos; row <= itemEndRowPosition; row++)
|
||||
{
|
||||
for (var column = itemStartHorizontalPos; column <= itemEndColPosition; column++)
|
||||
{
|
||||
if (container[row, column] == 1)
|
||||
{
|
||||
// Occupied by something
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Slot is free
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class CurrencyTypeExtensions
|
||||
{
|
||||
public static class CurrencyTypeExtensions
|
||||
/// <summary>
|
||||
/// Gets currency TPL from TAG
|
||||
/// </summary>
|
||||
/// <param name="currency"></param>
|
||||
/// <returns>Tpl of currency</returns>
|
||||
public static MongoId GetCurrencyTpl(this CurrencyType currency)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets currency TPL from TAG
|
||||
/// </summary>
|
||||
/// <param name="currency"></param>
|
||||
/// <returns>Tpl of currency</returns>
|
||||
public static MongoId GetCurrencyTpl(this CurrencyType currency)
|
||||
return currency switch
|
||||
{
|
||||
return currency switch
|
||||
{
|
||||
CurrencyType.EUR => Money.EUROS,
|
||||
CurrencyType.USD => Money.DOLLARS,
|
||||
CurrencyType.RUB => Money.ROUBLES,
|
||||
CurrencyType.GP => Money.GP,
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
CurrencyType.EUR => Money.EUROS,
|
||||
CurrencyType.USD => Money.DOLLARS,
|
||||
CurrencyType.RUB => Money.ROUBLES,
|
||||
CurrencyType.GP => Money.GP,
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,140 @@
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
public static class DateTimeExtensions
|
||||
/// <summary>
|
||||
/// Formats the time part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeOffset">The date to format in UTC.</param>
|
||||
/// <returns>The formatted time as 'HH-MM-SS'.</returns>
|
||||
public static string FormatToBsgTime(this DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
/// <summary>
|
||||
/// Formats the time part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeOffset">The date to format in UTC.</param>
|
||||
/// <returns>The formatted time as 'HH-MM-SS'.</returns>
|
||||
public static string FormatToBsgTime(this DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
var universalTime = dateTimeOffset.ToUniversalTime();
|
||||
var hour = Pad(universalTime.Hour);
|
||||
var minute = Pad(universalTime.Minute);
|
||||
var second = Pad(universalTime.Second);
|
||||
var universalTime = dateTimeOffset.ToUniversalTime();
|
||||
var hour = Pad(universalTime.Hour);
|
||||
var minute = Pad(universalTime.Minute);
|
||||
var second = Pad(universalTime.Second);
|
||||
|
||||
return $"{hour}-{minute}-{second}";
|
||||
return $"{hour}-{minute}-{second}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the time part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">The date to format in UTC.</param>
|
||||
/// <returns>The formatted time as 'HH-MM-SS'.</returns>
|
||||
public static string FormatToBsgTime(this DateTime dateTime)
|
||||
{
|
||||
var universalTime = dateTime.ToUniversalTime();
|
||||
var hour = Pad(universalTime.Hour);
|
||||
var minute = Pad(universalTime.Minute);
|
||||
var second = Pad(universalTime.Second);
|
||||
|
||||
return $"{hour}-{minute}-{second}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the date part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeOffset">The date to format in UTC.</param>
|
||||
/// <returns>The formatted date as 'YYYY-MM-DD'.</returns>
|
||||
public static string FormatToBsgDate(this DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
var universalTime = dateTimeOffset.ToUniversalTime();
|
||||
var day = Pad(universalTime.Day);
|
||||
var month = Pad(universalTime.Month);
|
||||
var year = Pad(universalTime.Year);
|
||||
|
||||
return $"{year}-{month}-{day}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the date part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">The date to format in UTC.</param>
|
||||
/// <returns>The formatted date as 'YYYY-MM-DD'.</returns>
|
||||
public static string FormatToBsgDate(this DateTime dateTime)
|
||||
{
|
||||
var universalTime = dateTime.ToUniversalTime();
|
||||
var day = Pad(universalTime.Day);
|
||||
var month = Pad(universalTime.Month);
|
||||
var year = Pad(universalTime.Year);
|
||||
|
||||
return $"{year}-{month}-{day}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pads a number with a leading zero if it is less than 10.
|
||||
/// </summary>
|
||||
/// <param name="number">The number to pad.</param>
|
||||
/// <returns>The padded number as a string.</returns>
|
||||
private static string Pad(int number)
|
||||
{
|
||||
return number.ToString().PadLeft(2, '0');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current time formatted to fit BSGs requirement
|
||||
/// </summary>
|
||||
/// <param name="date"> Date to format into bsg style </param>
|
||||
/// <returns> Time formatted in BSG format </returns>
|
||||
public static string GetBsgFormattedWeatherTime(this DateTime date)
|
||||
{
|
||||
return date.FormatToBsgTime().Replace("-", ":").Replace("-", ":");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the provided date fit between the two defined dates?
|
||||
/// Excludes year
|
||||
/// Inclusive of end date up to 23 hours 59 minutes
|
||||
/// </summary>
|
||||
/// <param name="dateToCheck">Date to check is between 2 dates</param>
|
||||
/// <param name="startMonth">Lower bound for month</param>
|
||||
/// <param name="startDay">Lower bound for day</param>
|
||||
/// <param name="endMonth">Upper bound for month</param>
|
||||
/// <param name="endDay">Upper bound for day</param>
|
||||
/// <returns>True when inside date range</returns>
|
||||
public static bool DateIsBetweenTwoDates(this DateTime dateToCheck, int startMonth, int startDay, int endMonth, int endDay)
|
||||
{
|
||||
var eventStartDate = new DateTime(dateToCheck.Year, startMonth, startDay);
|
||||
var eventEndDate = new DateTime(dateToCheck.Year, endMonth, endDay, 23, 59, 0);
|
||||
|
||||
return dateToCheck >= eventStartDate && dateToCheck <= eventEndDate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the closest monday to passed in datetime
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Date to get closest monday of</param>
|
||||
/// <param name="startDay">Starting day of week - Default = Monday</param>
|
||||
/// <returns>Monday as DateTime</returns>
|
||||
public static DateTime GetClosestDate(this DateTime dateTime, DayOfWeek startDay = DayOfWeek.Monday)
|
||||
{
|
||||
// Calculate difference from current day to Monday
|
||||
var diff = (7 + (dateTime.DayOfWeek - startDay)) % 7;
|
||||
|
||||
// Subtract difference to get date of most recent Monday
|
||||
return dateTime.AddDays(-1 * diff).Date;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the most recent requested day from date
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Date to start from</param>
|
||||
/// <param name="desiredDay">Desired day to find</param>
|
||||
/// <param name="inclusiveOfToday">Should today be included in check, default = true</param>
|
||||
/// <returns>Datetime of desired day</returns>
|
||||
public static DateTime GetMostRecentPreviousDay(this DateTime dateTime, DayOfWeek desiredDay, bool inclusiveOfToday = true)
|
||||
{
|
||||
// Get difference in day count from today to what day we want
|
||||
var dayDifferenceCount = (dateTime.DayOfWeek - desiredDay + 7) % 7;
|
||||
|
||||
// Today is wanted day + we are not counting today, we know desired day is exactly 7 days ago
|
||||
if (!inclusiveOfToday && dayDifferenceCount == 0)
|
||||
{
|
||||
dayDifferenceCount = 7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the time part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">The date to format in UTC.</param>
|
||||
/// <returns>The formatted time as 'HH-MM-SS'.</returns>
|
||||
public static string FormatToBsgTime(this DateTime dateTime)
|
||||
{
|
||||
var universalTime = dateTime.ToUniversalTime();
|
||||
var hour = Pad(universalTime.Hour);
|
||||
var minute = Pad(universalTime.Minute);
|
||||
var second = Pad(universalTime.Second);
|
||||
|
||||
return $"{hour}-{minute}-{second}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the date part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeOffset">The date to format in UTC.</param>
|
||||
/// <returns>The formatted date as 'YYYY-MM-DD'.</returns>
|
||||
public static string FormatToBsgDate(this DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
var universalTime = dateTimeOffset.ToUniversalTime();
|
||||
var day = Pad(universalTime.Day);
|
||||
var month = Pad(universalTime.Month);
|
||||
var year = Pad(universalTime.Year);
|
||||
|
||||
return $"{year}-{month}-{day}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the date part of a date as a UTC string.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">The date to format in UTC.</param>
|
||||
/// <returns>The formatted date as 'YYYY-MM-DD'.</returns>
|
||||
public static string FormatToBsgDate(this DateTime dateTime)
|
||||
{
|
||||
var universalTime = dateTime.ToUniversalTime();
|
||||
var day = Pad(universalTime.Day);
|
||||
var month = Pad(universalTime.Month);
|
||||
var year = Pad(universalTime.Year);
|
||||
|
||||
return $"{year}-{month}-{day}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pads a number with a leading zero if it is less than 10.
|
||||
/// </summary>
|
||||
/// <param name="number">The number to pad.</param>
|
||||
/// <returns>The padded number as a string.</returns>
|
||||
private static string Pad(int number)
|
||||
{
|
||||
return number.ToString().PadLeft(2, '0');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current time formatted to fit BSGs requirement
|
||||
/// </summary>
|
||||
/// <param name="date"> Date to format into bsg style </param>
|
||||
/// <returns> Time formatted in BSG format </returns>
|
||||
public static string GetBsgFormattedWeatherTime(this DateTime date)
|
||||
{
|
||||
return date.FormatToBsgTime().Replace("-", ":").Replace("-", ":");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the provided date fit between the two defined dates?
|
||||
/// Excludes year
|
||||
/// Inclusive of end date up to 23 hours 59 minutes
|
||||
/// </summary>
|
||||
/// <param name="dateToCheck">Date to check is between 2 dates</param>
|
||||
/// <param name="startMonth">Lower bound for month</param>
|
||||
/// <param name="startDay">Lower bound for day</param>
|
||||
/// <param name="endMonth">Upper bound for month</param>
|
||||
/// <param name="endDay">Upper bound for day</param>
|
||||
/// <returns>True when inside date range</returns>
|
||||
public static bool DateIsBetweenTwoDates(this DateTime dateToCheck, int startMonth, int startDay, int endMonth, int endDay)
|
||||
{
|
||||
var eventStartDate = new DateTime(dateToCheck.Year, startMonth, startDay);
|
||||
var eventEndDate = new DateTime(dateToCheck.Year, endMonth, endDay, 23, 59, 0);
|
||||
|
||||
return dateToCheck >= eventStartDate && dateToCheck <= eventEndDate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the closest monday to passed in datetime
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Date to get closest monday of</param>
|
||||
/// <param name="startDay">Starting day of week - Default = Monday</param>
|
||||
/// <returns>Monday as DateTime</returns>
|
||||
public static DateTime GetClosestDate(this DateTime dateTime, DayOfWeek startDay = DayOfWeek.Monday)
|
||||
{
|
||||
// Calculate difference from current day to Monday
|
||||
var diff = (7 + (dateTime.DayOfWeek - startDay)) % 7;
|
||||
|
||||
// Subtract difference to get date of most recent Monday
|
||||
return dateTime.AddDays(-1 * diff).Date;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the most recent requested day from date
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Date to start from</param>
|
||||
/// <param name="desiredDay">Desired day to find</param>
|
||||
/// <param name="inclusiveOfToday">Should today be included in check, default = true</param>
|
||||
/// <returns>Datetime of desired day</returns>
|
||||
public static DateTime GetMostRecentPreviousDay(this DateTime dateTime, DayOfWeek desiredDay, bool inclusiveOfToday = true)
|
||||
{
|
||||
// Get difference in day count from today to what day we want
|
||||
var dayDifferenceCount = (dateTime.DayOfWeek - desiredDay + 7) % 7;
|
||||
|
||||
// Today is wanted day + we are not counting today, we know desired day is exactly 7 days ago
|
||||
if (!inclusiveOfToday && dayDifferenceCount == 0)
|
||||
{
|
||||
dayDifferenceCount = 7;
|
||||
}
|
||||
|
||||
// Remove count of day difference to get desired day
|
||||
return dateTime.AddDays(-dayDifferenceCount);
|
||||
}
|
||||
// Remove count of day difference to get desired day
|
||||
return dateTime.AddDays(-dayDifferenceCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static class DictionaryExtensions
|
||||
/// <summary>
|
||||
/// Add a value by key to a dictionary, if the key doesn't exist, create it
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Dictionary key type</typeparam>
|
||||
/// <param name="dict">Dictionary to add/update</param>
|
||||
/// <param name="key">Key to update by</param>
|
||||
/// <param name="value">Value to add to key</param>
|
||||
public static void AddOrUpdate<T>(this IDictionary<T, double> dict, T key, double value)
|
||||
where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a value by key to a dictionary, if the key doesn't exist, create it
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Dictionary key type</typeparam>
|
||||
/// <param name="dict">Dictionary to add/update</param>
|
||||
/// <param name="key">Key to update by</param>
|
||||
/// <param name="value">Value to add to key</param>
|
||||
public static void AddOrUpdate<T>(this IDictionary<T, double> dict, T key, double value)
|
||||
where T : notnull
|
||||
if (!dict.TryAdd(key, value))
|
||||
{
|
||||
if (!dict.TryAdd(key, value))
|
||||
{
|
||||
dict[key] += value;
|
||||
}
|
||||
dict[key] += value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a value by key to a dictionary, if the key doesn't exist, create it
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Dictionary key type</typeparam>
|
||||
/// <param name="dict">Dictionary to add/update</param>
|
||||
/// <param name="key">Key to update by</param>
|
||||
/// <param name="value">Value to add to key</param>
|
||||
public static void AddOrUpdate<T>(this IDictionary<T, int> dict, T key, int value)
|
||||
where T : notnull
|
||||
/// <summary>
|
||||
/// Add a value by key to a dictionary, if the key doesn't exist, create it
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Dictionary key type</typeparam>
|
||||
/// <param name="dict">Dictionary to add/update</param>
|
||||
/// <param name="key">Key to update by</param>
|
||||
/// <param name="value">Value to add to key</param>
|
||||
public static void AddOrUpdate<T>(this IDictionary<T, int> dict, T key, int value)
|
||||
where T : notnull
|
||||
{
|
||||
if (!dict.TryAdd(key, value))
|
||||
{
|
||||
if (!dict.TryAdd(key, value))
|
||||
{
|
||||
dict[key] += value;
|
||||
}
|
||||
dict[key] += value;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveItems<K, V>(this IDictionary<K, V> collection, ISet<K> idsToRemove)
|
||||
public static void RemoveItems<K, V>(this IDictionary<K, V> collection, ISet<K> idsToRemove)
|
||||
{
|
||||
foreach (var key in idsToRemove)
|
||||
{
|
||||
foreach (var key in idsToRemove)
|
||||
{
|
||||
collection.Remove(key);
|
||||
}
|
||||
collection.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,72 @@
|
||||
using SPTarkov.Server.Core.Models.Eft.Match;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class EndRaidResultExtensions
|
||||
{
|
||||
public static class EndRaidResultExtensions
|
||||
private static readonly HashSet<ExitStatus> _deathStates = [ExitStatus.KILLED, ExitStatus.MISSINGINACTION, ExitStatus.LEFT];
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if player survives. run through will return false
|
||||
/// </summary>
|
||||
/// <param name="results"> Post raid request </param>
|
||||
/// <returns> True if survived </returns>
|
||||
public static bool IsPlayerSurvived(this EndRaidResult results)
|
||||
{
|
||||
private static readonly HashSet<ExitStatus> _deathStates = [ExitStatus.KILLED, ExitStatus.MISSINGINACTION, ExitStatus.LEFT];
|
||||
return results.Result == ExitStatus.SURVIVED;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if player survives. run through will return false
|
||||
/// </summary>
|
||||
/// <param name="results"> Post raid request </param>
|
||||
/// <returns> True if survived </returns>
|
||||
public static bool IsPlayerSurvived(this EndRaidResult results)
|
||||
/// <summary>
|
||||
/// Is the player dead after a raid - dead = anything other than "survived" / "runner"
|
||||
/// </summary>
|
||||
/// <param name="results"> Post raid request </param>
|
||||
/// <returns> True if dead </returns>
|
||||
public static bool IsPlayerDead(this EndRaidResult results)
|
||||
{
|
||||
return _deathStates.Contains(results.Result.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Has the player moved from one map to another
|
||||
/// </summary>
|
||||
/// <param name="results"> Post raid request </param>
|
||||
/// <returns> True if players transferred </returns>
|
||||
public static bool IsMapToMapTransfer(this EndRaidResult results)
|
||||
{
|
||||
return results.Result == ExitStatus.TRANSIT;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Was extract by car
|
||||
/// </summary>
|
||||
/// <param name="requestResults">Result object from completed raid</param>
|
||||
/// <param name="carExtracts">Car extract names</param>
|
||||
/// <returns> True if extract was by car </returns>
|
||||
public static bool TookCarExtract(this EndRaidResult? requestResults, HashSet<string> carExtracts)
|
||||
{
|
||||
// exit name is undefined on death
|
||||
if (string.IsNullOrEmpty(requestResults?.ExitName))
|
||||
{
|
||||
return results.Result == ExitStatus.SURVIVED;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the player dead after a raid - dead = anything other than "survived" / "runner"
|
||||
/// </summary>
|
||||
/// <param name="results"> Post raid request </param>
|
||||
/// <returns> True if dead </returns>
|
||||
public static bool IsPlayerDead(this EndRaidResult results)
|
||||
if (requestResults.ExitName.ToLowerInvariant().Contains("v-ex"))
|
||||
{
|
||||
return _deathStates.Contains(results.Result.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Has the player moved from one map to another
|
||||
/// </summary>
|
||||
/// <param name="results"> Post raid request </param>
|
||||
/// <returns> True if players transferred </returns>
|
||||
public static bool IsMapToMapTransfer(this EndRaidResult results)
|
||||
{
|
||||
return results.Result == ExitStatus.TRANSIT;
|
||||
}
|
||||
return carExtracts.Contains(requestResults.ExitName.Trim());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Was extract by car
|
||||
/// </summary>
|
||||
/// <param name="requestResults">Result object from completed raid</param>
|
||||
/// <param name="carExtracts">Car extract names</param>
|
||||
/// <returns> True if extract was by car </returns>
|
||||
public static bool TookCarExtract(this EndRaidResult? requestResults, HashSet<string> carExtracts)
|
||||
{
|
||||
// exit name is undefined on death
|
||||
if (string.IsNullOrEmpty(requestResults?.ExitName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requestResults.ExitName.ToLowerInvariant().Contains("v-ex"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return carExtracts.Contains(requestResults.ExitName.Trim());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raid exit was via coop extract
|
||||
/// </summary>
|
||||
/// <param name="raidResult">Result object from completed raid</param>
|
||||
/// <param name="coopExtracts"></param>
|
||||
/// <returns>True when exit was coop extract</returns>
|
||||
public static bool TookCoopExtract(this EndRaidResult? raidResult, HashSet<string> coopExtracts)
|
||||
{
|
||||
return raidResult?.ExitName is not null && coopExtracts.Contains(raidResult.ExitName.Trim());
|
||||
}
|
||||
/// <summary>
|
||||
/// Raid exit was via coop extract
|
||||
/// </summary>
|
||||
/// <param name="raidResult">Result object from completed raid</param>
|
||||
/// <param name="coopExtracts"></param>
|
||||
/// <returns>True when exit was coop extract</returns>
|
||||
public static bool TookCoopExtract(this EndRaidResult? raidResult, HashSet<string> coopExtracts)
|
||||
{
|
||||
return raidResult?.ExitName is not null && coopExtracts.Contains(raidResult.ExitName.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,221 +3,220 @@ using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
{
|
||||
public static class FullProfileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a list of suit ids to a profiles suit list, no duplicates
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to add clothing to</param>
|
||||
/// <param name="clothingIds">Clothing Ids to add to profile</param>
|
||||
public static void AddSuitsToProfile(this SptProfile fullProfile, IEnumerable<MongoId> clothingIds)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks ??= [];
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
foreach (var suitId in clothingIds)
|
||||
public static class FullProfileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a list of suit ids to a profiles suit list, no duplicates
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to add clothing to</param>
|
||||
/// <param name="clothingIds">Clothing Ids to add to profile</param>
|
||||
public static void AddSuitsToProfile(this SptProfile fullProfile, IEnumerable<MongoId> clothingIds)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks ??= [];
|
||||
|
||||
foreach (var suitId in clothingIds)
|
||||
{
|
||||
if (!fullProfile.CustomisationUnlocks.Exists(customisation => customisation.Id == suitId))
|
||||
{
|
||||
if (!fullProfile.CustomisationUnlocks.Exists(customisation => customisation.Id == suitId))
|
||||
{
|
||||
// Clothing item doesn't exist in profile, add it
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = suitId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
}
|
||||
);
|
||||
}
|
||||
// Clothing item doesn't exist in profile, add it
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = suitId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add customisations to game profiles based on game edition
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to add customisations to</param>
|
||||
public static void AddCustomisationUnlocksToProfile(this SptProfile fullProfile)
|
||||
/// <summary>
|
||||
/// Add customisations to game profiles based on game edition
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to add customisations to</param>
|
||||
public static void AddCustomisationUnlocksToProfile(this SptProfile fullProfile)
|
||||
{
|
||||
// Some game versions have additional customisation unlocks
|
||||
var gameEdition = fullProfile.GetGameEdition();
|
||||
|
||||
switch (gameEdition)
|
||||
{
|
||||
// Some game versions have additional customisation unlocks
|
||||
var gameEdition = fullProfile.GetGameEdition();
|
||||
case GameEditions.EDGE_OF_DARKNESS:
|
||||
// Gets EoD tags
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "6746fd09bafff85008048838",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
|
||||
switch (gameEdition)
|
||||
{
|
||||
case GameEditions.EDGE_OF_DARKNESS:
|
||||
// Gets EoD tags
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "6746fd09bafff85008048838",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67471938bafff850080488b7",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67471938bafff850080488b7",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
break;
|
||||
case GameEditions.UNHEARD:
|
||||
// Gets EoD+Unheard tags
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "6746fd09bafff85008048838",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
|
||||
break;
|
||||
case GameEditions.UNHEARD:
|
||||
// Gets EoD+Unheard tags
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "6746fd09bafff85008048838",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67471938bafff850080488b7",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67471938bafff850080488b7",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67471928d17d6431550563b5",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67471928d17d6431550563b5",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "6747193f170146228c0d2226",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "6747193f170146228c0d2226",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
// Unheard Clothing (Cultist Hood)
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "666841a02537107dc508b704",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.SUITE,
|
||||
}
|
||||
);
|
||||
|
||||
// Unheard Clothing (Cultist Hood)
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "666841a02537107dc508b704",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.SUITE,
|
||||
}
|
||||
);
|
||||
// Unheard background
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "675850ba33627edb710b0592",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.ENVIRONMENT,
|
||||
}
|
||||
);
|
||||
|
||||
// Unheard background
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "675850ba33627edb710b0592",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.ENVIRONMENT,
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
var prestigeLevel = fullProfile?.CharacterData?.PmcData?.Info?.PrestigeLevel;
|
||||
|
||||
var prestigeLevel = fullProfile?.CharacterData?.PmcData?.Info?.PrestigeLevel;
|
||||
|
||||
if (prestigeLevel is not null)
|
||||
{
|
||||
if (prestigeLevel >= 1)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "674dbf593bee1152d407f005",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (prestigeLevel >= 2)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "675dcfea7ae1a8792107ca99",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Dev profile additions
|
||||
if (fullProfile.ProfileInfo.Edition.ToLowerInvariant().Contains("developer"))
|
||||
// CyberTark background
|
||||
if (prestigeLevel is not null)
|
||||
{
|
||||
if (prestigeLevel >= 1)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67585108def253bd97084552",
|
||||
Id = "674dbf593bee1152d407f005",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.ENVIRONMENT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (prestigeLevel >= 2)
|
||||
{
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "675dcfea7ae1a8792107ca99",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.DOG_TAG,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the game edition of a profile chosen on creation in Launcher
|
||||
/// </summary>
|
||||
public static string GetGameEdition(this SptProfile fullProfile)
|
||||
// Dev profile additions
|
||||
if (fullProfile.ProfileInfo.Edition.ToLowerInvariant().Contains("developer"))
|
||||
// CyberTark background
|
||||
{
|
||||
var edition = fullProfile.CharacterData?.PmcData?.Info?.GameVersion;
|
||||
if (edition is not null)
|
||||
{
|
||||
return edition;
|
||||
}
|
||||
|
||||
// Edge case - profile not created yet, fall back to what launcher has set
|
||||
var launcherEdition = fullProfile.ProfileInfo.Edition;
|
||||
switch (launcherEdition.ToLowerInvariant())
|
||||
{
|
||||
case "edge of darkness":
|
||||
return GameEditions.EDGE_OF_DARKNESS;
|
||||
case "unheard":
|
||||
return GameEditions.UNHEARD;
|
||||
default:
|
||||
return GameEditions.STANDARD;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the given number of extra repeatable quests for the given type of repeatable to the users profile
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to add the extra repeatable to</param>
|
||||
/// <param name="repeatableId">The ID of the type of repeatable to increase</param>
|
||||
/// <param name="rewardValue">The number of extra repeatables to add</param>
|
||||
public static void AddExtraRepeatableQuest(this SptProfile fullProfile, MongoId repeatableId, double rewardValue)
|
||||
{
|
||||
fullProfile.SptData.ExtraRepeatableQuests ??= new Dictionary<MongoId, double>();
|
||||
|
||||
if (!fullProfile.SptData.ExtraRepeatableQuests.TryAdd(repeatableId, 0))
|
||||
{
|
||||
fullProfile.SptData.ExtraRepeatableQuests[repeatableId] += rewardValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the provided session id for a developer account
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to check</param>
|
||||
/// <returns>True if account is developer</returns>
|
||||
public static bool IsDeveloperAccount(this SptProfile fullProfile)
|
||||
{
|
||||
return fullProfile?.ProfileInfo?.Edition?.ToLowerInvariant().StartsWith("spt developer") ?? false;
|
||||
fullProfile.CustomisationUnlocks.Add(
|
||||
new CustomisationStorage
|
||||
{
|
||||
Id = "67585108def253bd97084552",
|
||||
Source = CustomisationSource.DEFAULT,
|
||||
Type = CustomisationType.ENVIRONMENT,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the game edition of a profile chosen on creation in Launcher
|
||||
/// </summary>
|
||||
public static string GetGameEdition(this SptProfile fullProfile)
|
||||
{
|
||||
var edition = fullProfile.CharacterData?.PmcData?.Info?.GameVersion;
|
||||
if (edition is not null)
|
||||
{
|
||||
return edition;
|
||||
}
|
||||
|
||||
// Edge case - profile not created yet, fall back to what launcher has set
|
||||
var launcherEdition = fullProfile.ProfileInfo.Edition;
|
||||
switch (launcherEdition.ToLowerInvariant())
|
||||
{
|
||||
case "edge of darkness":
|
||||
return GameEditions.EDGE_OF_DARKNESS;
|
||||
case "unheard":
|
||||
return GameEditions.UNHEARD;
|
||||
default:
|
||||
return GameEditions.STANDARD;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the given number of extra repeatable quests for the given type of repeatable to the users profile
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to add the extra repeatable to</param>
|
||||
/// <param name="repeatableId">The ID of the type of repeatable to increase</param>
|
||||
/// <param name="rewardValue">The number of extra repeatables to add</param>
|
||||
public static void AddExtraRepeatableQuest(this SptProfile fullProfile, MongoId repeatableId, double rewardValue)
|
||||
{
|
||||
fullProfile.SptData.ExtraRepeatableQuests ??= new Dictionary<MongoId, double>();
|
||||
|
||||
if (!fullProfile.SptData.ExtraRepeatableQuests.TryAdd(repeatableId, 0))
|
||||
{
|
||||
fullProfile.SptData.ExtraRepeatableQuests[repeatableId] += rewardValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the provided session id for a developer account
|
||||
/// </summary>
|
||||
/// <param name="fullProfile">Profile to check</param>
|
||||
/// <returns>True if account is developer</returns>
|
||||
public static bool IsDeveloperAccount(this SptProfile fullProfile)
|
||||
{
|
||||
return fullProfile?.ProfileInfo?.Edition?.ToLowerInvariant().StartsWith("spt developer") ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,31 @@
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Eft.ItemEvent;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class ItemEventRouterResponseExtensions
|
||||
{
|
||||
public static class ItemEventRouterResponseExtensions
|
||||
/// <summary>
|
||||
/// Add item stack change object into output route event response
|
||||
/// </summary>
|
||||
/// <param name="output">Response to add item change event into</param>
|
||||
/// <param name="sessionId">Session id</param>
|
||||
/// <param name="item">Item that was adjusted</param>
|
||||
public static void AddItemStackSizeChangeIntoEventResponse(this ItemEventRouterResponse output, MongoId sessionId, Item item)
|
||||
{
|
||||
/// <summary>
|
||||
/// Add item stack change object into output route event response
|
||||
/// </summary>
|
||||
/// <param name="output">Response to add item change event into</param>
|
||||
/// <param name="sessionId">Session id</param>
|
||||
/// <param name="item">Item that was adjusted</param>
|
||||
public static void AddItemStackSizeChangeIntoEventResponse(this ItemEventRouterResponse output, MongoId sessionId, Item item)
|
||||
{
|
||||
// TODO: replace with something safer like TryGet
|
||||
output
|
||||
.ProfileChanges[sessionId]
|
||||
.Items.ChangedItems.Add(
|
||||
new Item
|
||||
{
|
||||
Id = item.Id,
|
||||
Template = item.Template,
|
||||
ParentId = item.ParentId,
|
||||
SlotId = item.SlotId,
|
||||
Location = item.Location,
|
||||
Upd = new Upd { StackObjectsCount = item.Upd.StackObjectsCount },
|
||||
}
|
||||
);
|
||||
}
|
||||
// TODO: replace with something safer like TryGet
|
||||
output
|
||||
.ProfileChanges[sessionId]
|
||||
.Items.ChangedItems.Add(
|
||||
new Item
|
||||
{
|
||||
Id = item.Id,
|
||||
Template = item.Template,
|
||||
ParentId = item.ParentId,
|
||||
SlotId = item.SlotId,
|
||||
Location = item.Location,
|
||||
Upd = new Upd { StackObjectsCount = item.Upd.StackObjectsCount },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,447 +5,446 @@ using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class ItemExtensions
|
||||
{
|
||||
public static class ItemExtensions
|
||||
/// <summary>
|
||||
/// This method will compare two items and see if they are equivalent
|
||||
/// This method will NOT compare IDs on the items
|
||||
/// </summary>
|
||||
/// <param name="item1">first item to compare</param>
|
||||
/// <param name="item2">second item to compare</param>
|
||||
/// <param name="compareUpdProperties">Upd properties to compare between the items</param>
|
||||
/// <returns>true if they are the same</returns>
|
||||
public static bool IsSameItem(this Item item1, Item item2, ISet<string>? compareUpdProperties = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// This method will compare two items and see if they are equivalent
|
||||
/// This method will NOT compare IDs on the items
|
||||
/// </summary>
|
||||
/// <param name="item1">first item to compare</param>
|
||||
/// <param name="item2">second item to compare</param>
|
||||
/// <param name="compareUpdProperties">Upd properties to compare between the items</param>
|
||||
/// <returns>true if they are the same</returns>
|
||||
public static bool IsSameItem(this Item item1, Item item2, ISet<string>? compareUpdProperties = null)
|
||||
// Different tpl == different item
|
||||
if (item1.Template != item2.Template)
|
||||
{
|
||||
// Different tpl == different item
|
||||
if (item1.Template != item2.Template)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both lack upd object + same tpl = same
|
||||
if (item1.Upd is null && item2.Upd is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// item1 lacks upd, item2 has one
|
||||
if (item1.Upd is null && item2.Upd is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// item1 has upd, item2 lacks one
|
||||
if (item1.Upd is not null && item2.Upd is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// key = Upd property Type as string, value = comparison function that returns bool
|
||||
var comparers = new Dictionary<string, Func<Upd, Upd, bool>>
|
||||
{
|
||||
{ "Key", (upd1, upd2) => upd1.Key?.NumberOfUsages == upd2.Key?.NumberOfUsages },
|
||||
{ "Buff", (upd1, upd2) => upd1.Buff?.Value == upd2.Buff?.Value && upd1.Buff?.BuffType == upd2.Buff?.BuffType },
|
||||
{ "CultistAmulet", (upd1, upd2) => upd1.CultistAmulet?.NumberOfUsages == upd2.CultistAmulet?.NumberOfUsages },
|
||||
{ "Dogtag", (upd1, upd2) => upd1.Dogtag?.ProfileId == upd2.Dogtag?.ProfileId },
|
||||
{ "FaceShield", (upd1, upd2) => upd1.FaceShield?.Hits == upd2.FaceShield?.Hits },
|
||||
{
|
||||
"Foldable",
|
||||
(upd1, upd2) => upd1.Foldable?.Folded.GetValueOrDefault(false) == upd2.Foldable?.Folded.GetValueOrDefault(false)
|
||||
},
|
||||
{ "FoodDrink", (upd1, upd2) => upd1.FoodDrink?.HpPercent == upd2.FoodDrink?.HpPercent },
|
||||
{ "MedKit", (upd1, upd2) => upd1.MedKit?.HpResource == upd2.MedKit?.HpResource },
|
||||
{ "RecodableComponent", (upd1, upd2) => upd1.RecodableComponent?.IsEncoded == upd2.RecodableComponent?.IsEncoded },
|
||||
{ "RepairKit", (upd1, upd2) => upd1.RepairKit?.Resource == upd2.RepairKit?.Resource },
|
||||
{ "Resource", (upd1, upd2) => upd1.Resource?.UnitsConsumed == upd2.Resource?.UnitsConsumed },
|
||||
};
|
||||
|
||||
// Choose above keys or passed in keys to compare items with
|
||||
var valuesToCompare = compareUpdProperties?.Count > 0 ? compareUpdProperties : comparers.Keys.ToHashSet();
|
||||
foreach (var propertyName in valuesToCompare)
|
||||
{
|
||||
if (!comparers.TryGetValue(propertyName, out var comparer))
|
||||
// Key not found, skip
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!comparer(item1.Upd, item2.Upd))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both lack upd object + same tpl = same
|
||||
if (item1.Upd is null && item2.Upd is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if item is stored inside a container
|
||||
/// </summary>
|
||||
/// <param name="itemToCheck">Item to check is inside of container</param>
|
||||
/// <param name="desiredContainerSlotId">Name of slot to check item is in e.g. SecuredContainer/Backpack</param>
|
||||
/// <param name="items">Inventory with child parent items to check</param>
|
||||
/// <returns>True when item is in container</returns>
|
||||
public static bool ItemIsInsideContainer(this Item itemToCheck, string desiredContainerSlotId, IEnumerable<Item> items)
|
||||
// item1 lacks upd, item2 has one
|
||||
if (item1.Upd is null && item2.Upd is not null)
|
||||
{
|
||||
// Get items parent
|
||||
var parent = items.FirstOrDefault(item => item.Id.Equals(itemToCheck.ParentId));
|
||||
if (parent is null)
|
||||
// No parent, end of line, not inside container
|
||||
return false;
|
||||
}
|
||||
|
||||
// item1 has upd, item2 lacks one
|
||||
if (item1.Upd is not null && item2.Upd is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// key = Upd property Type as string, value = comparison function that returns bool
|
||||
var comparers = new Dictionary<string, Func<Upd, Upd, bool>>
|
||||
{
|
||||
{ "Key", (upd1, upd2) => upd1.Key?.NumberOfUsages == upd2.Key?.NumberOfUsages },
|
||||
{ "Buff", (upd1, upd2) => upd1.Buff?.Value == upd2.Buff?.Value && upd1.Buff?.BuffType == upd2.Buff?.BuffType },
|
||||
{ "CultistAmulet", (upd1, upd2) => upd1.CultistAmulet?.NumberOfUsages == upd2.CultistAmulet?.NumberOfUsages },
|
||||
{ "Dogtag", (upd1, upd2) => upd1.Dogtag?.ProfileId == upd2.Dogtag?.ProfileId },
|
||||
{ "FaceShield", (upd1, upd2) => upd1.FaceShield?.Hits == upd2.FaceShield?.Hits },
|
||||
{
|
||||
"Foldable",
|
||||
(upd1, upd2) => upd1.Foldable?.Folded.GetValueOrDefault(false) == upd2.Foldable?.Folded.GetValueOrDefault(false)
|
||||
},
|
||||
{ "FoodDrink", (upd1, upd2) => upd1.FoodDrink?.HpPercent == upd2.FoodDrink?.HpPercent },
|
||||
{ "MedKit", (upd1, upd2) => upd1.MedKit?.HpResource == upd2.MedKit?.HpResource },
|
||||
{ "RecodableComponent", (upd1, upd2) => upd1.RecodableComponent?.IsEncoded == upd2.RecodableComponent?.IsEncoded },
|
||||
{ "RepairKit", (upd1, upd2) => upd1.RepairKit?.Resource == upd2.RepairKit?.Resource },
|
||||
{ "Resource", (upd1, upd2) => upd1.Resource?.UnitsConsumed == upd2.Resource?.UnitsConsumed },
|
||||
};
|
||||
|
||||
// Choose above keys or passed in keys to compare items with
|
||||
var valuesToCompare = compareUpdProperties?.Count > 0 ? compareUpdProperties : comparers.Keys.ToHashSet();
|
||||
foreach (var propertyName in valuesToCompare)
|
||||
{
|
||||
if (!comparers.TryGetValue(propertyName, out var comparer))
|
||||
// Key not found, skip
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!comparer(item1.Upd, item2.Upd))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.SlotId == desiredContainerSlotId)
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if item is stored inside a container
|
||||
/// </summary>
|
||||
/// <param name="itemToCheck">Item to check is inside of container</param>
|
||||
/// <param name="desiredContainerSlotId">Name of slot to check item is in e.g. SecuredContainer/Backpack</param>
|
||||
/// <param name="items">Inventory with child parent items to check</param>
|
||||
/// <returns>True when item is in container</returns>
|
||||
public static bool ItemIsInsideContainer(this Item itemToCheck, string desiredContainerSlotId, IEnumerable<Item> items)
|
||||
{
|
||||
// Get items parent
|
||||
var parent = items.FirstOrDefault(item => item.Id.Equals(itemToCheck.ParentId));
|
||||
if (parent is null)
|
||||
// No parent, end of line, not inside container
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent.SlotId == desiredContainerSlotId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent.ItemIsInsideContainer(desiredContainerSlotId, items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the size of a stack, return 1 if no stack object count property found
|
||||
/// </summary>
|
||||
/// <param name="item">Item to get stack size of</param>
|
||||
/// <returns>size of stack</returns>
|
||||
public static int GetItemStackSize(this Item item)
|
||||
{
|
||||
if (item.Upd?.StackObjectsCount is not null)
|
||||
{
|
||||
return (int)item.Upd.StackObjectsCount;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dictionary from a collection of items, keyed by item id
|
||||
/// </summary>
|
||||
/// <param name="items">Collection of items</param>
|
||||
/// <returns>Dictionary of items</returns>
|
||||
public static Dictionary<MongoId, Item> GenerateItemsMap(this IEnumerable<Item> items)
|
||||
{
|
||||
// Convert list to dictionary, keyed by items Id
|
||||
return items.ToDictionary(item => item.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adopts orphaned items by resetting them as root "hideout" items. Helpful in situations where a parent has been
|
||||
/// deleted from a group of items and there are children still referencing the missing parent. This method will
|
||||
/// remove the reference from the children to the parent and set item properties to root values.
|
||||
/// </summary>
|
||||
/// <param name="rootId">The ID of the "root" of the container</param>
|
||||
/// <param name="items">Array of Items that should be adjusted</param>
|
||||
/// <returns>Returns Array of Items that have been adopted</returns>
|
||||
public static List<Item> AdoptOrphanedItems(this List<Item> items, string rootId)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
// Check if the item's parent exists.
|
||||
var parentExists = items.Any(parentItem => parentItem.Id.Equals(item.ParentId));
|
||||
|
||||
// If the parent does not exist and the item is not already a 'hideout' item, adopt the orphaned item by
|
||||
// setting the parent ID to the PMCs inventory equipment ID, the slot ID to 'hideout', and remove the location.
|
||||
if (!parentExists && item.ParentId != rootId && item.SlotId != "hideout")
|
||||
{
|
||||
return true;
|
||||
item.ParentId = rootId;
|
||||
item.SlotId = "hideout";
|
||||
item.Location = null;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursive function that looks at every item from parameter and gets their children's Ids + includes parent item in results
|
||||
/// </summary>
|
||||
/// <param name="items">List of items (item + possible children)</param>
|
||||
/// <param name="baseItemId">Parent item's id</param>
|
||||
/// <returns>list of child item ids</returns>
|
||||
public static List<MongoId> GetItemWithChildrenTpls(this IEnumerable<Item> items, MongoId baseItemId)
|
||||
{
|
||||
List<MongoId> list = [];
|
||||
|
||||
foreach (var childItem in items)
|
||||
{
|
||||
if (childItem.ParentId == baseItemId.ToString())
|
||||
{
|
||||
list.AddRange(GetItemWithChildrenTpls(items, childItem.Id));
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(baseItemId); // Required, push original item id onto array
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the passed in item has buy count restrictions
|
||||
/// </summary>
|
||||
/// <param name="itemToCheck">Item to check</param>
|
||||
/// <returns>true if it has buy restrictions</returns>
|
||||
public static bool HasBuyRestrictions(this Item itemToCheck)
|
||||
{
|
||||
return itemToCheck.Upd?.BuyRestrictionCurrent is not null && itemToCheck.Upd?.BuyRestrictionMax is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for a child using slotId, locationX and locationY.
|
||||
/// </summary>
|
||||
/// <param name="item">Item.</param>
|
||||
/// <returns>SlotId OR slotId, locationX, locationY.</returns>
|
||||
public static string GetChildId(this Item item)
|
||||
{
|
||||
if (item.Location is null)
|
||||
{
|
||||
return item.SlotId;
|
||||
}
|
||||
|
||||
var LocationTyped = (ItemLocation)item.Location;
|
||||
|
||||
return $"{item.SlotId},{LocationTyped.X},{LocationTyped.Y}";
|
||||
}
|
||||
|
||||
public static bool IsVertical(this ItemLocation itemLocation)
|
||||
{
|
||||
return itemLocation.R == ItemRotation.Vertical;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update items upd.StackObjectsCount to be 1 if its upd is missing or StackObjectsCount is undefined
|
||||
/// </summary>
|
||||
/// <param name="item">Item to update</param>
|
||||
/// <returns>Fixed item</returns>
|
||||
public static void FixItemStackCount(this Item item)
|
||||
{
|
||||
// Ensure item has 'Upd' object
|
||||
item.Upd ??= new Upd { StackObjectsCount = 1 };
|
||||
|
||||
// Ensure item has 'StackObjectsCount' property
|
||||
item.Upd.StackObjectsCount ??= 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an item with its attachments (children)
|
||||
/// </summary>
|
||||
/// <param name="items">List of items (item + possible children)</param>
|
||||
/// <param name="baseItemId">Parent item's id</param>
|
||||
/// <param name="excludeStoredItems">OPTIONAL - Include only mod items, exclude items stored inside root item</param>
|
||||
/// <returns>list of Item objects</returns>
|
||||
public static List<Item> GetItemWithChildren(this IEnumerable<Item> items, MongoId baseItemId, bool excludeStoredItems = false)
|
||||
{
|
||||
// Use dictionary to make key lookup faster, convert to list before being returned
|
||||
var itemList = items.ToList();
|
||||
OrderedDictionary<MongoId, Item> result = [];
|
||||
|
||||
// Find desired root item
|
||||
var desiredRootItem = itemList.FirstOrDefault(item => item.Id == baseItemId);
|
||||
if (desiredRootItem is null)
|
||||
{
|
||||
// Root not found, nothing to return, exit
|
||||
return [];
|
||||
}
|
||||
result.Add(desiredRootItem.Id, desiredRootItem);
|
||||
var rootItemIdString = desiredRootItem.Id.ToString();
|
||||
|
||||
foreach (var item in itemList)
|
||||
{
|
||||
if (result.ContainsKey(item.Id))
|
||||
{
|
||||
// Already processed, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
return parent.ItemIsInsideContainer(desiredContainerSlotId, items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the size of a stack, return 1 if no stack object count property found
|
||||
/// </summary>
|
||||
/// <param name="item">Item to get stack size of</param>
|
||||
/// <returns>size of stack</returns>
|
||||
public static int GetItemStackSize(this Item item)
|
||||
{
|
||||
if (item.Upd?.StackObjectsCount is not null)
|
||||
// Skip items with different parentId
|
||||
if (item.ParentId != rootItemIdString)
|
||||
{
|
||||
return (int)item.Upd.StackObjectsCount;
|
||||
continue;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a dictionary from a collection of items, keyed by item id
|
||||
/// </summary>
|
||||
/// <param name="items">Collection of items</param>
|
||||
/// <returns>Dictionary of items</returns>
|
||||
public static Dictionary<MongoId, Item> GenerateItemsMap(this IEnumerable<Item> items)
|
||||
{
|
||||
// Convert list to dictionary, keyed by items Id
|
||||
return items.ToDictionary(item => item.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adopts orphaned items by resetting them as root "hideout" items. Helpful in situations where a parent has been
|
||||
/// deleted from a group of items and there are children still referencing the missing parent. This method will
|
||||
/// remove the reference from the children to the parent and set item properties to root values.
|
||||
/// </summary>
|
||||
/// <param name="rootId">The ID of the "root" of the container</param>
|
||||
/// <param name="items">Array of Items that should be adjusted</param>
|
||||
/// <returns>Returns Array of Items that have been adopted</returns>
|
||||
public static List<Item> AdoptOrphanedItems(this List<Item> items, string rootId)
|
||||
{
|
||||
foreach (var item in items)
|
||||
// Is stored in parent and disallowed
|
||||
if (excludeStoredItems && item.Location is not null)
|
||||
{
|
||||
// Check if the item's parent exists.
|
||||
var parentExists = items.Any(parentItem => parentItem.Id.Equals(item.ParentId));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the parent does not exist and the item is not already a 'hideout' item, adopt the orphaned item by
|
||||
// setting the parent ID to the PMCs inventory equipment ID, the slot ID to 'hideout', and remove the location.
|
||||
if (!parentExists && item.ParentId != rootId && item.SlotId != "hideout")
|
||||
// Item may have children, check
|
||||
foreach (var subItem in GetItemWithChildren(itemList, item.Id))
|
||||
{
|
||||
result.Add(subItem.Id, subItem);
|
||||
}
|
||||
}
|
||||
|
||||
return result.Values.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an Item to SptLootItem
|
||||
/// </summary>
|
||||
/// <param name="item">Item to convert</param>
|
||||
/// <returns>Converted SptLootItem</returns>
|
||||
public static SptLootItem ToLootItem(this Item item)
|
||||
{
|
||||
return new SptLootItem
|
||||
{
|
||||
ComposedKey = null,
|
||||
Id = item.Id,
|
||||
Template = item.Template,
|
||||
Upd = item.Upd,
|
||||
ParentId = item.ParentId,
|
||||
SlotId = item.SlotId,
|
||||
Location = item.Location,
|
||||
Desc = item.Desc,
|
||||
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 HashSet<MongoId> GetSecureContainerItems(this IEnumerable<Item> items)
|
||||
{
|
||||
var secureContainer = items.First(x => x.SlotId == "SecuredContainer");
|
||||
|
||||
// No container found, drop out
|
||||
if (secureContainer is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var itemsInSecureContainer = items.GetItemWithChildrenTpls(secureContainer.Id);
|
||||
|
||||
// Return all items returned and exclude the secure container item itself
|
||||
return itemsInSecureContainer.Where(x => x != secureContainer.Id).ToHashSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerate all GUIDs with new IDs, except special item types (e.g. quest, sorting table, etc.)
|
||||
/// </summary>
|
||||
/// <param name="items"></param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<Item> ReplaceIDs(this IEnumerable<Item> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
// Generate new id
|
||||
var newId = new MongoId();
|
||||
|
||||
// Keep copy of original id
|
||||
var originalId = item.Id;
|
||||
|
||||
// Update items id to new one we generated
|
||||
item.Id = newId;
|
||||
|
||||
// Find all children of item and update their parent ids to match
|
||||
var childItems = items.Where(item => item.ParentId == originalId.ToString());
|
||||
foreach (var childItem in childItems)
|
||||
{
|
||||
childItem.ParentId = newId;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a root items _id property value to be unique
|
||||
/// </summary>
|
||||
/// <param name="itemWithChildren">Item to update root items _id property</param>
|
||||
/// <param name="newId">Optional: new id to use</param>
|
||||
/// <returns>New root id</returns>
|
||||
public static MongoId RemapRootItemId(this IEnumerable<Item> itemWithChildren, MongoId? newId = null)
|
||||
{
|
||||
newId ??= new MongoId();
|
||||
|
||||
var rootItemExistingId = itemWithChildren.FirstOrDefault().Id;
|
||||
|
||||
foreach (var item in itemWithChildren)
|
||||
{
|
||||
// Root, update id
|
||||
if (item.Id.Equals(rootItemExistingId))
|
||||
{
|
||||
item.Id = newId.Value;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Child with parent of root, update
|
||||
if (item.ParentId == rootItemExistingId)
|
||||
{
|
||||
item.ParentId = newId.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return newId.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create hashsets for passed in items, keyed by the items ID and by the items parentId
|
||||
/// </summary>
|
||||
/// <param name="inventoryItems">Items to hash</param>
|
||||
/// <returns>InventoryItemHash</returns>
|
||||
public static InventoryItemHash GetInventoryItemHash(this IEnumerable<Item> inventoryItems)
|
||||
{
|
||||
// Group by parentId + turn value into mongoId as we've filtered out non-mongoId values
|
||||
var byParentId = inventoryItems
|
||||
.Where(item => !string.IsNullOrEmpty(item.ParentId) && item.ParentId != "hideout")
|
||||
.GroupBy(item => new MongoId(item.ParentId))
|
||||
.ToDictionary(kvp => kvp.Key, group => group.ToHashSet());
|
||||
|
||||
return new InventoryItemHash { ByItemId = inventoryItems.ToDictionary(item => item.Id), ByParentId = byParentId };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove spawned in session (FiR) status from items inside a container
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Player profile</param>
|
||||
/// <param name="containerSlotId">Container slot id to find items for and remove FiR from e.g. "Backpack"</param>
|
||||
public static void RemoveFiRStatusFromItemsInContainer(this PmcData pmcData, string containerSlotId)
|
||||
{
|
||||
var container = pmcData?.Inventory?.Items?.FirstOrDefault(item => item.SlotId == containerSlotId);
|
||||
if (container is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parentItemLookup = pmcData.Inventory.Items.ToLookup(item => item.ParentId);
|
||||
var parentIdsToSearch = new Queue<string>();
|
||||
parentIdsToSearch.Enqueue(container.Id);
|
||||
|
||||
while (parentIdsToSearch.Count > 0)
|
||||
{
|
||||
var currentParentId = parentIdsToSearch.Dequeue();
|
||||
foreach (var childItem in parentItemLookup[currentParentId])
|
||||
{
|
||||
if (childItem.Upd?.SpawnedInSession != null && childItem.Upd.SpawnedInSession.Value)
|
||||
{
|
||||
item.ParentId = rootId;
|
||||
item.SlotId = "hideout";
|
||||
item.Location = null;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursive function that looks at every item from parameter and gets their children's Ids + includes parent item in results
|
||||
/// </summary>
|
||||
/// <param name="items">List of items (item + possible children)</param>
|
||||
/// <param name="baseItemId">Parent item's id</param>
|
||||
/// <returns>list of child item ids</returns>
|
||||
public static List<MongoId> GetItemWithChildrenTpls(this IEnumerable<Item> items, MongoId baseItemId)
|
||||
{
|
||||
List<MongoId> list = [];
|
||||
|
||||
foreach (var childItem in items)
|
||||
{
|
||||
if (childItem.ParentId == baseItemId.ToString())
|
||||
{
|
||||
list.AddRange(GetItemWithChildrenTpls(items, childItem.Id));
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(baseItemId); // Required, push original item id onto array
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the passed in item has buy count restrictions
|
||||
/// </summary>
|
||||
/// <param name="itemToCheck">Item to check</param>
|
||||
/// <returns>true if it has buy restrictions</returns>
|
||||
public static bool HasBuyRestrictions(this Item itemToCheck)
|
||||
{
|
||||
return itemToCheck.Upd?.BuyRestrictionCurrent is not null && itemToCheck.Upd?.BuyRestrictionMax is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for a child using slotId, locationX and locationY.
|
||||
/// </summary>
|
||||
/// <param name="item">Item.</param>
|
||||
/// <returns>SlotId OR slotId, locationX, locationY.</returns>
|
||||
public static string GetChildId(this Item item)
|
||||
{
|
||||
if (item.Location is null)
|
||||
{
|
||||
return item.SlotId;
|
||||
}
|
||||
|
||||
var LocationTyped = (ItemLocation)item.Location;
|
||||
|
||||
return $"{item.SlotId},{LocationTyped.X},{LocationTyped.Y}";
|
||||
}
|
||||
|
||||
public static bool IsVertical(this ItemLocation itemLocation)
|
||||
{
|
||||
return itemLocation.R == ItemRotation.Vertical;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update items upd.StackObjectsCount to be 1 if its upd is missing or StackObjectsCount is undefined
|
||||
/// </summary>
|
||||
/// <param name="item">Item to update</param>
|
||||
/// <returns>Fixed item</returns>
|
||||
public static void FixItemStackCount(this Item item)
|
||||
{
|
||||
// Ensure item has 'Upd' object
|
||||
item.Upd ??= new Upd { StackObjectsCount = 1 };
|
||||
|
||||
// Ensure item has 'StackObjectsCount' property
|
||||
item.Upd.StackObjectsCount ??= 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an item with its attachments (children)
|
||||
/// </summary>
|
||||
/// <param name="items">List of items (item + possible children)</param>
|
||||
/// <param name="baseItemId">Parent item's id</param>
|
||||
/// <param name="excludeStoredItems">OPTIONAL - Include only mod items, exclude items stored inside root item</param>
|
||||
/// <returns>list of Item objects</returns>
|
||||
public static List<Item> GetItemWithChildren(this IEnumerable<Item> items, MongoId baseItemId, bool excludeStoredItems = false)
|
||||
{
|
||||
// Use dictionary to make key lookup faster, convert to list before being returned
|
||||
var itemList = items.ToList();
|
||||
OrderedDictionary<MongoId, Item> result = [];
|
||||
|
||||
// Find desired root item
|
||||
var desiredRootItem = itemList.FirstOrDefault(item => item.Id == baseItemId);
|
||||
if (desiredRootItem is null)
|
||||
{
|
||||
// Root not found, nothing to return, exit
|
||||
return [];
|
||||
}
|
||||
result.Add(desiredRootItem.Id, desiredRootItem);
|
||||
var rootItemIdString = desiredRootItem.Id.ToString();
|
||||
|
||||
foreach (var item in itemList)
|
||||
{
|
||||
if (result.ContainsKey(item.Id))
|
||||
{
|
||||
// Already processed, skip
|
||||
continue;
|
||||
childItem.Upd.SpawnedInSession = false;
|
||||
}
|
||||
|
||||
// Skip items with different parentId
|
||||
if (item.ParentId != rootItemIdString)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is stored in parent and disallowed
|
||||
if (excludeStoredItems && item.Location is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Item may have children, check
|
||||
foreach (var subItem in GetItemWithChildren(itemList, item.Id))
|
||||
{
|
||||
result.Add(subItem.Id, subItem);
|
||||
}
|
||||
}
|
||||
|
||||
return result.Values.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an Item to SptLootItem
|
||||
/// </summary>
|
||||
/// <param name="item">Item to convert</param>
|
||||
/// <returns>Converted SptLootItem</returns>
|
||||
public static SptLootItem ToLootItem(this Item item)
|
||||
{
|
||||
return new SptLootItem
|
||||
{
|
||||
ComposedKey = null,
|
||||
Id = item.Id,
|
||||
Template = item.Template,
|
||||
Upd = item.Upd,
|
||||
ParentId = item.ParentId,
|
||||
SlotId = item.SlotId,
|
||||
Location = item.Location,
|
||||
Desc = item.Desc,
|
||||
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 HashSet<MongoId> GetSecureContainerItems(this IEnumerable<Item> items)
|
||||
{
|
||||
var secureContainer = items.First(x => x.SlotId == "SecuredContainer");
|
||||
|
||||
// No container found, drop out
|
||||
if (secureContainer is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var itemsInSecureContainer = items.GetItemWithChildrenTpls(secureContainer.Id);
|
||||
|
||||
// Return all items returned and exclude the secure container item itself
|
||||
return itemsInSecureContainer.Where(x => x != secureContainer.Id).ToHashSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerate all GUIDs with new IDs, except special item types (e.g. quest, sorting table, etc.)
|
||||
/// </summary>
|
||||
/// <param name="items"></param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<Item> ReplaceIDs(this IEnumerable<Item> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
// Generate new id
|
||||
var newId = new MongoId();
|
||||
|
||||
// Keep copy of original id
|
||||
var originalId = item.Id;
|
||||
|
||||
// Update items id to new one we generated
|
||||
item.Id = newId;
|
||||
|
||||
// Find all children of item and update their parent ids to match
|
||||
var childItems = items.Where(item => item.ParentId == originalId.ToString());
|
||||
foreach (var childItem in childItems)
|
||||
{
|
||||
childItem.ParentId = newId;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a root items _id property value to be unique
|
||||
/// </summary>
|
||||
/// <param name="itemWithChildren">Item to update root items _id property</param>
|
||||
/// <param name="newId">Optional: new id to use</param>
|
||||
/// <returns>New root id</returns>
|
||||
public static MongoId RemapRootItemId(this IEnumerable<Item> itemWithChildren, MongoId? newId = null)
|
||||
{
|
||||
newId ??= new MongoId();
|
||||
|
||||
var rootItemExistingId = itemWithChildren.FirstOrDefault().Id;
|
||||
|
||||
foreach (var item in itemWithChildren)
|
||||
{
|
||||
// Root, update id
|
||||
if (item.Id.Equals(rootItemExistingId))
|
||||
{
|
||||
item.Id = newId.Value;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Child with parent of root, update
|
||||
if (item.ParentId == rootItemExistingId)
|
||||
{
|
||||
item.ParentId = newId.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return newId.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create hashsets for passed in items, keyed by the items ID and by the items parentId
|
||||
/// </summary>
|
||||
/// <param name="inventoryItems">Items to hash</param>
|
||||
/// <returns>InventoryItemHash</returns>
|
||||
public static InventoryItemHash GetInventoryItemHash(this IEnumerable<Item> inventoryItems)
|
||||
{
|
||||
// Group by parentId + turn value into mongoId as we've filtered out non-mongoId values
|
||||
var byParentId = inventoryItems
|
||||
.Where(item => !string.IsNullOrEmpty(item.ParentId) && item.ParentId != "hideout")
|
||||
.GroupBy(item => new MongoId(item.ParentId))
|
||||
.ToDictionary(kvp => kvp.Key, group => group.ToHashSet());
|
||||
|
||||
return new InventoryItemHash { ByItemId = inventoryItems.ToDictionary(item => item.Id), ByParentId = byParentId };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove spawned in session (FiR) status from items inside a container
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Player profile</param>
|
||||
/// <param name="containerSlotId">Container slot id to find items for and remove FiR from e.g. "Backpack"</param>
|
||||
public static void RemoveFiRStatusFromItemsInContainer(this PmcData pmcData, string containerSlotId)
|
||||
{
|
||||
var container = pmcData?.Inventory?.Items?.FirstOrDefault(item => item.SlotId == containerSlotId);
|
||||
if (container is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parentItemLookup = pmcData.Inventory.Items.ToLookup(item => item.ParentId);
|
||||
var parentIdsToSearch = new Queue<string>();
|
||||
parentIdsToSearch.Enqueue(container.Id);
|
||||
|
||||
while (parentIdsToSearch.Count > 0)
|
||||
{
|
||||
var currentParentId = parentIdsToSearch.Dequeue();
|
||||
foreach (var childItem in parentItemLookup[currentParentId])
|
||||
{
|
||||
if (childItem.Upd?.SpawnedInSession != null && childItem.Upd.SpawnedInSession.Value)
|
||||
{
|
||||
childItem.Upd.SpawnedInSession = false;
|
||||
}
|
||||
|
||||
parentIdsToSearch.Enqueue(childItem.Id);
|
||||
}
|
||||
parentIdsToSearch.Enqueue(childItem.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
using SPTarkov.Server.Core.Models.Spt.Config;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the rouble amount for the desired container, multiplied by the current map bot will spawn on
|
||||
/// </summary>
|
||||
public static class LootContainerSettingsExtensions
|
||||
{
|
||||
public static double GetRoubleValue(this LootContainerSettings settings, int botLevel, string? locationId)
|
||||
{
|
||||
var roubleTotalByLevel = GetContainerRoubleTotalByLevel(botLevel, settings.TotalRubByLevel);
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
if (locationId is null)
|
||||
/// <summary>
|
||||
/// Get the rouble amount for the desired container, multiplied by the current map bot will spawn on
|
||||
/// </summary>
|
||||
public static class LootContainerSettingsExtensions
|
||||
{
|
||||
public static double GetRoubleValue(this LootContainerSettings settings, int botLevel, string? locationId)
|
||||
{
|
||||
var roubleTotalByLevel = GetContainerRoubleTotalByLevel(botLevel, settings.TotalRubByLevel);
|
||||
|
||||
if (locationId is null)
|
||||
{
|
||||
return roubleTotalByLevel;
|
||||
}
|
||||
|
||||
// Get multiplier for map, use default if map not found
|
||||
if (!settings.LocationMultiplier.TryGetValue(locationId, out var multiplier))
|
||||
{
|
||||
if (!settings.LocationMultiplier.TryGetValue("default", out multiplier))
|
||||
{
|
||||
return roubleTotalByLevel;
|
||||
}
|
||||
|
||||
// Get multiplier for map, use default if map not found
|
||||
if (!settings.LocationMultiplier.TryGetValue(locationId, out var multiplier))
|
||||
{
|
||||
if (!settings.LocationMultiplier.TryGetValue("default", out multiplier))
|
||||
{
|
||||
return roubleTotalByLevel;
|
||||
}
|
||||
}
|
||||
|
||||
return roubleTotalByLevel * multiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rouble cost total for loot in a bots backpack by the bots level
|
||||
/// Will return 0 for non PMCs
|
||||
/// </summary>
|
||||
/// <param name="botLevel">level of the bot</param>
|
||||
/// <param name="containerLootValuesPool">Pocket/vest/backpack</param>
|
||||
/// <returns>rouble amount</returns>
|
||||
private static double GetContainerRoubleTotalByLevel(int botLevel, IEnumerable<MinMaxLootValue> containerLootValuesPool)
|
||||
return roubleTotalByLevel * multiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rouble cost total for loot in a bots backpack by the bots level
|
||||
/// Will return 0 for non PMCs
|
||||
/// </summary>
|
||||
/// <param name="botLevel">level of the bot</param>
|
||||
/// <param name="containerLootValuesPool">Pocket/vest/backpack</param>
|
||||
/// <returns>rouble amount</returns>
|
||||
private static double GetContainerRoubleTotalByLevel(int botLevel, IEnumerable<MinMaxLootValue> containerLootValuesPool)
|
||||
{
|
||||
var matchingValue = containerLootValuesPool.FirstOrDefault(minMaxValue =>
|
||||
botLevel >= minMaxValue.Min && botLevel <= minMaxValue.Max
|
||||
);
|
||||
|
||||
if (matchingValue is null)
|
||||
{
|
||||
var matchingValue = containerLootValuesPool.FirstOrDefault(minMaxValue =>
|
||||
botLevel >= minMaxValue.Min && botLevel <= minMaxValue.Max
|
||||
);
|
||||
|
||||
if (matchingValue is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return matchingValue.Value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return matchingValue.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +1,82 @@
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class MathExtensions
|
||||
{
|
||||
public static class MathExtensions
|
||||
/// <summary>
|
||||
/// Helper to create the cumulative sum of all enumerable elements
|
||||
/// [1, 2, 3, 4].CumulativeSum() = [1, 3, 6, 10]
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable with numbers of which to calculate the cumulative sum</param>
|
||||
/// <returns>cumulative sum of values</returns>
|
||||
public static IEnumerable<double> CumulativeSum(this IEnumerable<double> values)
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper to create the cumulative sum of all enumerable elements
|
||||
/// [1, 2, 3, 4].CumulativeSum() = [1, 3, 6, 10]
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable with numbers of which to calculate the cumulative sum</param>
|
||||
/// <returns>cumulative sum of values</returns>
|
||||
public static IEnumerable<double> CumulativeSum(this IEnumerable<double> values)
|
||||
double sum = 0;
|
||||
foreach (var value in values)
|
||||
{
|
||||
double sum = 0;
|
||||
foreach (var value in values)
|
||||
{
|
||||
sum += value;
|
||||
yield return sum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create the cumulative sum of all enumerable elements
|
||||
/// [1, 2, 3, 4].CumulativeSum() = [1, 3, 6, 10]
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable with numbers of which to calculate the cumulative sum</param>
|
||||
/// <returns>cumulative sum of values</returns>
|
||||
public static IEnumerable<float> CumulativeSum(this IEnumerable<float> values)
|
||||
{
|
||||
float sum = 0;
|
||||
foreach (var value in values)
|
||||
{
|
||||
sum += value;
|
||||
yield return sum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create the product of each element times factor
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable of numbers which shall be multiplied by the factor</param>
|
||||
/// <param name="factor">Number to multiply each element by</param>
|
||||
/// <returns>An enumerable of elements all multiplied by the factor</returns>
|
||||
public static IEnumerable<double> Product(this IEnumerable<double> values, double factor)
|
||||
{
|
||||
return values.Select(v => v * factor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create the product of each element times factor
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable of numbers which shall be multiplied by the factor</param>
|
||||
/// <param name="factor">Number to multiply each element by</param>
|
||||
/// <returns>An enumerable of elements all multiplied by the factor</returns>
|
||||
public static IEnumerable<float> Product(this IEnumerable<float> values, float factor)
|
||||
{
|
||||
return values.Select(v => v * factor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to determine if one double is approx equal to another double
|
||||
/// </summary>
|
||||
/// <param name="value">Value to check</param>
|
||||
/// <param name="target">Target value</param>
|
||||
/// <param name="error">Error value</param>
|
||||
/// <returns>True if value is approx target within the error range</returns>
|
||||
public static bool Approx(this double value, double target, double error = 0.001d)
|
||||
{
|
||||
return Math.Abs(value - target) <= error;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to determine if one float is approx equal to another float
|
||||
/// </summary>
|
||||
/// <param name="value">Value to check</param>
|
||||
/// <param name="target">Target value</param>
|
||||
/// <param name="error">Error value</param>
|
||||
/// <returns>True if value is approx target within the error range</returns>
|
||||
public static bool Approx(this float value, float target, float error = 0.001f)
|
||||
{
|
||||
return Math.Abs(value - target) <= error;
|
||||
sum += value;
|
||||
yield return sum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create the cumulative sum of all enumerable elements
|
||||
/// [1, 2, 3, 4].CumulativeSum() = [1, 3, 6, 10]
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable with numbers of which to calculate the cumulative sum</param>
|
||||
/// <returns>cumulative sum of values</returns>
|
||||
public static IEnumerable<float> CumulativeSum(this IEnumerable<float> values)
|
||||
{
|
||||
float sum = 0;
|
||||
foreach (var value in values)
|
||||
{
|
||||
sum += value;
|
||||
yield return sum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create the product of each element times factor
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable of numbers which shall be multiplied by the factor</param>
|
||||
/// <param name="factor">Number to multiply each element by</param>
|
||||
/// <returns>An enumerable of elements all multiplied by the factor</returns>
|
||||
public static IEnumerable<double> Product(this IEnumerable<double> values, double factor)
|
||||
{
|
||||
return values.Select(v => v * factor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create the product of each element times factor
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable of numbers which shall be multiplied by the factor</param>
|
||||
/// <param name="factor">Number to multiply each element by</param>
|
||||
/// <returns>An enumerable of elements all multiplied by the factor</returns>
|
||||
public static IEnumerable<float> Product(this IEnumerable<float> values, float factor)
|
||||
{
|
||||
return values.Select(v => v * factor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to determine if one double is approx equal to another double
|
||||
/// </summary>
|
||||
/// <param name="value">Value to check</param>
|
||||
/// <param name="target">Target value</param>
|
||||
/// <param name="error">Error value</param>
|
||||
/// <returns>True if value is approx target within the error range</returns>
|
||||
public static bool Approx(this double value, double target, double error = 0.001d)
|
||||
{
|
||||
return Math.Abs(value - target) <= error;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to determine if one float is approx equal to another float
|
||||
/// </summary>
|
||||
/// <param name="value">Value to check</param>
|
||||
/// <param name="target">Target value</param>
|
||||
/// <param name="error">Error value</param>
|
||||
/// <returns>True if value is approx target within the error range</returns>
|
||||
public static bool Approx(this float value, float target, float error = 0.001f)
|
||||
{
|
||||
return Math.Abs(value - target) <= error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,70 @@
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class MongoIdExtensions
|
||||
{
|
||||
public static class MongoIdExtensions
|
||||
//Temporary, but necessary
|
||||
public static IEnumerable<MongoId> ToMongoIds(this IEnumerable<string> source)
|
||||
{
|
||||
//Temporary, but necessary
|
||||
public static IEnumerable<MongoId> ToMongoIds(this IEnumerable<string> source)
|
||||
return source.Select(s => (MongoId)s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="MongoId"/> is a valid 24-character hexadecimal string,
|
||||
/// which is the standard format for MongoDB ObjectIds.
|
||||
/// </summary>
|
||||
/// <param name="mongoId">The <see cref="MongoId"/> to validate.</param>
|
||||
/// <returns><see langword="true"/> if the <paramref name="mongoId"/> is a valid MongoDB ObjectId; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsValidMongoId(this MongoId mongoId)
|
||||
{
|
||||
var span = mongoId.ToString().AsSpan();
|
||||
|
||||
if (span.Length != 24)
|
||||
{
|
||||
return source.Select(s => (MongoId)s);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="MongoId"/> is a valid 24-character hexadecimal string,
|
||||
/// which is the standard format for MongoDB ObjectIds.
|
||||
/// </summary>
|
||||
/// <param name="mongoId">The <see cref="MongoId"/> to validate.</param>
|
||||
/// <returns><see langword="true"/> if the <paramref name="mongoId"/> is a valid MongoDB ObjectId; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsValidMongoId(this MongoId mongoId)
|
||||
for (var i = 0; i < 24; i++)
|
||||
{
|
||||
var span = mongoId.ToString().AsSpan();
|
||||
var c = span[i];
|
||||
var isHex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
|
||||
if (span.Length != 24)
|
||||
if (!isHex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < 24; i++)
|
||||
{
|
||||
var c = span[i];
|
||||
var isHex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
|
||||
if (!isHex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified string is a valid 24-character hexadecimal representation
|
||||
/// of a MongoDB ObjectId.
|
||||
/// </summary>
|
||||
/// <param name="mongoId">The string to validate as a MongoDB ObjectId.</param>
|
||||
/// <returns><see langword="true"/> if the <paramref name="mongoId"/> is a valid MongoDB ObjectId; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsValidMongoId(this string mongoId)
|
||||
{
|
||||
var span = mongoId.AsSpan();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (span.Length != 24)
|
||||
/// <summary>
|
||||
/// Determines whether the specified string is a valid 24-character hexadecimal representation
|
||||
/// of a MongoDB ObjectId.
|
||||
/// </summary>
|
||||
/// <param name="mongoId">The string to validate as a MongoDB ObjectId.</param>
|
||||
/// <returns><see langword="true"/> if the <paramref name="mongoId"/> is a valid MongoDB ObjectId; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsValidMongoId(this string mongoId)
|
||||
{
|
||||
var span = mongoId.AsSpan();
|
||||
|
||||
if (span.Length != 24)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < 24; i++)
|
||||
{
|
||||
var c = span[i];
|
||||
var isHex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
|
||||
if (!isHex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < 24; i++)
|
||||
{
|
||||
var c = span[i];
|
||||
var isHex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
|
||||
if (!isHex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,44 +2,43 @@
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
{
|
||||
public static class ProductionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Has the craft completed
|
||||
/// Ignores bitcoin farm/cultist circle as they're continuous crafts
|
||||
/// </summary>
|
||||
/// <param name="craft">Craft to check</param>
|
||||
/// <returns>True when craft is complete</returns>
|
||||
public static bool IsCraftComplete(this Production craft)
|
||||
{
|
||||
return craft.Progress >= craft.ProductionTime
|
||||
&& !craft.IsCraftOfType(HideoutAreas.BitcoinFarm)
|
||||
&& !craft.IsCraftOfType(HideoutAreas.CircleOfCultists);
|
||||
}
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Is a craft from a particular hideout area
|
||||
/// </summary>
|
||||
/// <param name="craft">Craft to check</param>
|
||||
/// <param name="hideoutType">Type to check craft against</param>
|
||||
/// <returns>True if it is from that area</returns>
|
||||
public static bool IsCraftOfType(this Production craft, HideoutAreas hideoutType)
|
||||
public static class ProductionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Has the craft completed
|
||||
/// Ignores bitcoin farm/cultist circle as they're continuous crafts
|
||||
/// </summary>
|
||||
/// <param name="craft">Craft to check</param>
|
||||
/// <returns>True when craft is complete</returns>
|
||||
public static bool IsCraftComplete(this Production craft)
|
||||
{
|
||||
return craft.Progress >= craft.ProductionTime
|
||||
&& !craft.IsCraftOfType(HideoutAreas.BitcoinFarm)
|
||||
&& !craft.IsCraftOfType(HideoutAreas.CircleOfCultists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is a craft from a particular hideout area
|
||||
/// </summary>
|
||||
/// <param name="craft">Craft to check</param>
|
||||
/// <param name="hideoutType">Type to check craft against</param>
|
||||
/// <returns>True if it is from that area</returns>
|
||||
public static bool IsCraftOfType(this Production craft, HideoutAreas hideoutType)
|
||||
{
|
||||
switch (hideoutType)
|
||||
{
|
||||
switch (hideoutType)
|
||||
{
|
||||
case HideoutAreas.WaterCollector:
|
||||
return craft.RecipeId == HideoutHelper.WaterCollectorId;
|
||||
case HideoutAreas.BitcoinFarm:
|
||||
return craft.RecipeId == HideoutHelper.BitcoinProductionId;
|
||||
case HideoutAreas.ScavCase:
|
||||
return craft.SptIsScavCase ?? false;
|
||||
case HideoutAreas.CircleOfCultists:
|
||||
return craft.SptIsCultistCircle ?? false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
case HideoutAreas.WaterCollector:
|
||||
return craft.RecipeId == HideoutHelper.WaterCollectorId;
|
||||
case HideoutAreas.BitcoinFarm:
|
||||
return craft.RecipeId == HideoutHelper.BitcoinProductionId;
|
||||
case HideoutAreas.ScavCase:
|
||||
return craft.SptIsScavCase ?? false;
|
||||
case HideoutAreas.CircleOfCultists:
|
||||
return craft.SptIsCultistCircle ?? false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,241 +3,240 @@ using SPTarkov.Server.Core.Models.Eft.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class ProfileExtensions
|
||||
{
|
||||
public static class ProfileExtensions
|
||||
/// <summary>
|
||||
/// Return all quest items current in the supplied profile
|
||||
/// </summary>
|
||||
/// <param name="profile">Profile to get quest items from</param>
|
||||
/// <returns>List of item objects</returns>
|
||||
public static IEnumerable<Item> GetQuestItemsInProfile(this PmcData profile)
|
||||
{
|
||||
/// <summary>
|
||||
/// Return all quest items current in the supplied profile
|
||||
/// </summary>
|
||||
/// <param name="profile">Profile to get quest items from</param>
|
||||
/// <returns>List of item objects</returns>
|
||||
public static IEnumerable<Item> GetQuestItemsInProfile(this PmcData profile)
|
||||
return profile?.Inventory?.Items.Where(i => i.ParentId == profile.Inventory.QuestRaidItems).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrade hideout wall from starting level to interactable level if necessary stations have been upgraded
|
||||
/// </summary>
|
||||
/// <param name="profile">Profile to upgrade wall in</param>
|
||||
public static void UnlockHideoutWallInProfile(this PmcData profile)
|
||||
{
|
||||
var profileHideoutAreas = profile.Hideout.Areas;
|
||||
var waterCollector = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.WaterCollector);
|
||||
var medStation = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.MedStation);
|
||||
var wall = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.EmergencyWall);
|
||||
|
||||
// No collector or med station, skip
|
||||
if (waterCollector is null && medStation is null)
|
||||
{
|
||||
return profile?.Inventory?.Items.Where(i => i.ParentId == profile.Inventory.QuestRaidItems).ToList();
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrade hideout wall from starting level to interactable level if necessary stations have been upgraded
|
||||
/// </summary>
|
||||
/// <param name="profile">Profile to upgrade wall in</param>
|
||||
public static void UnlockHideoutWallInProfile(this PmcData profile)
|
||||
// If med-station > level 1 AND water collector > level 1 AND wall is level 0
|
||||
if (waterCollector?.Level >= 1 && medStation?.Level >= 1 && wall?.Level <= 0)
|
||||
{
|
||||
var profileHideoutAreas = profile.Hideout.Areas;
|
||||
var waterCollector = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.WaterCollector);
|
||||
var medStation = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.MedStation);
|
||||
var wall = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.EmergencyWall);
|
||||
wall.Level = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// No collector or med station, skip
|
||||
if (waterCollector is null && medStation is null)
|
||||
/// <summary>
|
||||
/// Does the provided profile contain any condition counters
|
||||
/// </summary>
|
||||
/// <param name="profile"> Profile to check for condition counters </param>
|
||||
/// <returns> Profile has condition counters </returns>
|
||||
public static bool ProfileHasConditionCounters(this PmcData profile)
|
||||
{
|
||||
if (profile.TaskConditionCounters is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return profile.TaskConditionCounters.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific common skill from supplied profile
|
||||
/// </summary>
|
||||
/// <param name="profile">Player profile</param>
|
||||
/// <param name="skill">Skill to look up and return value from</param>
|
||||
/// <returns>Common skill object from desired profile</returns>
|
||||
public static CommonSkill? GetSkillFromProfile(this PmcData profile, SkillTypes skill)
|
||||
{
|
||||
return profile?.Skills?.Common?.FirstOrDefault(s => s.Id == skill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the scav karma level for a profile
|
||||
/// Is also the fence trader rep level
|
||||
/// </summary>
|
||||
/// <param name="pmcData">pmc profile</param>
|
||||
/// <returns>karma level</returns>
|
||||
public static double GetScavKarmaLevel(this PmcData pmcData)
|
||||
{
|
||||
// can be empty during profile creation
|
||||
if (!pmcData.TradersInfo.TryGetValue(Traders.FENCE, out var fenceInfo))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fenceInfo.Standing > 6)
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
|
||||
return Math.Floor(fenceInfo.Standing ?? 0);
|
||||
}
|
||||
|
||||
public static Skills GetSkillsOrDefault(this PmcData profile)
|
||||
{
|
||||
return profile?.Skills ?? GetDefaultSkills();
|
||||
}
|
||||
|
||||
private static Skills GetDefaultSkills()
|
||||
{
|
||||
return new Skills
|
||||
{
|
||||
Common = [],
|
||||
Mastering = [],
|
||||
Points = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively checks if the given item is
|
||||
/// inside the stash, that is it has the stash as
|
||||
/// ancestor with slotId=hideout
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Player profile</param>
|
||||
/// <param name="itemToCheck">Item to look for</param>
|
||||
/// <returns>True if item exists inside stash</returns>
|
||||
public static bool IsItemInStash(this PmcData pmcData, Item itemToCheck)
|
||||
{
|
||||
// Start recursive check
|
||||
return pmcData.IsParentInStash(itemToCheck.Id);
|
||||
}
|
||||
|
||||
public static bool IsParentInStash(this PmcData pmcData, MongoId itemId)
|
||||
{
|
||||
// Item not found / has no parent
|
||||
var item = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemId);
|
||||
if (item?.ParentId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Root level. Items parent is the stash with slotId "hideout"
|
||||
if (item.ParentId == pmcData.Inventory.Stash && item.SlotId == "hideout")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recursive case: Check the items parent
|
||||
return IsParentInStash(pmcData, item.ParentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterate over all bonuses and sum up all bonuses of desired type in provided profile
|
||||
/// </summary>
|
||||
/// <param name="pmcProfile">Player profile</param>
|
||||
/// <param name="desiredBonus">Bonus to sum up</param>
|
||||
/// <returns>Summed bonus value or 0 if no bonus found</returns>
|
||||
public static double GetBonusValueFromProfile(this PmcData pmcProfile, BonusType desiredBonus)
|
||||
{
|
||||
var bonuses = pmcProfile?.Bonuses?.Where(b => b.Type == desiredBonus);
|
||||
if (!bonuses.Any())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sum all bonuses found above
|
||||
return bonuses?.Sum(bonus => bonus?.Value ?? 0) ?? 0;
|
||||
}
|
||||
|
||||
public static bool PlayerIsFleaBanned(this PmcData pmcProfile, long currentTimestamp)
|
||||
{
|
||||
return pmcProfile?.Info?.Bans?.Any(b => b.BanType == BanType.RagFair && currentTimestamp < b.DateTime) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the current level of a player based on their accumulated experience points.
|
||||
/// This method iterates through an experience table to determine the highest level achieved
|
||||
/// by comparing the player's experience against cumulative thresholds.
|
||||
/// </summary>
|
||||
/// <param name="pmcData"> Player profile </param>
|
||||
/// <param name="expTable">Experience table from globals.json</param>
|
||||
/// <returns>
|
||||
/// The calculated level of the player as an integer, or null if the level cannot be determined.
|
||||
/// This value is also assigned to <see cref="PmcData.Info.Level" /> within the provided profile.
|
||||
/// </returns>
|
||||
public static int? CalculateLevel(this PmcData pmcData, ExpTable[] expTable)
|
||||
{
|
||||
var accExp = 0;
|
||||
for (var i = 0; i < expTable.Length; i++)
|
||||
{
|
||||
accExp += expTable[i].Experience;
|
||||
|
||||
if (pmcData.Info.Experience < accExp)
|
||||
{
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
// If med-station > level 1 AND water collector > level 1 AND wall is level 0
|
||||
if (waterCollector?.Level >= 1 && medStation?.Level >= 1 && wall?.Level <= 0)
|
||||
{
|
||||
wall.Level = 3;
|
||||
}
|
||||
pmcData.Info.Level = i + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the provided profile contain any condition counters
|
||||
/// </summary>
|
||||
/// <param name="profile"> Profile to check for condition counters </param>
|
||||
/// <returns> Profile has condition counters </returns>
|
||||
public static bool ProfileHasConditionCounters(this PmcData profile)
|
||||
return pmcData.Info.Level;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the provided item have a root item with the provided id
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Profile with items</param>
|
||||
/// <param name="item">Item to check</param>
|
||||
/// <param name="rootId">Root item id to check for</param>
|
||||
/// <returns>True when item has rootId, false when not</returns>
|
||||
public static bool DoesItemHaveRootId(this PmcData pmcData, Item item, MongoId rootId)
|
||||
{
|
||||
var currentItem = item;
|
||||
while (currentItem is not null)
|
||||
{
|
||||
if (profile.TaskConditionCounters is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return profile.TaskConditionCounters.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific common skill from supplied profile
|
||||
/// </summary>
|
||||
/// <param name="profile">Player profile</param>
|
||||
/// <param name="skill">Skill to look up and return value from</param>
|
||||
/// <returns>Common skill object from desired profile</returns>
|
||||
public static CommonSkill? GetSkillFromProfile(this PmcData profile, SkillTypes skill)
|
||||
{
|
||||
return profile?.Skills?.Common?.FirstOrDefault(s => s.Id == skill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the scav karma level for a profile
|
||||
/// Is also the fence trader rep level
|
||||
/// </summary>
|
||||
/// <param name="pmcData">pmc profile</param>
|
||||
/// <returns>karma level</returns>
|
||||
public static double GetScavKarmaLevel(this PmcData pmcData)
|
||||
{
|
||||
// can be empty during profile creation
|
||||
if (!pmcData.TradersInfo.TryGetValue(Traders.FENCE, out var fenceInfo))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fenceInfo.Standing > 6)
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
|
||||
return Math.Floor(fenceInfo.Standing ?? 0);
|
||||
}
|
||||
|
||||
public static Skills GetSkillsOrDefault(this PmcData profile)
|
||||
{
|
||||
return profile?.Skills ?? GetDefaultSkills();
|
||||
}
|
||||
|
||||
private static Skills GetDefaultSkills()
|
||||
{
|
||||
return new Skills
|
||||
{
|
||||
Common = [],
|
||||
Mastering = [],
|
||||
Points = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively checks if the given item is
|
||||
/// inside the stash, that is it has the stash as
|
||||
/// ancestor with slotId=hideout
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Player profile</param>
|
||||
/// <param name="itemToCheck">Item to look for</param>
|
||||
/// <returns>True if item exists inside stash</returns>
|
||||
public static bool IsItemInStash(this PmcData pmcData, Item itemToCheck)
|
||||
{
|
||||
// Start recursive check
|
||||
return pmcData.IsParentInStash(itemToCheck.Id);
|
||||
}
|
||||
|
||||
public static bool IsParentInStash(this PmcData pmcData, MongoId itemId)
|
||||
{
|
||||
// Item not found / has no parent
|
||||
var item = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemId);
|
||||
if (item?.ParentId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Root level. Items parent is the stash with slotId "hideout"
|
||||
if (item.ParentId == pmcData.Inventory.Stash && item.SlotId == "hideout")
|
||||
// If we've found the equipment root ID, return true
|
||||
if (currentItem.Id == rootId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recursive case: Check the items parent
|
||||
return IsParentInStash(pmcData, item.ParentId);
|
||||
// Otherwise get the parent item
|
||||
currentItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == currentItem.ParentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterate over all bonuses and sum up all bonuses of desired type in provided profile
|
||||
/// </summary>
|
||||
/// <param name="pmcProfile">Player profile</param>
|
||||
/// <param name="desiredBonus">Bonus to sum up</param>
|
||||
/// <returns>Summed bonus value or 0 if no bonus found</returns>
|
||||
public static double GetBonusValueFromProfile(this PmcData pmcProfile, BonusType desiredBonus)
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get status of a quest in player profile by its id
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Profile to search</param>
|
||||
/// <param name="questId">Quest id to look up</param>
|
||||
/// <returns>QuestStatus enum</returns>
|
||||
public static QuestStatusEnum GetQuestStatus(this PmcData pmcData, MongoId questId)
|
||||
{
|
||||
var quest = pmcData.Quests?.FirstOrDefault(q => q.QId == questId);
|
||||
|
||||
return quest?.Status ?? QuestStatusEnum.Locked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use values from the profiles template to reset all body part max values
|
||||
/// </summary>
|
||||
/// <param name="profile">Profile to update</param>
|
||||
/// <param name="profileTemplate">Template used to create profile</param>
|
||||
public static void ResetMaxLimbHp(this PmcData profile, TemplateSide profileTemplate)
|
||||
{
|
||||
foreach (var (partKey, bodyPart) in profile.Health.BodyParts)
|
||||
{
|
||||
var bonuses = pmcProfile?.Bonuses?.Where(b => b.Type == desiredBonus);
|
||||
if (!bonuses.Any())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sum all bonuses found above
|
||||
return bonuses?.Sum(bonus => bonus?.Value ?? 0) ?? 0;
|
||||
}
|
||||
|
||||
public static bool PlayerIsFleaBanned(this PmcData pmcProfile, long currentTimestamp)
|
||||
{
|
||||
return pmcProfile?.Info?.Bans?.Any(b => b.BanType == BanType.RagFair && currentTimestamp < b.DateTime) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the current level of a player based on their accumulated experience points.
|
||||
/// This method iterates through an experience table to determine the highest level achieved
|
||||
/// by comparing the player's experience against cumulative thresholds.
|
||||
/// </summary>
|
||||
/// <param name="pmcData"> Player profile </param>
|
||||
/// <param name="expTable">Experience table from globals.json</param>
|
||||
/// <returns>
|
||||
/// The calculated level of the player as an integer, or null if the level cannot be determined.
|
||||
/// This value is also assigned to <see cref="PmcData.Info.Level" /> within the provided profile.
|
||||
/// </returns>
|
||||
public static int? CalculateLevel(this PmcData pmcData, ExpTable[] expTable)
|
||||
{
|
||||
var accExp = 0;
|
||||
for (var i = 0; i < expTable.Length; i++)
|
||||
{
|
||||
accExp += expTable[i].Experience;
|
||||
|
||||
if (pmcData.Info.Experience < accExp)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
pmcData.Info.Level = i + 1;
|
||||
}
|
||||
|
||||
return pmcData.Info.Level;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the provided item have a root item with the provided id
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Profile with items</param>
|
||||
/// <param name="item">Item to check</param>
|
||||
/// <param name="rootId">Root item id to check for</param>
|
||||
/// <returns>True when item has rootId, false when not</returns>
|
||||
public static bool DoesItemHaveRootId(this PmcData pmcData, Item item, MongoId rootId)
|
||||
{
|
||||
var currentItem = item;
|
||||
while (currentItem is not null)
|
||||
{
|
||||
// If we've found the equipment root ID, return true
|
||||
if (currentItem.Id == rootId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise get the parent item
|
||||
currentItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == currentItem.ParentId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get status of a quest in player profile by its id
|
||||
/// </summary>
|
||||
/// <param name="pmcData">Profile to search</param>
|
||||
/// <param name="questId">Quest id to look up</param>
|
||||
/// <returns>QuestStatus enum</returns>
|
||||
public static QuestStatusEnum GetQuestStatus(this PmcData pmcData, MongoId questId)
|
||||
{
|
||||
var quest = pmcData.Quests?.FirstOrDefault(q => q.QId == questId);
|
||||
|
||||
return quest?.Status ?? QuestStatusEnum.Locked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use values from the profiles template to reset all body part max values
|
||||
/// </summary>
|
||||
/// <param name="profile">Profile to update</param>
|
||||
/// <param name="profileTemplate">Template used to create profile</param>
|
||||
public static void ResetMaxLimbHp(this PmcData profile, TemplateSide profileTemplate)
|
||||
{
|
||||
foreach (var (partKey, bodyPart) in profile.Health.BodyParts)
|
||||
{
|
||||
bodyPart.Health.Maximum = profileTemplate.Character.Health.BodyParts[partKey].Health.Maximum;
|
||||
}
|
||||
bodyPart.Health.Maximum = profileTemplate.Character.Health.BodyParts[partKey].Health.Maximum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,66 @@
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class QuestConditionExtensions
|
||||
{
|
||||
public static class QuestConditionExtensions
|
||||
/// <summary>
|
||||
/// Get all quest conditions from provided list
|
||||
/// </summary>
|
||||
/// <param name="questConditions">Input conditions</param>
|
||||
/// <param name="furtherFilter">OPTIONAL - Additional filter code to run</param>
|
||||
/// <returns></returns>
|
||||
public static List<QuestCondition> GetQuestConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all quest conditions from provided list
|
||||
/// </summary>
|
||||
/// <param name="questConditions">Input conditions</param>
|
||||
/// <param name="furtherFilter">OPTIONAL - Additional filter code to run</param>
|
||||
/// <returns></returns>
|
||||
public static List<QuestCondition> GetQuestConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
return FilterConditions(questConditions, "Quest", furtherFilter);
|
||||
}
|
||||
return FilterConditions(questConditions, "Quest", furtherFilter);
|
||||
}
|
||||
|
||||
public static List<QuestCondition> GetLevelConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
return FilterConditions(questConditions, "Level", furtherFilter);
|
||||
}
|
||||
public static List<QuestCondition> GetLevelConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
return FilterConditions(questConditions, "Level", furtherFilter);
|
||||
}
|
||||
|
||||
public static List<QuestCondition> GetLoyaltyConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
return FilterConditions(questConditions, "TraderLoyalty", furtherFilter);
|
||||
}
|
||||
public static List<QuestCondition> GetLoyaltyConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
return FilterConditions(questConditions, "TraderLoyalty", furtherFilter);
|
||||
}
|
||||
|
||||
public static List<QuestCondition> GetStandingConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
return FilterConditions(questConditions, "TraderStanding", furtherFilter);
|
||||
}
|
||||
public static List<QuestCondition> GetStandingConditions(
|
||||
this IEnumerable<QuestCondition> questConditions,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
return FilterConditions(questConditions, "TraderStanding", furtherFilter);
|
||||
}
|
||||
|
||||
private static List<QuestCondition> FilterConditions(
|
||||
IEnumerable<QuestCondition> questConditions,
|
||||
string questType,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
var filteredQuests = questConditions
|
||||
.Where(c =>
|
||||
private static List<QuestCondition> FilterConditions(
|
||||
IEnumerable<QuestCondition> questConditions,
|
||||
string questType,
|
||||
Func<QuestCondition, List<QuestCondition>>? furtherFilter = null
|
||||
)
|
||||
{
|
||||
var filteredQuests = questConditions
|
||||
.Where(c =>
|
||||
{
|
||||
if (c.ConditionType == questType)
|
||||
// return true or run the passed in function
|
||||
{
|
||||
if (c.ConditionType == questType)
|
||||
// return true or run the passed in function
|
||||
{
|
||||
return furtherFilter is null || furtherFilter(c).Any();
|
||||
}
|
||||
return furtherFilter is null || furtherFilter(c).Any();
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.ToList();
|
||||
return false;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return filteredQuests;
|
||||
}
|
||||
return filteredQuests;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,48 @@
|
||||
using SPTarkov.Server.Core.Models.Eft.Ragfair;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class RagfairOfferExtensions
|
||||
{
|
||||
public static class RagfairOfferExtensions
|
||||
/// <summary>
|
||||
/// Is the passed in offer stale - end time > passed in time
|
||||
/// </summary>
|
||||
/// <param name="offer">Offer to check</param>
|
||||
/// <param name="time">Time to check offer against</param>
|
||||
/// <returns>True - offer is stale</returns>
|
||||
public static bool IsStale(this RagfairOffer offer, long time)
|
||||
{
|
||||
/// <summary>
|
||||
/// Is the passed in offer stale - end time > passed in time
|
||||
/// </summary>
|
||||
/// <param name="offer">Offer to check</param>
|
||||
/// <param name="time">Time to check offer against</param>
|
||||
/// <returns>True - offer is stale</returns>
|
||||
public static bool IsStale(this RagfairOffer offer, long time)
|
||||
return offer.EndTime < time || (offer.Quantity) < 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does this offer come from a trader
|
||||
/// </summary>
|
||||
/// <param name="offer">Offer to check</param>
|
||||
/// <returns>True = from trader</returns>
|
||||
public static bool IsTraderOffer(this RagfairOffer offer)
|
||||
{
|
||||
if (offer.CreatedBy is not null)
|
||||
{
|
||||
return offer.EndTime < time || (offer.Quantity) < 1;
|
||||
return offer.CreatedBy == OfferCreator.Trader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does this offer come from a trader
|
||||
/// </summary>
|
||||
/// <param name="offer">Offer to check</param>
|
||||
/// <returns>True = from trader</returns>
|
||||
public static bool IsTraderOffer(this RagfairOffer offer)
|
||||
{
|
||||
if (offer.CreatedBy is not null)
|
||||
{
|
||||
return offer.CreatedBy == OfferCreator.Trader;
|
||||
}
|
||||
return offer.User.MemberType == MemberCategory.Trader;
|
||||
}
|
||||
|
||||
return offer.User.MemberType == MemberCategory.Trader;
|
||||
/// <summary>
|
||||
/// Was this offer created by a human player
|
||||
/// </summary>
|
||||
/// <param name="offer"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsPlayerOffer(this RagfairOffer offer)
|
||||
{
|
||||
if (offer.CreatedBy is not null)
|
||||
{
|
||||
return offer.CreatedBy == OfferCreator.Player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Was this offer created by a human player
|
||||
/// </summary>
|
||||
/// <param name="offer"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsPlayerOffer(this RagfairOffer offer)
|
||||
{
|
||||
if (offer.CreatedBy is not null)
|
||||
{
|
||||
return offer.CreatedBy == OfferCreator.Player;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
using SPTarkov.Server.Core.Models.Spt.Mod;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
{
|
||||
public static class SptModExtensions
|
||||
{
|
||||
public static string GetModPath(this SptMod sptMod)
|
||||
{
|
||||
var relativeModPath = Path.GetRelativePath(Directory.GetCurrentDirectory(), sptMod.Directory).Replace('\\', '/');
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
return relativeModPath;
|
||||
}
|
||||
public static class SptModExtensions
|
||||
{
|
||||
public static string GetModPath(this SptMod sptMod)
|
||||
{
|
||||
var relativeModPath = Path.GetRelativePath(Directory.GetCurrentDirectory(), sptMod.Directory).Replace('\\', '/');
|
||||
|
||||
return relativeModPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
using System.Text;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static class StringExtensions
|
||||
public static string Encode(this string value, EncodeType encode)
|
||||
{
|
||||
public static string Encode(this string value, EncodeType encode)
|
||||
return encode switch
|
||||
{
|
||||
return encode switch
|
||||
{
|
||||
EncodeType.BASE64 => Convert.ToBase64String(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.HEX => Convert.ToHexString(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.ASCII => Encoding.ASCII.GetString(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.UTF8 => Encoding.UTF8.GetString(Encoding.Default.GetBytes(value)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(encode), encode, null),
|
||||
};
|
||||
}
|
||||
EncodeType.BASE64 => Convert.ToBase64String(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.HEX => Convert.ToHexString(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.ASCII => Encoding.ASCII.GetString(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.UTF8 => Encoding.UTF8.GetString(Encoding.Default.GetBytes(value)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(encode), encode, null),
|
||||
};
|
||||
}
|
||||
|
||||
public static string Decode(this string value, EncodeType encode)
|
||||
public static string Decode(this string value, EncodeType encode)
|
||||
{
|
||||
return encode switch
|
||||
{
|
||||
return encode switch
|
||||
{
|
||||
EncodeType.BASE64 => Encoding.UTF8.GetString(Convert.FromBase64String(value)),
|
||||
EncodeType.HEX => Encoding.UTF8.GetString(Convert.FromHexString(value)),
|
||||
EncodeType.ASCII => Encoding.ASCII.GetString(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.UTF8 => Encoding.UTF8.GetString(Encoding.Default.GetBytes(value)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(encode), encode, null),
|
||||
};
|
||||
}
|
||||
EncodeType.BASE64 => Encoding.UTF8.GetString(Convert.FromBase64String(value)),
|
||||
EncodeType.HEX => Encoding.UTF8.GetString(Convert.FromHexString(value)),
|
||||
EncodeType.ASCII => Encoding.ASCII.GetString(Encoding.Default.GetBytes(value)),
|
||||
EncodeType.UTF8 => Encoding.UTF8.GetString(Encoding.Default.GetBytes(value)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(encode), encode, null),
|
||||
};
|
||||
}
|
||||
|
||||
public enum EncodeType
|
||||
{
|
||||
BASE64,
|
||||
HEX,
|
||||
ASCII,
|
||||
UTF8,
|
||||
}
|
||||
public enum EncodeType
|
||||
{
|
||||
BASE64,
|
||||
HEX,
|
||||
ASCII,
|
||||
UTF8,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +1,79 @@
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class TemplateItemExtensions
|
||||
{
|
||||
public static class TemplateItemExtensions
|
||||
public static IEnumerable<TemplateItem> OfClass(this Dictionary<MongoId, TemplateItem> templates, params MongoId[] baseClasses)
|
||||
{
|
||||
public static IEnumerable<TemplateItem> OfClass(this Dictionary<MongoId, TemplateItem> templates, params MongoId[] baseClasses)
|
||||
return templates.Where(x => baseClasses.Contains(x.Value.Parent)).Select(x => x.Value);
|
||||
}
|
||||
|
||||
public static IEnumerable<TemplateItem> OfClass(
|
||||
this Dictionary<MongoId, TemplateItem> templates,
|
||||
Func<TemplateItem, bool> pred,
|
||||
params MongoId[] baseClasses
|
||||
)
|
||||
{
|
||||
return templates.Where(x => baseClasses.Contains(x.Value.Parent) && pred(x.Value)).Select(x => x.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if item is quest item
|
||||
/// </summary>
|
||||
/// <param name="templateItem">Item to check quest status of</param>
|
||||
/// <returns>true if item is flagged as quest item</returns>
|
||||
public static bool IsQuestItem(this TemplateItem templateItem)
|
||||
{
|
||||
if (templateItem.Properties.QuestItem.GetValueOrDefault(false))
|
||||
{
|
||||
return templates.Where(x => baseClasses.Contains(x.Value.Parent)).Select(x => x.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IEnumerable<TemplateItem> OfClass(
|
||||
this Dictionary<MongoId, TemplateItem> templates,
|
||||
Func<TemplateItem, bool> pred,
|
||||
params MongoId[] baseClasses
|
||||
)
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a weapons default magazine template id
|
||||
/// </summary>
|
||||
/// <param name="weaponTemplate">Weapon to get default magazine for</param>
|
||||
/// <returns>Tpl of magazine</returns>
|
||||
public static MongoId? GetWeaponsDefaultMagazineTpl(this TemplateItem weaponTemplate)
|
||||
{
|
||||
return weaponTemplate.Properties.DefMagType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the default plate an armor has in its db item
|
||||
/// </summary>
|
||||
/// <param name="armorItem">Item to look up default plate</param>
|
||||
/// <param name="modSlot">front/back</param>
|
||||
/// <returns>Tpl of plate</returns>
|
||||
public static MongoId? GetDefaultPlateTpl(this TemplateItem armorItem, string modSlot)
|
||||
{
|
||||
var relatedItemDbModSlot = armorItem.Properties.Slots?.FirstOrDefault(slot =>
|
||||
string.Equals(slot.Name, modSlot, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
return relatedItemDbModSlot?.Props?.Filters?.FirstOrDefault()?.Plate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the passed in <see cref="TemplateItem"/> lack slots, cartridges or chambers
|
||||
/// </summary>
|
||||
/// <param name="item">Item to check</param>
|
||||
/// <returns>True if it lacks cartridges/chamber slots, False if not</returns>
|
||||
public static bool HasNoSlotsCartridgesOrChambers(this TemplateItem item)
|
||||
{
|
||||
if (item.Properties is null)
|
||||
{
|
||||
return templates.Where(x => baseClasses.Contains(x.Value.Parent) && pred(x.Value)).Select(x => x.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if item is quest item
|
||||
/// </summary>
|
||||
/// <param name="templateItem">Item to check quest status of</param>
|
||||
/// <returns>true if item is flagged as quest item</returns>
|
||||
public static bool IsQuestItem(this TemplateItem templateItem)
|
||||
{
|
||||
if (templateItem.Properties.QuestItem.GetValueOrDefault(false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a weapons default magazine template id
|
||||
/// </summary>
|
||||
/// <param name="weaponTemplate">Weapon to get default magazine for</param>
|
||||
/// <returns>Tpl of magazine</returns>
|
||||
public static MongoId? GetWeaponsDefaultMagazineTpl(this TemplateItem weaponTemplate)
|
||||
{
|
||||
return weaponTemplate.Properties.DefMagType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the default plate an armor has in its db item
|
||||
/// </summary>
|
||||
/// <param name="armorItem">Item to look up default plate</param>
|
||||
/// <param name="modSlot">front/back</param>
|
||||
/// <returns>Tpl of plate</returns>
|
||||
public static MongoId? GetDefaultPlateTpl(this TemplateItem armorItem, string modSlot)
|
||||
{
|
||||
var relatedItemDbModSlot = armorItem.Properties.Slots?.FirstOrDefault(slot =>
|
||||
string.Equals(slot.Name, modSlot, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
return relatedItemDbModSlot?.Props?.Filters?.FirstOrDefault()?.Plate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the passed in <see cref="TemplateItem"/> lack slots, cartridges or chambers
|
||||
/// </summary>
|
||||
/// <param name="item">Item to check</param>
|
||||
/// <returns>True if it lacks cartridges/chamber slots, False if not</returns>
|
||||
public static bool HasNoSlotsCartridgesOrChambers(this TemplateItem item)
|
||||
{
|
||||
if (item.Properties is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.Properties.Slots is null
|
||||
|| !item.Properties.Slots.Any()
|
||||
&& (item.Properties.Cartridges is null || !item.Properties.Cartridges.Any())
|
||||
&& (item.Properties.Chambers is null || !item.Properties.Chambers.Any());
|
||||
}
|
||||
return item.Properties.Slots is null
|
||||
|| !item.Properties.Slots.Any()
|
||||
&& (item.Properties.Cartridges is null || !item.Properties.Cartridges.Any())
|
||||
&& (item.Properties.Chambers is null || !item.Properties.Chambers.Any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,50 @@
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
public static class TraderAssortExtensions
|
||||
{
|
||||
public static class TraderAssortExtensions
|
||||
/// <summary>
|
||||
/// Remove an item from an assort
|
||||
/// Must be removed from the assorts; items + barterScheme + LoyaltyLevel
|
||||
/// </summary>
|
||||
/// <param name="assort">Assort to remove item from</param>
|
||||
/// <param name="itemId">Id of item to remove from assort</param>
|
||||
/// <param name="isFlea">Is the assort being modified the flea market assort</param>
|
||||
/// <returns>Modified assort</returns>
|
||||
public static TraderAssort RemoveItemFromAssort(this TraderAssort assort, MongoId itemId, bool isFlea = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// Remove an item from an assort
|
||||
/// Must be removed from the assorts; items + barterScheme + LoyaltyLevel
|
||||
/// </summary>
|
||||
/// <param name="assort">Assort to remove item from</param>
|
||||
/// <param name="itemId">Id of item to remove from assort</param>
|
||||
/// <param name="isFlea">Is the assort being modified the flea market assort</param>
|
||||
/// <returns>Modified assort</returns>
|
||||
public static TraderAssort RemoveItemFromAssort(this TraderAssort assort, MongoId itemId, bool isFlea = false)
|
||||
// Flea assort needs special handling, item must remain in assort but be flagged as locked
|
||||
if (isFlea && assort.BarterScheme.TryGetValue(itemId, out var listToUse))
|
||||
{
|
||||
// Flea assort needs special handling, item must remain in assort but be flagged as locked
|
||||
if (isFlea && assort.BarterScheme.TryGetValue(itemId, out var listToUse))
|
||||
foreach (var barterScheme in listToUse.SelectMany(barterSchemes => barterSchemes))
|
||||
{
|
||||
foreach (var barterScheme in listToUse.SelectMany(barterSchemes => barterSchemes))
|
||||
{
|
||||
barterScheme.SptQuestLocked = true;
|
||||
}
|
||||
|
||||
return assort;
|
||||
barterScheme.SptQuestLocked = true;
|
||||
}
|
||||
|
||||
assort.BarterScheme.Remove(itemId);
|
||||
assort.LoyalLevelItems.Remove(itemId);
|
||||
|
||||
// The item being removed may have children linked to it, find and remove them too
|
||||
var idsToRemove = assort.Items.GetItemWithChildrenTpls(itemId).ToHashSet();
|
||||
assort.Items.RemoveAll(item => idsToRemove.Contains(item.Id));
|
||||
|
||||
return assort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the blacklist provided, remove root items from assort
|
||||
/// </summary>
|
||||
/// <param name="assortToFilter">Trader assort to modify</param>
|
||||
/// <param name="itemsTplsToRemove">Item TPLs the assort should not have</param>
|
||||
public static void RemoveItemsFromAssort(this TraderAssort assortToFilter, HashSet<MongoId> itemsTplsToRemove)
|
||||
{
|
||||
assortToFilter.Items = assortToFilter
|
||||
.Items.Where(item => item.ParentId == "hideout" && itemsTplsToRemove.Contains(item.Template))
|
||||
.ToList();
|
||||
}
|
||||
assort.BarterScheme.Remove(itemId);
|
||||
assort.LoyalLevelItems.Remove(itemId);
|
||||
|
||||
// The item being removed may have children linked to it, find and remove them too
|
||||
var idsToRemove = assort.Items.GetItemWithChildrenTpls(itemId).ToHashSet();
|
||||
assort.Items.RemoveAll(item => idsToRemove.Contains(item.Id));
|
||||
|
||||
return assort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given the blacklist provided, remove root items from assort
|
||||
/// </summary>
|
||||
/// <param name="assortToFilter">Trader assort to modify</param>
|
||||
/// <param name="itemsTplsToRemove">Item TPLs the assort should not have</param>
|
||||
public static void RemoveItemsFromAssort(this TraderAssort assortToFilter, HashSet<MongoId> itemsTplsToRemove)
|
||||
{
|
||||
assortToFilter.Items = assortToFilter
|
||||
.Items.Where(item => item.ParentId == "hideout" && itemsTplsToRemove.Contains(item.Template))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
{
|
||||
public static class UtilityExtensions
|
||||
{
|
||||
public static IEnumerable<T> IntersectWith<T>(this IEnumerable<T> first, IEnumerable<T> second)
|
||||
{
|
||||
//a.Intersect(x => b.Contains(x)).ToList();
|
||||
// gives error Delegate type could not be inferred
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
return first.Where(second.Contains);
|
||||
}
|
||||
public static class UtilityExtensions
|
||||
{
|
||||
public static IEnumerable<T> IntersectWith<T>(this IEnumerable<T> first, IEnumerable<T> second)
|
||||
{
|
||||
//a.Intersect(x => b.Contains(x)).ToList();
|
||||
// gives error Delegate type could not be inferred
|
||||
|
||||
return first.Where(second.Contains);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
using SPTarkov.Server.Core.Constants;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common;
|
||||
|
||||
namespace SPTarkov.Server.Core.Extensions
|
||||
{
|
||||
public static class WildSpawnTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Is the passed in bot role a PMC (USEC/Bear/PMC)
|
||||
/// </summary>
|
||||
/// <param name="botRole">bot role to check</param>
|
||||
/// <returns>true if is pmc</returns>
|
||||
public static bool IsPmc(this WildSpawnType botRole)
|
||||
{
|
||||
return botRole is WildSpawnType.pmcBEAR or WildSpawnType.pmcUSEC;
|
||||
}
|
||||
namespace SPTarkov.Server.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Get the corresponding side when pmcBEAR or pmcUSEC is passed in
|
||||
/// </summary>
|
||||
/// <param name="botRole">role to get side for</param>
|
||||
/// <returns>Usec/Bear</returns>
|
||||
public static string? GetPmcSideByRole(this WildSpawnType botRole)
|
||||
public static class WildSpawnTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Is the passed in bot role a PMC (USEC/Bear/PMC)
|
||||
/// </summary>
|
||||
/// <param name="botRole">bot role to check</param>
|
||||
/// <returns>true if is pmc</returns>
|
||||
public static bool IsPmc(this WildSpawnType botRole)
|
||||
{
|
||||
return botRole is WildSpawnType.pmcBEAR or WildSpawnType.pmcUSEC;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the corresponding side when pmcBEAR or pmcUSEC is passed in
|
||||
/// </summary>
|
||||
/// <param name="botRole">role to get side for</param>
|
||||
/// <returns>Usec/Bear</returns>
|
||||
public static string? GetPmcSideByRole(this WildSpawnType botRole)
|
||||
{
|
||||
switch (botRole)
|
||||
{
|
||||
switch (botRole)
|
||||
{
|
||||
case WildSpawnType.pmcBEAR:
|
||||
return Sides.Bear;
|
||||
case WildSpawnType.pmcUSEC:
|
||||
return Sides.Usec;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
case WildSpawnType.pmcBEAR:
|
||||
return Sides.Bear;
|
||||
case WildSpawnType.pmcUSEC:
|
||||
return Sides.Usec;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration
|
||||
namespace SPTarkov.Server.Core.Migration;
|
||||
|
||||
public abstract class AbstractProfileMigration : IProfileMigration
|
||||
{
|
||||
public abstract class AbstractProfileMigration : IProfileMigration
|
||||
public abstract string FromVersion { get; }
|
||||
public abstract string ToVersion { get; }
|
||||
public abstract string MigrationName { get; }
|
||||
|
||||
public abstract IEnumerable<Type> PrerequisiteMigrations { get; }
|
||||
|
||||
public abstract bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations);
|
||||
|
||||
public virtual JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
public abstract string FromVersion { get; }
|
||||
public abstract string ToVersion { get; }
|
||||
public abstract string MigrationName { get; }
|
||||
return profile;
|
||||
}
|
||||
|
||||
public abstract IEnumerable<Type> PrerequisiteMigrations { get; }
|
||||
public virtual bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public abstract bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations);
|
||||
protected SemanticVersioning.Version? GetProfileVersion(JsonObject profile)
|
||||
{
|
||||
var versionString = profile["spt"]?["version"]?.GetValue<string>();
|
||||
|
||||
public virtual JsonObject? Migrate(JsonObject profile)
|
||||
if (versionString is null)
|
||||
{
|
||||
return profile;
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var versionNumber = versionString.Split(' ')[0];
|
||||
|
||||
protected SemanticVersioning.Version? GetProfileVersion(JsonObject profile)
|
||||
{
|
||||
var versionString = profile["spt"]?["version"]?.GetValue<string>();
|
||||
|
||||
if (versionString is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var versionNumber = versionString.Split(' ')[0];
|
||||
|
||||
return SemanticVersioning.Version.TryParse(versionNumber, out var version) ? version : null;
|
||||
}
|
||||
return SemanticVersioning.Version.TryParse(versionNumber, out var version) ? version : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration
|
||||
namespace SPTarkov.Server.Core.Migration;
|
||||
|
||||
public interface IProfileMigration
|
||||
{
|
||||
public interface IProfileMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows for adding checks if the profile in question can migrate
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to check</param>
|
||||
/// <param name="previouslyRanMigrations"></param>
|
||||
/// <returns>Returns true if the profile can migrate, returns false if not</returns>
|
||||
public bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations);
|
||||
/// <summary>
|
||||
/// Allows for adding checks if the profile in question can migrate
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to check</param>
|
||||
/// <param name="previouslyRanMigrations"></param>
|
||||
/// <returns>Returns true if the profile can migrate, returns false if not</returns>
|
||||
public bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations);
|
||||
|
||||
/// <summary>
|
||||
/// Migrate the profile, this should be used to handle and fix old data that has been removed from the <see cref="SptProfile"/> record
|
||||
/// or a general incompatibility due to different typing
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to migrate</param>
|
||||
/// <returns>Returns the migrated profile on success, or null if it failed</returns>
|
||||
public JsonObject? Migrate(JsonObject profile);
|
||||
/// <summary>
|
||||
/// Migrate the profile, this should be used to handle and fix old data that has been removed from the <see cref="SptProfile"/> record
|
||||
/// or a general incompatibility due to different typing
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to migrate</param>
|
||||
/// <returns>Returns the migrated profile on success, or null if it failed</returns>
|
||||
public JsonObject? Migrate(JsonObject profile);
|
||||
|
||||
/// <summary>
|
||||
/// Handles post migration of the profile, this can be used to fill new types with (old) data gotten from <see cref="Migrate"/>
|
||||
/// </summary>
|
||||
/// <returns>Should return true if successful, should return false if not</returns>
|
||||
public bool PostMigrate(SptProfile profile);
|
||||
}
|
||||
/// <summary>
|
||||
/// Handles post migration of the profile, this can be used to fill new types with (old) data gotten from <see cref="Migrate"/>
|
||||
/// </summary>
|
||||
/// <returns>Should return true if successful, should return false if not</returns>
|
||||
public bool PostMigrate(SptProfile profile);
|
||||
}
|
||||
|
||||
@@ -3,53 +3,52 @@ using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// In 0.16.1.3.35312 BSG changed this to from an int to a hex64 encoded value.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class HideoutSeed : AbstractProfileMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// In 0.16.1.3.35312 BSG changed this to from an int to a hex64 encoded value.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class HideoutSeed : AbstractProfileMigration
|
||||
public override string FromVersion
|
||||
{
|
||||
public override string FromVersion
|
||||
{
|
||||
get { return "~3.10"; }
|
||||
}
|
||||
get { return "~3.10"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "3.11"; }
|
||||
}
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "3.11"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "HideoutSeed311-SPTSharp"; }
|
||||
}
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "HideoutSeed311-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return []; }
|
||||
}
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return []; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
var profileVersionMatches = fromRange.IsSatisfied(profileVersion);
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
var profileVersionMatches = fromRange.IsSatisfied(profileVersion);
|
||||
|
||||
var seedNode = profile["characters"]?["pmc"]?["Hideout"]?["Seed"];
|
||||
var seedNode = profile["characters"]?["pmc"]?["Hideout"]?["Seed"];
|
||||
|
||||
// Check if the seed still has it's numeric value, this is not valid anymore
|
||||
var seedIsNumeric = seedNode is JsonValue seedValue && seedValue.TryGetValue<long>(out _);
|
||||
// Check if the seed still has it's numeric value, this is not valid anymore
|
||||
var seedIsNumeric = seedNode is JsonValue seedValue && seedValue.TryGetValue<long>(out _);
|
||||
|
||||
return profileVersionMatches && seedIsNumeric;
|
||||
}
|
||||
return profileVersionMatches && seedIsNumeric;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
profile["characters"]!["pmc"]!["Hideout"]!["Seed"] = Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(16));
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
profile["characters"]!["pmc"]!["Hideout"]!["Seed"] = Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(16));
|
||||
|
||||
return base.Migrate(profile);
|
||||
}
|
||||
return base.Migrate(profile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,53 +2,52 @@
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// In the minor versions of 3.10 or somewhere in between these properties were added, it's possible that a profile has not updated
|
||||
/// To these thus never having received them, re-add them here.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class ThreeTenMinorFixes : AbstractProfileMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// In the minor versions of 3.10 or somewhere in between these properties were added, it's possible that a profile has not updated
|
||||
/// To these thus never having received them, re-add them here.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class ThreeTenMinorFixes : AbstractProfileMigration
|
||||
public override string FromVersion
|
||||
{
|
||||
public override string FromVersion
|
||||
get { return "~3.10"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "3.11"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "ThreeTenMinorFixes-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return [typeof(HideoutSeed)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var cultistRewardsMissing = profile["spt"]?["cultistRewards"] == null;
|
||||
var friendProfileIdsMissing = profile["friends"] == null;
|
||||
|
||||
return cultistRewardsMissing || friendProfileIdsMissing;
|
||||
}
|
||||
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
if (profile.SptData!.CultistRewards == null)
|
||||
{
|
||||
get { return "~3.10"; }
|
||||
profile.SptData.CultistRewards = [];
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "3.11"; }
|
||||
}
|
||||
profile.FriendProfileIds ??= [];
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "ThreeTenMinorFixes-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return [typeof(HideoutSeed)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var cultistRewardsMissing = profile["spt"]?["cultistRewards"] == null;
|
||||
var friendProfileIdsMissing = profile["friends"] == null;
|
||||
|
||||
return cultistRewardsMissing || friendProfileIdsMissing;
|
||||
}
|
||||
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
if (profile.SptData!.CultistRewards == null)
|
||||
{
|
||||
profile.SptData.CultistRewards = [];
|
||||
}
|
||||
|
||||
profile.FriendProfileIds ??= [];
|
||||
|
||||
return base.PostMigrate(profile);
|
||||
}
|
||||
return base.PostMigrate(profile);
|
||||
}
|
||||
}
|
||||
|
||||
+170
-179
@@ -8,224 +8,215 @@ using SPTarkov.Server.Core.Models.Enums;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations;
|
||||
|
||||
[Injectable]
|
||||
public class ThreeTenToThreeEleven(
|
||||
DatabaseService databaseService,
|
||||
// Yes, referencing the helpers directly causes a circular dependency. Too bad!
|
||||
IServiceProvider serviceProvider
|
||||
) : AbstractProfileMigration
|
||||
{
|
||||
[Injectable]
|
||||
public class ThreeTenToThreeEleven(
|
||||
DatabaseService databaseService,
|
||||
// Yes, referencing the helpers directly causes a circular dependency. Too bad!
|
||||
IServiceProvider serviceProvider
|
||||
) : AbstractProfileMigration
|
||||
private List<string> _oldSuiteData = [];
|
||||
|
||||
public override string FromVersion
|
||||
{
|
||||
private List<string> _oldSuiteData = [];
|
||||
get { return "~3.10"; }
|
||||
}
|
||||
|
||||
public override string FromVersion
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "3.11"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "310x-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return [typeof(HideoutSeed)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
|
||||
var versionMatches = fromRange.IsSatisfied(profileVersion);
|
||||
|
||||
return versionMatches;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (profile["suits"] is JsonArray suitsArray)
|
||||
{
|
||||
get { return "~3.10"; }
|
||||
_oldSuiteData = suitsArray.Select(node => node?.GetValue<string>()).Where(suit => suit != null).ToList()!;
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
profile.Remove("suits");
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
if (profile.CustomisationUnlocks is null)
|
||||
{
|
||||
get { return "3.11"; }
|
||||
profile.CustomisationUnlocks = [];
|
||||
profile.AddCustomisationUnlocksToProfile();
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
profile.CharacterData.PmcData.Prestige ??= [];
|
||||
|
||||
if (profile.CharacterData.PmcData.Inventory.HideoutCustomizationStashId is null)
|
||||
{
|
||||
get { return "310x-SPTSharp"; }
|
||||
profile.CharacterData.PmcData.Inventory.HideoutCustomizationStashId = new("676db384777490e23c45b657");
|
||||
|
||||
//Directly injecting CreateProfileService causes a circular dependency which I can't be bothered to fix just for this
|
||||
(serviceProvider.GetService(typeof(CreateProfileService)) as CreateProfileService)!.AddMissingInternalContainersToProfile(
|
||||
profile.CharacterData.PmcData
|
||||
);
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
if (profile.CharacterData.PmcData.Hideout.Customization is null)
|
||||
{
|
||||
get { return [typeof(HideoutSeed)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
|
||||
var versionMatches = fromRange.IsSatisfied(profileVersion);
|
||||
|
||||
return versionMatches;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (profile["suits"] is JsonArray suitsArray)
|
||||
profile.CharacterData.PmcData.Hideout.Customization = new Dictionary<string, Models.Common.MongoId>
|
||||
{
|
||||
_oldSuiteData = suitsArray.Select(node => node?.GetValue<string>()).Where(suit => suit != null).ToList()!;
|
||||
}
|
||||
|
||||
profile.Remove("suits");
|
||||
|
||||
return profile;
|
||||
{ "Wall", new("675844bdf94a97cbbe096f1a") },
|
||||
{ "Floor", new("6758443ff94a97cbbe096f18") },
|
||||
{ "Light", new("675fe8abbc3deae49a0b947f") },
|
||||
{ "Ceiling", new("673b3f977038192ee006aa09") },
|
||||
{ "ShootingRangeMark", new("67585d416c72998cf60ed85a") },
|
||||
};
|
||||
}
|
||||
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
if (profile.CharacterData.PmcData.Info.Side == "Bear")
|
||||
{
|
||||
if (profile.CustomisationUnlocks is null)
|
||||
ProcessBearProfile(profile);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.Side == "Usec")
|
||||
{
|
||||
ProcessUsecProfile(profile);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Achievements.Count > 0)
|
||||
{
|
||||
var achievementsDb = databaseService.GetTemplates().Achievements;
|
||||
|
||||
foreach (var achievementId in profile.CharacterData.PmcData.Achievements.Keys)
|
||||
{
|
||||
profile.CustomisationUnlocks = [];
|
||||
profile.AddCustomisationUnlocksToProfile();
|
||||
}
|
||||
var achievementDb = achievementsDb.FirstOrDefault(a => a.Id == achievementId);
|
||||
var rewards = achievementDb?.Rewards;
|
||||
|
||||
profile.CharacterData.PmcData.Prestige ??= [];
|
||||
if (rewards == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Inventory.HideoutCustomizationStashId is null)
|
||||
{
|
||||
profile.CharacterData.PmcData.Inventory.HideoutCustomizationStashId = new("676db384777490e23c45b657");
|
||||
// Only hand out the new hideout customization rewards.
|
||||
var filteredRewards = rewards.Where(r => r.Type == RewardType.CustomizationDirect).ToList();
|
||||
|
||||
//Directly injecting CreateProfileService causes a circular dependency which I can't be bothered to fix just for this
|
||||
(serviceProvider.GetService(typeof(CreateProfileService)) as CreateProfileService)!.AddMissingInternalContainersToProfile(
|
||||
profile.CharacterData.PmcData
|
||||
//Directly injecting RewardHelper causes a circular dependency which I can't be bothered to fix just for this
|
||||
(serviceProvider.GetService(typeof(RewardHelper)) as RewardHelper)!.ApplyRewards(
|
||||
filteredRewards,
|
||||
CustomisationSource.ACHIEVEMENT,
|
||||
profile,
|
||||
profile.CharacterData.PmcData,
|
||||
achievementId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Hideout.Customization is null)
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ProcessBearProfile(SptProfile profile)
|
||||
{
|
||||
// Reset clothing customization back to default as customization changed in 3.11
|
||||
profile.CharacterData.PmcData.Customization.Body = new("5cc0858d14c02e000c6bea66");
|
||||
profile.CharacterData.PmcData.Customization.Feet = new("5cc085bb14c02e000e67a5c5");
|
||||
profile.CharacterData.PmcData.Customization.Hands = new("5cc0876314c02e000c6bea6b");
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("674731c8bafff850080488bb");
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "edge_of_darkness")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("6746fd09bafff85008048838");
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "unheard_edition")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("67471928d17d6431550563b5");
|
||||
}
|
||||
|
||||
foreach (var oldSuite in _oldSuiteData)
|
||||
{
|
||||
// Default Bear clothing, dont need to add this
|
||||
if (oldSuite == "5cd946231388ce000d572fe3" || oldSuite == "5cd945d71388ce000a659dfb" || oldSuite == "666841a02537107dc508b704")
|
||||
{
|
||||
profile.CharacterData.PmcData.Hideout.Customization = new Dictionary<string, Models.Common.MongoId>
|
||||
continue;
|
||||
}
|
||||
|
||||
var trader = databaseService.GetTrader("5ac3b934156ae10c4430e83c");
|
||||
var traderClothing = trader?.Suits?.FirstOrDefault(s => s.SuiteId == oldSuite);
|
||||
|
||||
if (traderClothing != null)
|
||||
{
|
||||
var clothingToAdd = new CustomisationStorage
|
||||
{
|
||||
{ "Wall", new("675844bdf94a97cbbe096f1a") },
|
||||
{ "Floor", new("6758443ff94a97cbbe096f18") },
|
||||
{ "Light", new("675fe8abbc3deae49a0b947f") },
|
||||
{ "Ceiling", new("673b3f977038192ee006aa09") },
|
||||
{ "ShootingRangeMark", new("67585d416c72998cf60ed85a") },
|
||||
Id = traderClothing.SuiteId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
};
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.Side == "Bear")
|
||||
{
|
||||
ProcessBearProfile(profile);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.Side == "Usec")
|
||||
{
|
||||
ProcessUsecProfile(profile);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Achievements.Count > 0)
|
||||
{
|
||||
var achievementsDb = databaseService.GetTemplates().Achievements;
|
||||
|
||||
foreach (var achievementId in profile.CharacterData.PmcData.Achievements.Keys)
|
||||
{
|
||||
var achievementDb = achievementsDb.FirstOrDefault(a => a.Id == achievementId);
|
||||
var rewards = achievementDb?.Rewards;
|
||||
|
||||
if (rewards == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only hand out the new hideout customization rewards.
|
||||
var filteredRewards = rewards.Where(r => r.Type == RewardType.CustomizationDirect).ToList();
|
||||
|
||||
//Directly injecting RewardHelper causes a circular dependency which I can't be bothered to fix just for this
|
||||
(serviceProvider.GetService(typeof(RewardHelper)) as RewardHelper)!.ApplyRewards(
|
||||
filteredRewards,
|
||||
CustomisationSource.ACHIEVEMENT,
|
||||
profile,
|
||||
profile.CharacterData.PmcData,
|
||||
achievementId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ProcessBearProfile(SptProfile profile)
|
||||
{
|
||||
// Reset clothing customization back to default as customization changed in 3.11
|
||||
profile.CharacterData.PmcData.Customization.Body = new("5cc0858d14c02e000c6bea66");
|
||||
profile.CharacterData.PmcData.Customization.Feet = new("5cc085bb14c02e000e67a5c5");
|
||||
profile.CharacterData.PmcData.Customization.Hands = new("5cc0876314c02e000c6bea6b");
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("674731c8bafff850080488bb");
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "edge_of_darkness")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("6746fd09bafff85008048838");
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "unheard_edition")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("67471928d17d6431550563b5");
|
||||
}
|
||||
|
||||
foreach (var oldSuite in _oldSuiteData)
|
||||
{
|
||||
// Default Bear clothing, dont need to add this
|
||||
if (
|
||||
oldSuite == "5cd946231388ce000d572fe3"
|
||||
|| oldSuite == "5cd945d71388ce000a659dfb"
|
||||
|| oldSuite == "666841a02537107dc508b704"
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var trader = databaseService.GetTrader("5ac3b934156ae10c4430e83c");
|
||||
var traderClothing = trader?.Suits?.FirstOrDefault(s => s.SuiteId == oldSuite);
|
||||
|
||||
if (traderClothing != null)
|
||||
{
|
||||
var clothingToAdd = new CustomisationStorage
|
||||
{
|
||||
Id = traderClothing.SuiteId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
};
|
||||
|
||||
profile.CustomisationUnlocks.Add(clothingToAdd);
|
||||
}
|
||||
profile.CustomisationUnlocks.Add(clothingToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessUsecProfile(SptProfile profile)
|
||||
private void ProcessUsecProfile(SptProfile profile)
|
||||
{
|
||||
// Reset clothing customization back to default as customization changed in 3.11
|
||||
profile.CharacterData.PmcData.Customization.Body = new("5cde95d97d6c8b647a3769b0"); //Usec default clothing
|
||||
profile.CharacterData.PmcData.Customization.Feet = new("5cde95ef7d6c8b04713c4f2d");
|
||||
profile.CharacterData.PmcData.Customization.Hands = new("5cde95fa7d6c8b04737c2d13");
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("674731d1170146228c0d222a"); //Usec base dogtag
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "edge_of_darkness")
|
||||
{
|
||||
// Reset clothing customization back to default as customization changed in 3.11
|
||||
profile.CharacterData.PmcData.Customization.Body = new("5cde95d97d6c8b647a3769b0"); //Usec default clothing
|
||||
profile.CharacterData.PmcData.Customization.Feet = new("5cde95ef7d6c8b04713c4f2d");
|
||||
profile.CharacterData.PmcData.Customization.Hands = new("5cde95fa7d6c8b04737c2d13");
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("674731d1170146228c0d222a"); //Usec base dogtag
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("67471938bafff850080488b7");
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "edge_of_darkness")
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "unheard_edition")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("6747193f170146228c0d2226");
|
||||
}
|
||||
|
||||
foreach (var oldSuite in _oldSuiteData)
|
||||
{
|
||||
// Default Usec clothing, dont need to add this
|
||||
if (oldSuite == "5cde9ec17d6c8b04723cf479" || oldSuite == "5cde9e957d6c8b0474535da7" || oldSuite == "666841a02537107dc508b704")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("67471938bafff850080488b7");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "unheard_edition")
|
||||
var trader = databaseService.GetTrader("5ac3b934156ae10c4430e83c");
|
||||
var traderClothing = trader?.Suits?.FirstOrDefault(s => s.SuiteId == oldSuite);
|
||||
|
||||
if (traderClothing != null)
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("6747193f170146228c0d2226");
|
||||
}
|
||||
|
||||
foreach (var oldSuite in _oldSuiteData)
|
||||
{
|
||||
// Default Usec clothing, dont need to add this
|
||||
if (
|
||||
oldSuite == "5cde9ec17d6c8b04723cf479"
|
||||
|| oldSuite == "5cde9e957d6c8b0474535da7"
|
||||
|| oldSuite == "666841a02537107dc508b704"
|
||||
)
|
||||
var clothingToAdd = new CustomisationStorage
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Id = traderClothing.SuiteId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
};
|
||||
|
||||
var trader = databaseService.GetTrader("5ac3b934156ae10c4430e83c");
|
||||
var traderClothing = trader?.Suits?.FirstOrDefault(s => s.SuiteId == oldSuite);
|
||||
|
||||
if (traderClothing != null)
|
||||
{
|
||||
var clothingToAdd = new CustomisationStorage
|
||||
{
|
||||
Id = traderClothing.SuiteId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
};
|
||||
|
||||
profile.CustomisationUnlocks.Add(clothingToAdd);
|
||||
}
|
||||
profile.CustomisationUnlocks.Add(clothingToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,84 +2,83 @@
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// In 16.8.0.37972 BSG added customization for voices, technically this only affects BE profiles, but this should fix these.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class TheVoices(DatabaseService databaseService) : AbstractProfileMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// In 16.8.0.37972 BSG added customization for voices, technically this only affects BE profiles, but this should fix these.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class TheVoices(DatabaseService databaseService) : AbstractProfileMigration
|
||||
private bool _pmcVoiceIsMissing = false;
|
||||
private bool _scavVoiceIsMissing = false;
|
||||
|
||||
public override string FromVersion
|
||||
{
|
||||
private bool _pmcVoiceIsMissing = false;
|
||||
private bool _scavVoiceIsMissing = false;
|
||||
get { return "~4.0"; }
|
||||
}
|
||||
|
||||
public override string FromVersion
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "~4.0"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "TheVoices400"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
// Requires ThreeTenToThreeEleven on legacy profiles, due to that adding the customization object for the first time
|
||||
get { return [typeof(ThreeTenToThreeEleven)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
_pmcVoiceIsMissing = profile["characters"]?["pmc"]?["Customization"]?["Voice"] == null;
|
||||
|
||||
_scavVoiceIsMissing = profile["characters"]?["scav"]?["Customization"]?["Voice"] == null;
|
||||
|
||||
return _pmcVoiceIsMissing || _scavVoiceIsMissing;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (_pmcVoiceIsMissing)
|
||||
{
|
||||
get { return "~4.0"; }
|
||||
HandlePmcVoice(profile);
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
if (_scavVoiceIsMissing)
|
||||
{
|
||||
get { return "~4.0"; }
|
||||
HandleScavVoice(profile);
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "TheVoices400"; }
|
||||
}
|
||||
return base.Migrate(profile);
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
// Requires ThreeTenToThreeEleven on legacy profiles, due to that adding the customization object for the first time
|
||||
get { return [typeof(ThreeTenToThreeEleven)]; }
|
||||
}
|
||||
private void HandlePmcVoice(JsonObject profileObject)
|
||||
{
|
||||
var pmcInfo = profileObject["characters"]!["pmc"]!["Info"] as JsonObject;
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
_pmcVoiceIsMissing = profile["characters"]?["pmc"]?["Customization"]?["Voice"] == null;
|
||||
var oldVoice = pmcInfo["Voice"]?.ToString() ?? "";
|
||||
pmcInfo.Remove("Voice");
|
||||
|
||||
_scavVoiceIsMissing = profile["characters"]?["scav"]?["Customization"]?["Voice"] == null;
|
||||
var voiceMongoId = databaseService.GetCustomization().FirstOrDefault(x => x.Value.Properties.Name == oldVoice).Key;
|
||||
|
||||
return _pmcVoiceIsMissing || _scavVoiceIsMissing;
|
||||
}
|
||||
profileObject["characters"]!["pmc"]!["Customization"]!["Voice"] = voiceMongoId.ToString();
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (_pmcVoiceIsMissing)
|
||||
{
|
||||
HandlePmcVoice(profile);
|
||||
}
|
||||
private void HandleScavVoice(JsonObject profileObject)
|
||||
{
|
||||
var pmcInfo = profileObject["characters"]!["scav"]!["Info"] as JsonObject;
|
||||
|
||||
if (_scavVoiceIsMissing)
|
||||
{
|
||||
HandleScavVoice(profile);
|
||||
}
|
||||
var oldVoice = pmcInfo["Voice"]?.ToString() ?? "";
|
||||
pmcInfo.Remove("Voice");
|
||||
|
||||
return base.Migrate(profile);
|
||||
}
|
||||
var voiceMongoId = databaseService.GetCustomization().FirstOrDefault(x => x.Value.Properties.Name == oldVoice).Key;
|
||||
|
||||
private void HandlePmcVoice(JsonObject profileObject)
|
||||
{
|
||||
var pmcInfo = profileObject["characters"]!["pmc"]!["Info"] as JsonObject;
|
||||
|
||||
var oldVoice = pmcInfo["Voice"]?.ToString() ?? "";
|
||||
pmcInfo.Remove("Voice");
|
||||
|
||||
var voiceMongoId = databaseService.GetCustomization().FirstOrDefault(x => x.Value.Properties.Name == oldVoice).Key;
|
||||
|
||||
profileObject["characters"]!["pmc"]!["Customization"]!["Voice"] = voiceMongoId.ToString();
|
||||
}
|
||||
|
||||
private void HandleScavVoice(JsonObject profileObject)
|
||||
{
|
||||
var pmcInfo = profileObject["characters"]!["scav"]!["Info"] as JsonObject;
|
||||
|
||||
var oldVoice = pmcInfo["Voice"]?.ToString() ?? "";
|
||||
pmcInfo.Remove("Voice");
|
||||
|
||||
var voiceMongoId = databaseService.GetCustomization().FirstOrDefault(x => x.Value.Properties.Name == oldVoice).Key;
|
||||
|
||||
profileObject["characters"]!["scav"]!["Customization"]!["Voice"] = voiceMongoId.ToString();
|
||||
}
|
||||
profileObject["characters"]!["scav"]!["Customization"]!["Voice"] = voiceMongoId.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,72 +4,71 @@ using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations;
|
||||
|
||||
[Injectable]
|
||||
public class ThreeElevenToFourZero(Watermark watermark) : AbstractProfileMigration
|
||||
{
|
||||
[Injectable]
|
||||
public class ThreeElevenToFourZero(Watermark watermark) : AbstractProfileMigration
|
||||
public override string FromVersion
|
||||
{
|
||||
public override string FromVersion
|
||||
get { return "~3.11"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "4.0"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "311x-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return [typeof(ThreeTenToThreeEleven)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
|
||||
var versionMatches =
|
||||
fromRange.IsSatisfied(profileVersion)
|
||||
|| PrerequisiteMigrations.All(prereq => previouslyRanMigrations.Any(r => r.GetType() == prereq));
|
||||
|
||||
return versionMatches;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (profile["characters"]!["pmc"]!["Hideout"]!["Production"] is JsonObject production)
|
||||
{
|
||||
get { return "~3.11"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "4.0"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "311x-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return [typeof(ThreeTenToThreeEleven)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
|
||||
var versionMatches =
|
||||
fromRange.IsSatisfied(profileVersion)
|
||||
|| PrerequisiteMigrations.All(prereq => previouslyRanMigrations.Any(r => r.GetType() == prereq));
|
||||
|
||||
return versionMatches;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (profile["characters"]!["pmc"]!["Hideout"]!["Production"] is JsonObject production)
|
||||
foreach (var entry in production)
|
||||
{
|
||||
foreach (var entry in production)
|
||||
if (
|
||||
entry.Value is JsonObject productionEntry
|
||||
&& productionEntry["StartTimestamp"] is JsonValue startTimestampValue
|
||||
&& startTimestampValue.TryGetValue<string>(out var startTimestampStr)
|
||||
&& long.TryParse(startTimestampStr, out var startTimestampInt)
|
||||
)
|
||||
{
|
||||
if (
|
||||
entry.Value is JsonObject productionEntry
|
||||
&& productionEntry["StartTimestamp"] is JsonValue startTimestampValue
|
||||
&& startTimestampValue.TryGetValue<string>(out var startTimestampStr)
|
||||
&& long.TryParse(startTimestampStr, out var startTimestampInt)
|
||||
)
|
||||
{
|
||||
productionEntry["StartTimestamp"] = startTimestampInt;
|
||||
}
|
||||
productionEntry["StartTimestamp"] = startTimestampInt;
|
||||
}
|
||||
}
|
||||
|
||||
return base.Migrate(profile);
|
||||
}
|
||||
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
profile.SptData.ExtraRepeatableQuests = [];
|
||||
return base.Migrate(profile);
|
||||
}
|
||||
|
||||
profile.SptData.Version = $"{watermark.GetVersionTag()} (Migrated from 3.11)";
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
profile.SptData.ExtraRepeatableQuests = [];
|
||||
|
||||
return base.PostMigrate(profile);
|
||||
}
|
||||
profile.SptData.Version = $"{watermark.GetVersionTag()} (Migrated from 3.11)";
|
||||
|
||||
return base.PostMigrate(profile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPTarkov.Server.Core.Models.Eft.Ws
|
||||
{
|
||||
public record WsRagfairNewRating : WsNotificationEvent
|
||||
{
|
||||
[JsonPropertyName("rating")]
|
||||
public double? Rating { get; set; }
|
||||
namespace SPTarkov.Server.Core.Models.Eft.Ws;
|
||||
|
||||
[JsonPropertyName("isRatingGrowing")]
|
||||
public bool? IsRatingGrowing { get; set; }
|
||||
}
|
||||
public record WsRagfairNewRating : WsNotificationEvent
|
||||
{
|
||||
[JsonPropertyName("rating")]
|
||||
public double? Rating { get; set; }
|
||||
|
||||
[JsonPropertyName("isRatingGrowing")]
|
||||
public bool? IsRatingGrowing { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
namespace SPTarkov.Server.Core.Models.Enums.Hideout
|
||||
namespace SPTarkov.Server.Core.Models.Enums.Hideout;
|
||||
|
||||
public enum HideoutNotificationType
|
||||
{
|
||||
public enum HideoutNotificationType
|
||||
{
|
||||
None,
|
||||
FuelIsLow,
|
||||
NoFuel,
|
||||
ReadyToConstruct,
|
||||
ReadyToUpgrade,
|
||||
ReadyToInstall,
|
||||
ItemReady,
|
||||
ItemCollected,
|
||||
RepairComplete,
|
||||
ScavCaseReady,
|
||||
DecryptionComplete,
|
||||
}
|
||||
None,
|
||||
FuelIsLow,
|
||||
NoFuel,
|
||||
ReadyToConstruct,
|
||||
ReadyToUpgrade,
|
||||
ReadyToInstall,
|
||||
ItemReady,
|
||||
ItemCollected,
|
||||
RepairComplete,
|
||||
ScavCaseReady,
|
||||
DecryptionComplete,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPTarkov.Server.Core.Models.Enums
|
||||
namespace SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SkillClass
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SkillClass
|
||||
{
|
||||
Physical,
|
||||
Combat,
|
||||
Special,
|
||||
Practical,
|
||||
Mental,
|
||||
}
|
||||
Physical,
|
||||
Combat,
|
||||
Special,
|
||||
Practical,
|
||||
Mental,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Launcher
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Launcher;
|
||||
|
||||
public enum NicknameValidationResult
|
||||
{
|
||||
public enum NicknameValidationResult
|
||||
{
|
||||
Taken,
|
||||
Short,
|
||||
Valid,
|
||||
}
|
||||
Taken,
|
||||
Short,
|
||||
Valid,
|
||||
}
|
||||
|
||||
@@ -2,48 +2,47 @@
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Ragfair
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Ragfair;
|
||||
|
||||
public record CreateFleaOfferDetails
|
||||
{
|
||||
public record CreateFleaOfferDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Owner of the offer
|
||||
/// </summary>
|
||||
public MongoId UserId { get; set; }
|
||||
/// <summary>
|
||||
/// Owner of the offer
|
||||
/// </summary>
|
||||
public MongoId UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time offer is listed at
|
||||
/// </summary>
|
||||
public long Time { get; set; }
|
||||
/// <summary>
|
||||
/// Time offer is listed at
|
||||
/// </summary>
|
||||
public long Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Items in the offer
|
||||
/// </summary>
|
||||
public List<Item> Items { get; set; }
|
||||
/// <summary>
|
||||
/// Items in the offer
|
||||
/// </summary>
|
||||
public List<Item> Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cost of item (currency or barter)
|
||||
/// </summary>
|
||||
public List<BarterScheme> BarterScheme { get; set; }
|
||||
/// <summary>
|
||||
/// Cost of item (currency or barter)
|
||||
/// </summary>
|
||||
public List<BarterScheme> BarterScheme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loyalty level needed to buy item
|
||||
/// </summary>
|
||||
public int LoyalLevel { get; set; }
|
||||
/// <summary>
|
||||
/// Loyalty level needed to buy item
|
||||
/// </summary>
|
||||
public int LoyalLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount of item being listed
|
||||
/// </summary>
|
||||
public int Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Amount of item being listed
|
||||
/// </summary>
|
||||
public int Quantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Who created the offer
|
||||
/// </summary>
|
||||
public OfferCreator Creator { get; set; }
|
||||
/// <summary>
|
||||
/// Who created the offer
|
||||
/// </summary>
|
||||
public OfferCreator Creator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offer should be sold all in one offer
|
||||
/// </summary>
|
||||
public bool SellInOnePiece { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Offer should be sold all in one offer
|
||||
/// </summary>
|
||||
public bool SellInOnePiece { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,32 +2,31 @@ using System.Text.Json.Serialization;
|
||||
using SPTarkov.Server.Core.Models.Eft.Match;
|
||||
using SPTarkov.Server.Core.Models.Spt.Location;
|
||||
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Services
|
||||
namespace SPTarkov.Server.Core.Models.Spt.Services;
|
||||
|
||||
public class ProfileActivityData
|
||||
{
|
||||
public class ProfileActivityData
|
||||
{
|
||||
public long ClientStartedTimestamp { get; set; }
|
||||
public long LastActive { get; set; }
|
||||
public ProfileActivityRaidData? RaidData { get; set; } = null;
|
||||
public IReadOnlyList<ProfileActiveClientMods> ActiveClientMods { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ProfileActivityRaidData
|
||||
{
|
||||
public GetRaidConfigurationRequestData? RaidConfiguration { get; set; } = null;
|
||||
public RaidChanges? RaidAdjustments { get; set; } = null;
|
||||
public LocationTransit? LocationTransit { get; set; } = null;
|
||||
}
|
||||
|
||||
public record ProfileActiveClientMods
|
||||
{
|
||||
[JsonPropertyName("modName")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("modGUID")]
|
||||
public required string GUID { get; init; }
|
||||
|
||||
[JsonPropertyName("modVersion")]
|
||||
public required Version Version { get; init; }
|
||||
}
|
||||
public long ClientStartedTimestamp { get; set; }
|
||||
public long LastActive { get; set; }
|
||||
public ProfileActivityRaidData? RaidData { get; set; } = null;
|
||||
public IReadOnlyList<ProfileActiveClientMods> ActiveClientMods { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ProfileActivityRaidData
|
||||
{
|
||||
public GetRaidConfigurationRequestData? RaidConfiguration { get; set; } = null;
|
||||
public RaidChanges? RaidAdjustments { get; set; } = null;
|
||||
public LocationTransit? LocationTransit { get; set; } = null;
|
||||
}
|
||||
|
||||
public record ProfileActiveClientMods
|
||||
{
|
||||
[JsonPropertyName("modName")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("modGUID")]
|
||||
public required string GUID { get; init; }
|
||||
|
||||
[JsonPropertyName("modVersion")]
|
||||
public required Version Version { get; init; }
|
||||
}
|
||||
|
||||
@@ -6,132 +6,131 @@ using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
|
||||
namespace SPTarkov.Server.Core.Services
|
||||
namespace SPTarkov.Server.Core.Services;
|
||||
|
||||
[Injectable(InjectionType.Singleton)]
|
||||
public class ProfileMigratorService(
|
||||
IEnumerable<IProfileMigration> profileMigrations,
|
||||
TimeUtil timeUtil,
|
||||
ISptLogger<ProfileMigratorService> logger
|
||||
)
|
||||
{
|
||||
[Injectable(InjectionType.Singleton)]
|
||||
public class ProfileMigratorService(
|
||||
IEnumerable<IProfileMigration> profileMigrations,
|
||||
TimeUtil timeUtil,
|
||||
ISptLogger<ProfileMigratorService> logger
|
||||
)
|
||||
private IEnumerable<AbstractProfileMigration> _sortedMigrations = [];
|
||||
|
||||
public SptProfile HandlePendingMigrations(JsonObject profile)
|
||||
{
|
||||
private IEnumerable<AbstractProfileMigration> _sortedMigrations = [];
|
||||
|
||||
public SptProfile HandlePendingMigrations(JsonObject profile)
|
||||
// On the initial run, begin sorting our migrations
|
||||
// This will sort it so that any non-prerequisite migrations go first
|
||||
// And then all the prerequisite ones.
|
||||
if (!_sortedMigrations.Any())
|
||||
{
|
||||
// On the initial run, begin sorting our migrations
|
||||
// This will sort it so that any non-prerequisite migrations go first
|
||||
// And then all the prerequisite ones.
|
||||
if (!_sortedMigrations.Any())
|
||||
{
|
||||
_sortedMigrations = SortMigrations();
|
||||
}
|
||||
|
||||
var profileId = profile["info"]?["id"]?.GetValue<string>();
|
||||
|
||||
// Profile is due for a wipe or a reset, do not continue here.
|
||||
if (
|
||||
profile["characters"]?["pmc"]?["Info"] == null
|
||||
|| profile["characters"]?["scav"]?["Info"] == null
|
||||
|| (profile["info"]?["wipe"]?.GetValue<bool>() == true)
|
||||
)
|
||||
{
|
||||
return profile.Deserialize<SptProfile>(JsonUtil.JsonSerializerOptionsNoIndent)
|
||||
?? throw new InvalidOperationException($"Could not deserialize the profile {profileId}");
|
||||
}
|
||||
|
||||
var ranMigrations = new List<AbstractProfileMigration>();
|
||||
|
||||
foreach (var profileMigration in _sortedMigrations)
|
||||
{
|
||||
if (profileMigration.CanMigrate(profile, ranMigrations))
|
||||
{
|
||||
logger.Warning($"{profileId} has a pending profile migration: {profileMigration.MigrationName}");
|
||||
|
||||
var migratedProfile = profileMigration.Migrate(profile);
|
||||
|
||||
if (migratedProfile is not null)
|
||||
{
|
||||
profile = migratedProfile;
|
||||
|
||||
ranMigrations.Add(profileMigration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sptReadyProfile =
|
||||
profile.Deserialize<SptProfile>(JsonUtil.JsonSerializerOptionsNoIndent)
|
||||
?? throw new InvalidOperationException($"Could not deserialize the profile {profileId}");
|
||||
|
||||
foreach (var ranMigration in ranMigrations)
|
||||
{
|
||||
if (ranMigration.PostMigrate(sptReadyProfile))
|
||||
{
|
||||
logger.Success($"{profileId} successfully ran profile migration: {ranMigration.MigrationName}");
|
||||
|
||||
if (sptReadyProfile.SptData!.Migrations is null)
|
||||
{
|
||||
sptReadyProfile.SptData.Migrations = [];
|
||||
}
|
||||
|
||||
sptReadyProfile.SptData.Migrations.Add(ranMigration.MigrationName, timeUtil.GetTimeStamp());
|
||||
}
|
||||
}
|
||||
|
||||
return sptReadyProfile;
|
||||
_sortedMigrations = SortMigrations();
|
||||
}
|
||||
|
||||
protected IEnumerable<AbstractProfileMigration> SortMigrations()
|
||||
{
|
||||
var sortedMigrations = new List<AbstractProfileMigration>();
|
||||
var visitedMigrations = new Dictionary<Type, bool>();
|
||||
var migrationDict = profileMigrations.Cast<AbstractProfileMigration>().ToDictionary(m => m.GetType());
|
||||
var profileId = profile["info"]?["id"]?.GetValue<string>();
|
||||
|
||||
foreach (var migration in profileMigrations.Cast<AbstractProfileMigration>())
|
||||
{
|
||||
VisitMigrationForSort(migration, migrationDict, visitedMigrations, sortedMigrations);
|
||||
}
|
||||
|
||||
return sortedMigrations;
|
||||
}
|
||||
|
||||
protected void VisitMigrationForSort(
|
||||
AbstractProfileMigration migration,
|
||||
Dictionary<Type, AbstractProfileMigration> migrationTypeDictionary,
|
||||
Dictionary<Type, bool> visitedTypeDictionary,
|
||||
List<AbstractProfileMigration> sortedMigrations
|
||||
// Profile is due for a wipe or a reset, do not continue here.
|
||||
if (
|
||||
profile["characters"]?["pmc"]?["Info"] == null
|
||||
|| profile["characters"]?["scav"]?["Info"] == null
|
||||
|| (profile["info"]?["wipe"]?.GetValue<bool>() == true)
|
||||
)
|
||||
{
|
||||
var migrationType = migration.GetType();
|
||||
|
||||
if (visitedTypeDictionary.TryGetValue(migrationType, out var isVisited))
|
||||
{
|
||||
if (isVisited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Big error, two migrations should never depend on one another
|
||||
throw new InvalidOperationException($"Cycle detected in migration prerequisites involving: {migrationType.Name}");
|
||||
}
|
||||
|
||||
// Mark the current migration type for visiting
|
||||
visitedTypeDictionary[migrationType] = false;
|
||||
|
||||
foreach (var prerequisiteType in migration.PrerequisiteMigrations)
|
||||
{
|
||||
if (!migrationTypeDictionary.TryGetValue(prerequisiteType, out var prereqMigration))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Visit the next prerequisite
|
||||
VisitMigrationForSort(prereqMigration, migrationTypeDictionary, visitedTypeDictionary, sortedMigrations);
|
||||
}
|
||||
|
||||
// Done visiting, mark it as fully visited and add it to the sorted migrations
|
||||
visitedTypeDictionary[migrationType] = true;
|
||||
sortedMigrations.Add(migration);
|
||||
return profile.Deserialize<SptProfile>(JsonUtil.JsonSerializerOptionsNoIndent)
|
||||
?? throw new InvalidOperationException($"Could not deserialize the profile {profileId}");
|
||||
}
|
||||
|
||||
var ranMigrations = new List<AbstractProfileMigration>();
|
||||
|
||||
foreach (var profileMigration in _sortedMigrations)
|
||||
{
|
||||
if (profileMigration.CanMigrate(profile, ranMigrations))
|
||||
{
|
||||
logger.Warning($"{profileId} has a pending profile migration: {profileMigration.MigrationName}");
|
||||
|
||||
var migratedProfile = profileMigration.Migrate(profile);
|
||||
|
||||
if (migratedProfile is not null)
|
||||
{
|
||||
profile = migratedProfile;
|
||||
|
||||
ranMigrations.Add(profileMigration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sptReadyProfile =
|
||||
profile.Deserialize<SptProfile>(JsonUtil.JsonSerializerOptionsNoIndent)
|
||||
?? throw new InvalidOperationException($"Could not deserialize the profile {profileId}");
|
||||
|
||||
foreach (var ranMigration in ranMigrations)
|
||||
{
|
||||
if (ranMigration.PostMigrate(sptReadyProfile))
|
||||
{
|
||||
logger.Success($"{profileId} successfully ran profile migration: {ranMigration.MigrationName}");
|
||||
|
||||
if (sptReadyProfile.SptData!.Migrations is null)
|
||||
{
|
||||
sptReadyProfile.SptData.Migrations = [];
|
||||
}
|
||||
|
||||
sptReadyProfile.SptData.Migrations.Add(ranMigration.MigrationName, timeUtil.GetTimeStamp());
|
||||
}
|
||||
}
|
||||
|
||||
return sptReadyProfile;
|
||||
}
|
||||
|
||||
protected IEnumerable<AbstractProfileMigration> SortMigrations()
|
||||
{
|
||||
var sortedMigrations = new List<AbstractProfileMigration>();
|
||||
var visitedMigrations = new Dictionary<Type, bool>();
|
||||
var migrationDict = profileMigrations.Cast<AbstractProfileMigration>().ToDictionary(m => m.GetType());
|
||||
|
||||
foreach (var migration in profileMigrations.Cast<AbstractProfileMigration>())
|
||||
{
|
||||
VisitMigrationForSort(migration, migrationDict, visitedMigrations, sortedMigrations);
|
||||
}
|
||||
|
||||
return sortedMigrations;
|
||||
}
|
||||
|
||||
protected void VisitMigrationForSort(
|
||||
AbstractProfileMigration migration,
|
||||
Dictionary<Type, AbstractProfileMigration> migrationTypeDictionary,
|
||||
Dictionary<Type, bool> visitedTypeDictionary,
|
||||
List<AbstractProfileMigration> sortedMigrations
|
||||
)
|
||||
{
|
||||
var migrationType = migration.GetType();
|
||||
|
||||
if (visitedTypeDictionary.TryGetValue(migrationType, out var isVisited))
|
||||
{
|
||||
if (isVisited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Big error, two migrations should never depend on one another
|
||||
throw new InvalidOperationException($"Cycle detected in migration prerequisites involving: {migrationType.Name}");
|
||||
}
|
||||
|
||||
// Mark the current migration type for visiting
|
||||
visitedTypeDictionary[migrationType] = false;
|
||||
|
||||
foreach (var prerequisiteType in migration.PrerequisiteMigrations)
|
||||
{
|
||||
if (!migrationTypeDictionary.TryGetValue(prerequisiteType, out var prereqMigration))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Visit the next prerequisite
|
||||
VisitMigrationForSort(prereqMigration, migrationTypeDictionary, visitedTypeDictionary, sortedMigrations);
|
||||
}
|
||||
|
||||
// Done visiting, mark it as fully visited and add it to the sorted migrations
|
||||
visitedTypeDictionary[migrationType] = true;
|
||||
sortedMigrations.Add(migration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,47 +8,46 @@ using SPTarkov.Server.Core.Servers.Http;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
|
||||
namespace SPTarkov.Server.Core.Status
|
||||
namespace SPTarkov.Server.Core.Status;
|
||||
|
||||
[Injectable(TypePriority = 0)]
|
||||
public class StatusPage(TimeUtil timeUtil, ProfileActivityService profileActivityService, ConfigServer configServer) : IHttpListener
|
||||
{
|
||||
[Injectable(TypePriority = 0)]
|
||||
public class StatusPage(TimeUtil timeUtil, ProfileActivityService profileActivityService, ConfigServer configServer) : IHttpListener
|
||||
protected readonly CoreConfig _coreConfig = configServer.GetConfig<CoreConfig>();
|
||||
|
||||
public bool CanHandle(MongoId sessionId, HttpRequest req)
|
||||
{
|
||||
protected readonly CoreConfig _coreConfig = configServer.GetConfig<CoreConfig>();
|
||||
return req.Method == "GET" && req.Path.Value.Contains("/status");
|
||||
}
|
||||
|
||||
public bool CanHandle(MongoId sessionId, HttpRequest req)
|
||||
{
|
||||
return req.Method == "GET" && req.Path.Value.Contains("/status");
|
||||
}
|
||||
public async Task Handle(MongoId sessionId, HttpRequest req, HttpResponse resp)
|
||||
{
|
||||
var sptVersion = $"SPT version: {ProgramStatics.SPT_VERSION()}";
|
||||
var debugEnabled = $"Debug enabled: {ProgramStatics.DEBUG()}";
|
||||
var modsEnabled = $"Mods enabled: {ProgramStatics.MODS()}";
|
||||
var timeStarted = $"Started : {timeUtil.GetDateTimeFromTimeStamp(_coreConfig.ServerStartTime.Value)}";
|
||||
var uptime = $"Uptime: {DateTimeOffset.UtcNow.ToUnixTimeSeconds() - _coreConfig.ServerStartTime} seconds".ToArray();
|
||||
var activeProfiles = profileActivityService.GetActiveProfileIdsWithinMinutes(30);
|
||||
var activePlayerCount = $"Profiles active in last 30 minutes: {activeProfiles.Count}. {string.Join(",", activeProfiles)}";
|
||||
|
||||
public async Task Handle(MongoId sessionId, HttpRequest req, HttpResponse resp)
|
||||
{
|
||||
var sptVersion = $"SPT version: {ProgramStatics.SPT_VERSION()}";
|
||||
var debugEnabled = $"Debug enabled: {ProgramStatics.DEBUG()}";
|
||||
var modsEnabled = $"Mods enabled: {ProgramStatics.MODS()}";
|
||||
var timeStarted = $"Started : {timeUtil.GetDateTimeFromTimeStamp(_coreConfig.ServerStartTime.Value)}";
|
||||
var uptime = $"Uptime: {DateTimeOffset.UtcNow.ToUnixTimeSeconds() - _coreConfig.ServerStartTime} seconds".ToArray();
|
||||
var activeProfiles = profileActivityService.GetActiveProfileIdsWithinMinutes(30);
|
||||
var activePlayerCount = $"Profiles active in last 30 minutes: {activeProfiles.Count}. {string.Join(",", activeProfiles)}";
|
||||
resp.StatusCode = 200;
|
||||
resp.ContentType = "text/html";
|
||||
|
||||
resp.StatusCode = 200;
|
||||
resp.ContentType = "text/html";
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(sptVersion));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(debugEnabled));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(modsEnabled));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(sptVersion));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(debugEnabled));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(modsEnabled));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(timeStarted));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(uptime));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(activePlayerCount));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(timeStarted));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(uptime));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(activePlayerCount));
|
||||
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
|
||||
|
||||
await resp.StartAsync();
|
||||
await resp.CompleteAsync();
|
||||
}
|
||||
await resp.StartAsync();
|
||||
await resp.CompleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user