From edc11a63ee513b29f3bb6388b0b4a328b94d9e3c Mon Sep 17 00:00:00 2001 From: CWX Date: Fri, 10 Jan 2025 22:16:02 +0000 Subject: [PATCH] Complete transfer of ProfileHelper --- Core/Helpers/ProfileHelper.cs | 377 +++++++++++++++--- Core/Models/Eft/Common/Tables/BotBase.cs | 4 +- .../Eft/Profile/SearchFriendResponse.cs | 11 +- 3 files changed, 339 insertions(+), 53 deletions(-) diff --git a/Core/Helpers/ProfileHelper.cs b/Core/Helpers/ProfileHelper.cs index 409d330a..9d8d2d3a 100644 --- a/Core/Helpers/ProfileHelper.cs +++ b/Core/Helpers/ProfileHelper.cs @@ -3,12 +3,52 @@ using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Profile; using Core.Models.Enums; +using Core.Models.Spt.Config; +using Core.Servers; +using Core.Services; +using Core.Utils; +using Core.Utils.Cloners; namespace Core.Helpers; [Injectable] public class ProfileHelper { + protected Models.Utils.ILogger _logger; + + protected ICloner _cloner; + protected SaveServer _saveServer; + protected DatabaseService _databaseService; + protected Watermark _watermark; + protected ItemHelper _itemHelper; + protected TimeUtil _timeUtil; + protected LocalisationService _localisationService; + protected InventoryConfig _inventoryConfig; + protected HashUtil _hashUtil; + + public ProfileHelper( + ICloner cloner, + SaveServer saveServer, + DatabaseService databaseService, + Watermark watermark, + ItemHelper itemHelper, + TimeUtil timeUtil, + LocalisationService localisationService, + InventoryConfig inventoryConfig, + HashUtil hashUtil + ) + { + _cloner = cloner; + _saveServer = saveServer; + _databaseService = databaseService; + _watermark = watermark; + _itemHelper = itemHelper; + _timeUtil = timeUtil; + _localisationService = localisationService; + _inventoryConfig = inventoryConfig; + _hashUtil = hashUtil; + } + /// /// Remove/reset a completed quest condtion from players profile quest data /// @@ -16,7 +56,14 @@ public class ProfileHelper /// Quest with condition to remove public void RemoveQuestConditionFromProfile(PmcData pmcData, Dictionary questConditionId) { - throw new NotImplementedException(); + foreach (var questId in questConditionId) + { + var conditionId = questId.Value; + var profileQuest = pmcData.Quests.FirstOrDefault(q => q.QId == conditionId); + + if (profileQuest != null) // Remove condition + profileQuest.CompletedConditions.Remove(conditionId); + } } /// @@ -25,7 +72,7 @@ public class ProfileHelper /// Dictionary of profiles public Dictionary GetProfiles() { - throw new NotImplementedException(); + return _saveServer.GetProfiles(); } /// @@ -35,7 +82,21 @@ public class ProfileHelper /// Array of PmcData objects public List GetCompleteProfile(string sessionId) { - throw new NotImplementedException(); + var output = new List(); + + if (IsWiped(sessionId)) + return output; + + var FullProfileClone = _cloner.Clone(GetFullProfile(sessionId)); + + // Sanitize any data the client can not receive + SanitizeProfileForClient(FullProfileClone); + + // PMC must be at array index 0, scav at 1 + output.Add(FullProfileClone.CharacterData.PmcData); + output.Add(FullProfileClone.CharacterData.ScavData); + + return output; } /// @@ -44,7 +105,12 @@ public class ProfileHelper /// A clone of the full player profile protected void SanitizeProfileForClient(SptProfile clonedProfile) { - throw new NotImplementedException(); + // Remove `loyaltyLevel` from `TradersInfo`, as otherwise it causes the client to not + // properly calculate the player's `loyaltyLevel` + foreach (var trader in clonedProfile.CharacterData.PmcData.TradersInfo.Values) + { + trader.LoyaltyLevel = null; + } } /// @@ -55,17 +121,22 @@ public class ProfileHelper /// True if already in use public bool IsNicknameTaken(ValidateNicknameRequestData nicknameRequest, string sessionID) { - throw new NotImplementedException(); + var allProfiles = _saveServer.GetProfiles().Values; + + // Find a profile that doesn't have same session id but has same name + return allProfiles.Any(p => + ProfileHasInfoProperty(p) && !StringsMatch(p.ProfileInfo.ProfileId, sessionID) && // SessionIds dont match + StringsMatch(p.CharacterData.PmcData.Info.LowerNickname.ToLower(), nicknameRequest.Nickname.ToLower())); // Nicknames do } protected bool ProfileHasInfoProperty(SptProfile profile) { - throw new NotImplementedException(); + return profile?.CharacterData?.PmcData?.Info != null; } protected bool StringsMatch(string stringA, string stringB) { - throw new NotImplementedException(); + return stringA == stringB; } /// @@ -75,7 +146,11 @@ public class ProfileHelper /// Experience to add to PMC character public void AddExperienceToPmc(string sessionID, int experienceToAdd) { - throw new NotImplementedException(); + var pmcData = GetPmcProfile(sessionID); + if (pmcData != null) + pmcData.Info.Experience += experienceToAdd; + else + _logger.Error($"Profile {sessionID} does not exist"); } /// @@ -85,7 +160,7 @@ public class ProfileHelper /// PmcData public PmcData? GetProfileByPmcId(string pmcId) { - throw new NotImplementedException(); + return _saveServer.GetProfiles().Values.First(p => p.CharacterData?.PmcData?.Id == pmcId).CharacterData.PmcData; } /// @@ -93,9 +168,21 @@ public class ProfileHelper /// /// Level to get xp for /// Number of xp points for level - public int GetExperience(int level) + public double? GetExperience(int level) { - throw new NotImplementedException(); + var playerLevel = level; + var expTable = _databaseService.GetGlobals().Configuration.Exp.Level.ExperienceTable; + double? exp = null; + + if (playerLevel >= expTable.Length) // make sure to not go out of bounds + playerLevel = expTable.Length - 1; + + foreach (var expLevel in expTable) + { + exp += expLevel.Experience; + } + + return exp; } /// @@ -104,7 +191,7 @@ public class ProfileHelper /// Max level public int GetMaxLevel() { - throw new NotImplementedException(); + return _databaseService.GetGlobals().Configuration.Exp.Level.ExperienceTable.Length - 1; } /// @@ -113,7 +200,16 @@ public class ProfileHelper /// Spt public Spt GetDefaultSptDataObject() { - throw new NotImplementedException(); + return new() + { + Version = _watermark.GetVersionTag(true), + Mods = new(), + ReceivedGifts = new(), + BlacklistedItemTemplates = new(), + FreeRepeatableRefreshUsedCount = new(), + Migrations = new(), + CultistRewards = new() + }; } /// @@ -123,7 +219,7 @@ public class ProfileHelper /// SptProfile object public SptProfile? GetFullProfile(string sessionID) { - throw new NotImplementedException(); + return _saveServer.ProfileExists(sessionID) ? _saveServer.GetProfile(sessionID) : null; } /// @@ -133,7 +229,11 @@ public class ProfileHelper /// public SptProfile? GetFullProfileByAccountId(string accountID) { - throw new NotImplementedException(); + var check = int.TryParse(accountID, out var aid); + if (!check) + _logger.Error($"Account {accountID} does not exist"); + + return _saveServer.GetProfiles().FirstOrDefault(p => p.Value?.ProfileInfo?.Aid == aid).Value; } /// @@ -143,7 +243,11 @@ public class ProfileHelper /// public SearchFriendResponse? GetChatRoomMemberFromSessionId(string sessionID) { - throw new NotImplementedException(); + var pmcProfile = GetFullProfile(sessionID)?.CharacterData?.PmcData; + if (pmcProfile == null) + return null; + + return GetChatRoomMemberFromPmcProfile(pmcProfile); } /// @@ -151,9 +255,21 @@ public class ProfileHelper /// /// The PMC profile data to format into a ChatRoomMember structure /// - public SearchFriendResponse GetChatRoomMemberFromPmcProfile(PmcData pmcProfile) + public SearchFriendResponse? GetChatRoomMemberFromPmcProfile(PmcData pmcProfile) { - throw new NotImplementedException(); + return new() + { + Id = pmcProfile.Id, + Aid = pmcProfile.Aid, + Info = new() + { + Nickname = pmcProfile.Info.Nickname, + Side = pmcProfile.Info.Side, + Level = pmcProfile.Info.Level, + MemberCategory = pmcProfile.Info.MemberCategory, + SelectedMemberCategory = pmcProfile.Info.SelectedMemberCategory + } + }; } /// @@ -163,7 +279,9 @@ public class ProfileHelper /// PmcData object public PmcData? GetPmcProfile(string sessionID) { - throw new NotImplementedException(); + var fullProfile = GetFullProfile(sessionID); + + return fullProfile?.CharacterData?.PmcData; } /// @@ -171,9 +289,10 @@ public class ProfileHelper /// /// Id to validate /// True is a player + /// UNUSED? public bool IsPlayer(string userId) { - throw new NotImplementedException(); + return _saveServer.ProfileExists(userId); } /// @@ -181,9 +300,9 @@ public class ProfileHelper /// /// Profiles id /// IPmcData object - public PmcData GetScavProfile(string sessionID) + public PmcData? GetScavProfile(string sessionID) { - throw new NotImplementedException(); + return _saveServer.GetProfile(sessionID)?.CharacterData?.ScavData; } /// @@ -192,7 +311,26 @@ public class ProfileHelper /// Default profile Stats object public Stats GetDefaultCounters() { - throw new NotImplementedException(); + return new() + { + Eft = new() + { + CarriedQuestItems = new(), + DamageHistory = new() { LethalDamagePart = "Head", LethalDamage = null, BodyParts = new()}, + DroppedItems = new(), + ExperienceBonusMult = 0, + FoundInRaidItems = new(), + LastPlayerState = null, + LastSessionDate = 0, + OverallCounters = new(), + SessionCounters = new(), + SessionExperienceMult = 0, + SurvivorClass = "Unknown", + TotalInGameTime = 0, + TotalSessionExperience = 0, + Victims = new() + } + }; } /// @@ -200,9 +338,10 @@ public class ProfileHelper /// /// Profile id /// True if profile is to be wiped of data/progress + /// TODO: logic doesnt feel right to have IsWiped being nullable protected bool IsWiped(string sessionID) { - throw new NotImplementedException(); + return _saveServer.GetProfile(sessionID)?.ProfileInfo?.IsWiped ?? false; } /// @@ -212,7 +351,18 @@ public class ProfileHelper /// profile without secure container public PmcData RemoveSecureContainer(PmcData profile) { - throw new NotImplementedException(); + var items = profile.Inventory.Items; + var secureContainer = items.First(i => i.SlotId == "SecuredContainer"); + if (secureContainer != null) + { + // Find and remove container + children + var childItemsInSecureContainer = _itemHelper.FindAndReturnChildrenByItems(items, secureContainer.Id); + + // Remove child items + secure container + profile.Inventory.Items = items.Where(i => !childItemsInSecureContainer.Contains(i.Id)).ToList(); + } + + return profile; } /// @@ -224,7 +374,24 @@ public class ProfileHelper /// Limit of how many of this gift a player can have public void FlagGiftReceivedInProfile(string playerId, string giftId, int maxCount) { - throw new NotImplementedException(); + var profileToUpdate = GetFullProfile(playerId); + profileToUpdate.SptData.ReceivedGifts ??= new(); + + var giftData = profileToUpdate.SptData.ReceivedGifts.FirstOrDefault(g => g.GiftId == giftId); + if (giftData != null) + { + // Increment counter + giftData.Current++; + return; + } + + // Player has never received gift, make a new object + profileToUpdate.SptData.ReceivedGifts.Add(new() + { + GiftId = giftId, + TimestampLastAccepted = _timeUtil.GetTimeStamp(), + Current = 1 + }); } /// @@ -236,28 +403,56 @@ public class ProfileHelper /// True if player has recieved gift previously public bool PlayerHasRecievedMaxNumberOfGift(string playerId, string giftId, int maxGiftCount) { - throw new NotImplementedException(); + var profile = GetFullProfile(playerId); + if (profile == null) + { + _logger.Debug($"Unable to gift {giftId}, Profile: {playerId} does not exist"); + return false; + } + + if (profile.SptData.ReceivedGifts == null) + return false; + + var giftDataFromProfile = profile.SptData.ReceivedGifts.FirstOrDefault(g => g.GiftId == giftId); + if (giftDataFromProfile == null) + return false; + + return giftDataFromProfile.Current >= maxGiftCount; } /// - /// Find Stat in profile counters and increment by one + /// Find Stat in profile counters and increment by one. /// /// Counters to search for key /// Key + /// Was Includes in Node so might not be exact? public void IncrementStatCounter(CounterKeyValue[] counters, string keyToIncrement) { - throw new NotImplementedException(); + var stat = counters.FirstOrDefault(c => c.Key.Contains(keyToIncrement)); + if (stat != null) + stat.Value++; } /// /// Check if player has a skill at elite level /// - /// Skill to check + /// Skill to check /// Profile to find skill in /// True if player has skill at elite level - public bool HasEliteSkillLevel(SkillTypes skillType, PmcData pmcProfile) + public bool HasEliteSkillLevel(SkillTypes skill, PmcData pmcProfile) { - throw new NotImplementedException(); + var profileSkills = pmcProfile.Skills.Common; + if (profileSkills == null) + return false; + + var profileSkill = profileSkills.Dictionary.FirstOrDefault(s => s.Value.Id == skill.ToString()).Value; + if (profileSkill == null) + { + _logger.Error(_localisationService.GetText("quest-no_skill_found", skill)); + return false; + } + + return profileSkill.Progress >= 5100; // 51 } /// @@ -267,9 +462,42 @@ public class ProfileHelper /// Skill to add points to /// Points to add /// Skills are multiplied by a value in globals, default is off to maintain compatibility with legacy code - public void AddSkillPointsToPlayer(PmcData pmcProfile, SkillTypes skill, int pointsToAdd, bool useSkillProgressRateMultipler = false) + public void AddSkillPointsToPlayer(PmcData pmcProfile, SkillTypes skill, double? pointsToAdd, bool useSkillProgressRateMultipler = false) { - throw new NotImplementedException(); + var pointsToAddToSkill = pointsToAdd; + + if (pointsToAddToSkill < 0D) + { + _logger.Warning(_localisationService.GetText("player-attempt_to_increment_skill_with_negative_value", skill)); + return; + } + + var profileSkills = pmcProfile?.Skills?.Common; + if (profileSkills == null) + { + _logger.Warning($"Unable to add {pointsToAddToSkill} points to {skill}, Profile has no skills"); + return; + } + + var profileSkill = profileSkills.Dictionary.FirstOrDefault(s => s.Value.Id == skill.ToString()).Value; + if (profileSkill == null) + { + _logger.Error(_localisationService.GetText("quest-no_skill_found", skill)); + return; + } + + if (useSkillProgressRateMultipler) + { + var skillProgressRate = _databaseService.GetGlobals().Configuration.SkillsSettings.SkillProgressRate; + pointsToAddToSkill *= skillProgressRate; + } + + if (_inventoryConfig.SkillGainMultipliers[skill.ToString()] != null) + pointsToAddToSkill *= _inventoryConfig.SkillGainMultipliers[skill.ToString()]; + + profileSkill.Progress += pointsToAddToSkill; + profileSkill.Progress = Math.Min(profileSkill?.Progress ?? 0D, 5100); // Prevent skill from ever going above level 51 (5100) + profileSkill.LastAccess = _timeUtil.GetTimeStamp(); } /// @@ -278,9 +506,13 @@ public class ProfileHelper /// Player profile /// Skill to look up and return value from /// Common skill object from desired profile - public Common GetSkillFromProfile(PmcData pmcData, SkillTypes skill) + public Common? GetSkillFromProfile(PmcData pmcData, SkillTypes skill) { - throw new NotImplementedException(); + var skillToReturn = pmcData?.Skills?.Common.List.FirstOrDefault(s => s.Id == skill.ToString()); + if (skillToReturn == null) + _logger.Warning($"Profile {pmcData.SessionId} does not have a skill named: {skill.ToString()}"); + + return skillToReturn; } /// @@ -290,7 +522,7 @@ public class ProfileHelper /// True if account is developer public bool IsDeveloperAccount(string sessionID) { - throw new NotImplementedException(); + return GetFullProfile(sessionID)?.ProfileInfo?.Edition?.ToLower().StartsWith("spt developer") == false; } /// @@ -300,7 +532,24 @@ public class ProfileHelper /// How many rows to give profile public void AddStashRowsBonusToProfile(string sessionId, int rowsToAdd) { - throw new NotImplementedException(); + var profile = GetPmcProfile(sessionId); + var existingBonus = profile?.Bonuses?.FirstOrDefault(b => b.Type == BonusType.StashRows); + if (existingBonus != null) + { + profile?.Bonuses?.Add(new() + { + Id = _hashUtil.Generate(), + Value = rowsToAdd, + Type = BonusType.StashRows, + IsPassive = true, + IsVisible = true, + IsProduction = false + }); + } + else + { + existingBonus.Value += rowsToAdd; + } } /// @@ -309,14 +558,20 @@ public class ProfileHelper /// Player profile /// Bonus to sum up /// Summed bonus value or 0 if no bonus found - public int GetBonusValueFromProfile(PmcData pmcProfile, BonusType desiredBonus) + public double GetBonusValueFromProfile(PmcData pmcProfile, BonusType desiredBonus) { - throw new NotImplementedException(); + var bonuses = pmcProfile?.Bonuses?.Where(b => b.Type == desiredBonus); + if (bonuses.Count() == 0) + return 0; + + // Sum all bonuses found above + return bonuses?.Sum(new Func(bonus => bonus?.Value ?? 0)) ?? 0; } public bool PlayerIsFleaBanned(PmcData pmcProfile) { - throw new NotImplementedException(); + var currentTimestamp = _timeUtil.GetTimeStamp(); + return pmcProfile?.Info?.Bans?.Any(b => b.BanType == BanType.RAGFAIR && currentTimestamp < b.DateTime) ?? false; } /// @@ -326,12 +581,14 @@ public class ProfileHelper /// Id of achievement to add public void AddAchievementToProfile(PmcData pmcProfile, string achievementId) { - throw new NotImplementedException(); + pmcProfile.Achievements[achievementId] = _timeUtil.GetTimeStamp(); } + protected readonly List gameEditions = ["edge_of_darkness", "unheard_edition"]; + public bool HasAccessToRepeatableFreeRefreshSystem(PmcData pmcProfile) { - throw new NotImplementedException(); + return gameEditions.Contains(pmcProfile.Info.GameVersion); } /// @@ -341,7 +598,19 @@ public class ProfileHelper /// New tpl to set profiles Pockets to public void ReplaceProfilePocketTpl(PmcData pmcProfile, string newPocketTpl) { - throw new NotImplementedException(); + // Find all pockets in profile, may be multiple as they could have equipment stand + // (1 pocket for each upgrade level of equipment stand) + var pockets = pmcProfile.Inventory.Items.Where(i => i.SlotId == "Pockets"); + if (pockets.Count() == 0) + { + _logger.Error($"Unable to replace profile: {pmcProfile.Id} pocket tpl with: {newPocketTpl} as Pocket item could not be found."); + return; + } + + foreach (var pocket in pockets) + { + pocket.Id = newPocketTpl; + } } /// @@ -351,7 +620,7 @@ public class ProfileHelper /// List of item objects public List GetQuestItemsInProfile(PmcData profile) { - throw new NotImplementedException(); + return profile?.Inventory?.Items.Where(i => i.ParentId == profile.Inventory.QuestRaidItems).ToList(); } /// @@ -361,6 +630,22 @@ public class ProfileHelper /// A list of Item objects representing the favorited data public List GetOtherProfileFavorites(PmcData profile) { - throw new NotImplementedException(); + var fullFavorites = new List(); + + foreach (var itemId in profile.Inventory.FavoriteItems ?? new List()) + { + // When viewing another users profile, the client expects a full item with children, so get that + var itemAndChildren = _itemHelper.FindAndReturnChildrenAsItems(profile.Inventory.Items, itemId); + if (itemAndChildren != null && itemAndChildren.Count > 0) + { + // To get the client to actually see the items, we set the main item's parent to null, so it's treated as a root item + var clonedItems = _cloner.Clone(itemAndChildren); + clonedItems.First().ParentId = null; + + fullFavorites.AddRange(clonedItems); + } + } + + return fullFavorites; } } diff --git a/Core/Models/Eft/Common/Tables/BotBase.cs b/Core/Models/Eft/Common/Tables/BotBase.cs index 2ca32223..cc6dd3d2 100644 --- a/Core/Models/Eft/Common/Tables/BotBase.cs +++ b/Core/Models/Eft/Common/Tables/BotBase.cs @@ -71,7 +71,7 @@ public class BotBase /** Achievement id and timestamp */ [JsonPropertyName("Achievements")] [JsonConverter(typeof(ArrayToObjectFactoryConverter))] - public Dictionary? Achievements { get; set; } + public Dictionary? Achievements { get; set; } [JsonPropertyName("RepeatableQuests")] public List? RepeatableQuests { get; set; } @@ -332,7 +332,7 @@ public class BaseSkill public class Common : BaseSkill { public int? PointsEarnedDuringSession { get; set; } - public int? LastAccess { get; set; } + public long? LastAccess { get; set; } } public class Mastering : BaseSkill diff --git a/Core/Models/Eft/Profile/SearchFriendResponse.cs b/Core/Models/Eft/Profile/SearchFriendResponse.cs index a0d0d949..dcf9c1f9 100644 --- a/Core/Models/Eft/Profile/SearchFriendResponse.cs +++ b/Core/Models/Eft/Profile/SearchFriendResponse.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Core.Models.Enums; namespace Core.Models.Eft.Profile; @@ -8,7 +9,7 @@ public class SearchFriendResponse public string? Id { get; set; } [JsonPropertyName("aid")] - public int? Aid { get; set; } + public double? Aid { get; set; } [JsonPropertyName("Info")] public FriendInfo? Info { get; set; } @@ -24,11 +25,11 @@ public class FriendInfo public string? Side { get; set; } [JsonPropertyName("Level")] - public int? Level { get; set; } + public double? Level { get; set; } [JsonPropertyName("MemberCategory")] - public int? MemberCategory { get; set; } + public MemberCategory? MemberCategory { get; set; } [JsonPropertyName("SelectedMemberCategory")] - public int? SelectedMemberCategory { get; set; } -} \ No newline at end of file + public MemberCategory? SelectedMemberCategory { get; set; } +}