Apply enforced file scoped namespacing

This commit is contained in:
Archangel
2025-07-31 15:23:32 +02:00
parent aec8420d53
commit cec47fefd2
39 changed files with 2466 additions and 2513 deletions
+19 -20
View File
@@ -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);
}
}
@@ -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();
}
}