c7e40deb90
* Add support for partially loading invalid profiles * Return early in exception
293 lines
10 KiB
C#
293 lines
10 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using SPTarkov.Server.Core.Models.Common;
|
|
using SPTarkov.Server.Core.Models.Eft.Common;
|
|
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
|
using SPTarkov.Server.Core.Models.Eft.ItemEvent;
|
|
using SPTarkov.Server.Core.Models.Enums;
|
|
using SPTarkov.Server.Core.Services;
|
|
|
|
namespace SPTarkov.Server.Core.Extensions;
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle Remove event
|
|
/// Remove item from player inventory + insured items array
|
|
/// Also deletes child items
|
|
/// </summary>
|
|
/// <param name="profile">Profile to remove item from (pmc or scav)</param>
|
|
/// <param name="itemId">Items id to remove</param>
|
|
/// <param name="sessionId">Session id</param>
|
|
/// <param name="output">OPTIONAL - ItemEventRouterResponse</param>
|
|
public static void RemoveItem(this PmcData profile, MongoId itemId, MongoId sessionId, ItemEventRouterResponse? output = null)
|
|
{
|
|
if (itemId.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get children of item, they get deleted too
|
|
var itemAndChildrenToRemove = profile.Inventory.Items.GetItemWithChildren(itemId);
|
|
if (!itemAndChildrenToRemove.Any())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var inventoryItems = profile.Inventory.Items;
|
|
var insuredItems = profile.InsuredItems;
|
|
|
|
// We have output object, inform client of root item deletion, not children
|
|
output?.ProfileChanges[sessionId].Items.DeletedItems.Add(new DeletedItem { Id = itemId });
|
|
|
|
foreach (var item in itemAndChildrenToRemove)
|
|
{
|
|
// We expect that each inventory item and each insured item has unique "_id", respective "itemId".
|
|
// Therefore, we want to use a NON-Greedy function and escape the iteration as soon as we find requested item.
|
|
var inventoryIndex = inventoryItems.FindIndex(inventoryItem => inventoryItem.Id == item.Id);
|
|
if (inventoryIndex != -1)
|
|
{
|
|
inventoryItems.RemoveAt(inventoryIndex);
|
|
}
|
|
|
|
var insuredItemIndex = insuredItems.FindIndex(insuredItem => insuredItem.ItemId == item.Id);
|
|
if (insuredItemIndex != -1)
|
|
{
|
|
insuredItems.RemoveAt(insuredItemIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|