From 20fcc26d9a1a4ec29845a9e3d957ec9d56ac5ecb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 17:44:53 +0000 Subject: [PATCH] partial work on profile callbacks --- Core/Callbacks/ProfileCallbacks.cs | 249 ++++---- Core/Controllers/ProfileController.cs | 539 +++++++++++++++++- Core/Helpers/QuestHelper.cs | 5 +- .../Eft/Common/Tables/ProfileTemplate.cs | 17 + .../Eft/Launcher/GetMiniProfileRequestData.cs | 5 +- .../Eft/Profile/GetOtherProfileRequest.cs | 5 +- .../Eft/Profile/GetProfileSettingsRequest.cs | 5 +- .../ProfileChangeNicknameRequestData.cs | 5 +- .../Profile/ProfileChangeVoiceRequestData.cs | 5 +- .../Eft/Profile/ProfileCreateRequestData.cs | 5 +- .../Eft/Profile/SearchFriendRequestData.cs | 5 +- Core/Models/Eft/Profile/SptProfile.cs | 2 +- .../Profile/ValidateNicknameRequestData.cs | 5 +- Core/Routers/Static/ProfileStaticRouter.cs | 123 ++++ Core/Servers/Http/SptHttpListener.cs | 22 +- Core/Utils/Cloners/ICloner.cs | 5 + Core/Utils/Cloners/JsonCloner.cs | 17 + Core/Utils/HttpResponseUtil.cs | 4 +- 18 files changed, 867 insertions(+), 156 deletions(-) create mode 100644 Core/Routers/Static/ProfileStaticRouter.cs create mode 100644 Core/Utils/Cloners/ICloner.cs create mode 100644 Core/Utils/Cloners/JsonCloner.cs diff --git a/Core/Callbacks/ProfileCallbacks.cs b/Core/Callbacks/ProfileCallbacks.cs index 8b6736eb..4bc861b6 100644 --- a/Core/Callbacks/ProfileCallbacks.cs +++ b/Core/Callbacks/ProfileCallbacks.cs @@ -1,175 +1,166 @@ -using Core.Models.Eft.Common; -using Core.Models.Eft.HttpResponse; +using Core.Annotations; +using Core.Controllers; +using Core.Helpers; +using Core.Models.Eft.Common; using Core.Models.Eft.Launcher; using Core.Models.Eft.Profile; +using Core.Utils; namespace Core.Callbacks; +[Injectable] public class ProfileCallbacks { - public ProfileCallbacks() + protected HttpResponseUtil _httpResponse; + protected TimeUtil _timeUtil; + protected ProfileController _profileController; + protected ProfileHelper _profileHelper; + + public ProfileCallbacks( + HttpResponseUtil httpResponse, + TimeUtil timeUtil, + ProfileController profileController, + ProfileHelper profileHelper + ) + { + _httpResponse = httpResponse; + _timeUtil = timeUtil; + _profileController = profileController; + _profileHelper = profileHelper; + } + + /** + * Handle client/game/profile/create + */ + public string CreateProfile(string url, ProfileCreateRequestData info, string sessionID) { + var id = _profileController.CreateProfile(info, sessionID); + return _httpResponse.GetBody(new CreateProfileResponse(){ UserId = id }); } - /// - /// Handle client/game/profile/create - /// - /// - /// - /// - /// - public GetBodyResponseData CreateProfile(string url, ProfileCreateRequestData info, string sessionID) + /** + * Handle client/game/profile/list + * Get the complete player profile (scav + pmc character) + */ + public string GetProfileData(string url, EmptyRequestData info, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.GetBody(_profileController.GetCompleteProfile(sessionID)); } - /// - /// Handle client/game/profile/list - /// Get the complete player profile (scav + pmc character) - /// - /// - /// - /// - /// - public GetBodyResponseData GetProfileData(string url, EmptyRequestData info, string sessionID) - { - throw new NotImplementedException(); + /** + * Handle client/game/profile/savage/regenerate + * Handle the creation of a scav profile for player + * Occurs post-raid and when profile first created immediately after character details are confirmed by player + * @param url + * @param info empty + * @param sessionID Session id + * @returns Profile object + */ + public string RegenerateScav(string url, EmptyRequestData info, string sessionID) { + return _httpResponse.GetBody(new List() { _profileController.GeneratePlayerScav(sessionID) }); } - /// - /// Handle client/game/profile/savage/regenerate - /// Handle the creation of a scav profile for player - /// Occurs post-raid and when profile first created immediately after character details are confirmed by player - /// - /// - /// - /// - /// - public GetBodyResponseData RegenerateScav(string url, EmptyRequestData info, string sessionID) + /** + * Handle client/game/profile/voice/change event + */ + public string ChangeVoice(string url, ProfileChangeVoiceRequestData info, string sessionID) { - throw new NotImplementedException(); + _profileController.ChangeVoice(info, sessionID); + return _httpResponse.NullResponse(); } - /// - /// Handle client/game/profile/voice/change event - /// - /// - /// - /// - /// - public NullResponseData ChangeVoice(string url, ProfileChangeVoiceRequestData info, string sessionID) + /** + * Handle client/game/profile/nickname/change event + * Client allows player to adjust their profile name + */ + public string ChangeNickname(string url, ProfileChangeNicknameRequestData info, string sessionID) { - throw new NotImplementedException(); + var output = _profileController.ChangeNickname(info, sessionID); + + if (output == "taken") { + return _httpResponse.GetBody(null, 255, "The nickname is already in use"); + } + + if (output == "tooshort") { + return _httpResponse.GetBody(null, 1, "The nickname is too short"); + } + + return _httpResponse.GetBody(new { status = 0, nicknamechangedate = _timeUtil.GetTimeStamp() }); } - /// - /// Handle client/game/profile/nickname/change event - /// Client allows player to adjust their profile name - /// - /// - /// - /// - /// - public GetBodyResponseData ChangeNickname(string url, ProfileChangeNicknameRequestData info, string sessionID) + /** + * Handle client/game/profile/nickname/validate + */ + public string ValidateNickname(string url, ValidateNicknameRequestData info, string sessionID) { - throw new NotImplementedException(); + var output = _profileController.ValidateNickname(info, sessionID); + + if (output == "taken") { + return _httpResponse.GetBody(null, 255, "225 - "); + } + + if (output == "tooshort") { + return _httpResponse.GetBody(null, 256, "256 - "); + } + + return _httpResponse.GetBody(new { status = "ok" }); } - /// - /// Handle client/game/profile/nickname/validate - /// - /// - /// - /// - /// - public GetBodyResponseData ValidateNickname(string url, ValidateNicknameRequestData info, string sessionID) + /** + * Handle client/game/profile/nickname/reserved + */ + public string GetReservedNickname(string url, EmptyRequestData info, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.GetBody("SPTarkov"); } - /// - /// Handle client/game/profile/nickname/reserved - /// - /// - /// - /// - /// - public GetBodyResponseData GetReservedNickname(string url, EmptyRequestData info, string sessionID) + /** + * Handle client/profile/status + * Called when creating a character when choosing a character face/voice + */ + public string GetProfileStatus(string url, EmptyRequestData info, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.GetBody(_profileController.GetProfileStatus(sessionID)); } - /// - /// Handle client/profile/status - /// - /// - /// - /// - /// - public GetBodyResponseData GetProfileStatus(string url, EmptyRequestData info, string sessionID) + /** + * Handle client/profile/view + * Called when viewing another players profile + */ + public string GetOtherProfile(string url, GetOtherProfileRequest request, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.GetBody(_profileController.GetOtherProfile(sessionID, request)); } - /// - /// Handle client/profile/status - /// Called when creating a character when choosing a character face/voice - /// - /// - /// - /// - /// - public GetBodyResponseData GetOtherProfile(string url, GetOtherProfileRequest info, string sessionID) + /** + * Handle client/profile/settings + */ + public string GetProfileSettings(string url, GetProfileSettingsRequest info, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.GetBody(_profileController.SetChosenProfileIcon(sessionID, info)); } - /// - /// Handle client/profile/view - /// Called when viewing another players profile - /// - /// - /// - /// - /// - public GetBodyResponseData GetProfileSettings(string url, GetProfileSettingsRequest info, string sessionID) + /** + * Handle client/game/profile/search + */ + public string SearchFriend(string url, SearchFriendRequestData info, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.GetBody(_profileController.GetFriends(info, sessionID)); } - /// - /// Handle client/profile/settings - /// - /// - /// - /// - /// - public GetBodyResponseData SearchFriend(string url, SearchFriendRequestData info, string sessionID) - { - throw new NotImplementedException(); - } - - /// - /// Handle launcher/profile/info - /// - /// - /// - /// - /// + /** + * Handle launcher/profile/info + */ public string GetMiniProfile(string url, GetMiniProfileRequestData info, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.NoBody(_profileController.GetMiniProfile(sessionID)); } - /// - /// Handle /launcher/profiles - /// - /// - /// - /// - /// + /** + * Handle /launcher/profiles + */ public string GetAllMiniProfiles(string url, EmptyRequestData info, string sessionID) { - throw new NotImplementedException(); + return _httpResponse.NoBody(_profileController.GetMiniProfiles()); } -} \ No newline at end of file +} diff --git a/Core/Controllers/ProfileController.cs b/Core/Controllers/ProfileController.cs index 8642da04..544b03e6 100644 --- a/Core/Controllers/ProfileController.cs +++ b/Core/Controllers/ProfileController.cs @@ -1,6 +1,541 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Text.Json; +using Core.Annotations; +using Core.Generators; +using Core.Helpers; +using Core.Models.Eft.Common; +using Core.Models.Eft.Common.Tables; +using Core.Models.Eft.ItemEvent; +using Core.Models.Eft.Launcher; +using Core.Models.Eft.Profile; +using Core.Models.Enums; +using Core.Servers; +using Core.Services; +using Core.Utils; +using Core.Utils.Cloners; + namespace Core.Controllers; +[Injectable] public class ProfileController { - // TODO -} \ No newline at end of file + protected Models.Utils.ILogger _logger; + + protected HashUtil _hashUtil; + protected ICloner _cloner; + protected TimeUtil _timeUtil; + protected SaveServer _saveServer; + protected DatabaseService _databaseService; + protected ItemHelper _itemHelper; + protected ProfileFixerService _profileFixerService; + protected LocalisationService _localisationService; + + protected SeasonalEventService _seasonalEventService; + // TODO: MailSendService mailSendService: MailSendService + protected PlayerScavGenerator _playerScavGenerator; + // TODO: EventOutputHolder eventOutputHolder: EventOutputHolder + protected TraderHelper _traderHelper; + protected DialogueHelper _dialogueHelper; + protected QuestHelper _questHelper; + protected ProfileHelper _profileHelper; + + public ProfileController( + Models.Utils.ILogger logger, + HashUtil hashUtil, + ICloner cloner, + TimeUtil timeUtil, + SaveServer saveServer, + DatabaseService databaseService, + ItemHelper itemHelper, + ProfileFixerService profileFixerService, + LocalisationService localisationService, + SeasonalEventService seasonalEventService, + // TODO: MailSendService mailSendService: MailSendService, + PlayerScavGenerator playerScavGenerator, + // TODO: EventOutputHolder eventOutputHolder: EventOutputHolder, + TraderHelper traderHelper, + DialogueHelper dialogueHelper, + QuestHelper questHelper, + ProfileHelper profileHelper + ) + { + _logger = logger; + _cloner = cloner; + _hashUtil = hashUtil; + _timeUtil = timeUtil; + _saveServer = saveServer; + _databaseService = databaseService; + _itemHelper = itemHelper; + _profileFixerService = profileFixerService; + _localisationService = localisationService; + _seasonalEventService = seasonalEventService; + _playerScavGenerator = playerScavGenerator; + _traderHelper = traderHelper; + _dialogueHelper = dialogueHelper; + _questHelper = questHelper; + _profileHelper = profileHelper; + } + + /** + * Handle /launcher/profiles + */ + public List GetMiniProfiles() + { + return _saveServer.GetProfiles().Select(kv => GetMiniProfile(kv.Key)).ToList(); + } + + /** + * Handle launcher/profile/info + */ + public MiniProfile GetMiniProfile(string sessionID) + { + var profile = _saveServer.GetProfile(sessionID); + if (profile?.CharacterData == null) { + throw new Exception($"Unable to find character data for id: {sessionID}. Profile may be corrupt"); + } + + var pmc = profile.CharacterData.PmcData; + var maxlvl = _profileHelper.GetMaxLevel(); + + // Player hasn't completed profile creation process, send defaults + if (pmc?.Info?.Level == null) { + return new MiniProfile(){ + Username = profile.ProfileInfo?.UserName ?? "", + Nickname = "unknown", + Side= "unknown", + CurrentLevel= 0, + CurrentExperience= 0, + PreviousExperience= 0, + NextLevel= 0, + MaxLevel= maxlvl, + Edition= profile.ProfileInfo?.Edition ?? "", + ProfileId= profile.ProfileInfo?.ProfileId ?? "", + SptData= _profileHelper.GetDefaultSptDataObject(), + }; + } + + var currlvl = pmc.Info.Level; + var nextlvl = _profileHelper.GetExperience((int)(currlvl + 1)); + return new MiniProfile(){ + Username= profile.ProfileInfo.UserName, + Nickname= pmc.Info.Nickname, + Side= pmc.Info.Side, + CurrentLevel= (int) (pmc.Info.Level), + CurrentExperience= (int) (pmc.Info.Experience ?? 0), + PreviousExperience= currlvl == 0 ? 0 : _profileHelper.GetExperience((int) currlvl), + NextLevel= nextlvl, + MaxLevel= maxlvl, + Edition= profile.ProfileInfo?.Edition ?? "", + ProfileId= profile.ProfileInfo?.ProfileId ?? "", + SptData= profile.SptData, + }; + } + + /** + * Handle client/game/profile/list + */ + public List GetCompleteProfile(string sessionID) + { + return _profileHelper.GetCompleteProfile(sessionID); + } + + /** + * Handle client/game/profile/create + * @param info Client reqeust object + * @param sessionID Player id + * @returns Profiles _id value + */ + public string CreateProfile(ProfileCreateRequestData info, string sessionID) + { + var account = _saveServer.GetProfile(sessionID).ProfileInfo; + var profileTemplate = _cloner.Clone(_databaseService.GetProfiles()?[account.Edition]?[info.Side.ToLower()]); + var pmcData = profileTemplate.Character; + + // Delete existing profile + DeleteProfileBySessionId(sessionID); + // PMC + pmcData.Id = account.ProfileId; + pmcData.Aid = account.Aid; + pmcData.Savage = account.ScavengerId; + pmcData.SessionId = sessionID; + pmcData.Info.Nickname = info.Nickname; + pmcData.Info.LowerNickname = account.UserName.ToLower(); + pmcData.Info.RegistrationDate = _timeUtil.GetTimeStamp(); + pmcData.Info.Voice = _databaseService.GetCustomization()[info.VoiceId].Name; + pmcData.Stats = _profileHelper.GetDefaultCounters(); + pmcData.Info.NeedWipeOptions = []; + pmcData.Customization.Head = info.HeadId; + pmcData.Health.UpdateTime = _timeUtil.GetTimeStamp(); + pmcData.Quests = []; + pmcData.Hideout.Seed = _timeUtil.GetTimeStamp() + 8 * 60 * 60 * 24 * 365; // 8 years in future why? who knows, we saw it in live + pmcData.RepeatableQuests = []; + pmcData.CarExtractCounts = new(); + pmcData.CoopExtractCounts = new(); + pmcData.Achievements = new(); + + UpdateInventoryEquipmentId(pmcData); + + if (pmcData.UnlockedInfo == null) { + pmcData.UnlockedInfo = new UnlockedInfo { UnlockedProductionRecipe = [] }; + } + + // Add required items to pmc stash + AddMissingInternalContainersToProfile(pmcData); + + // Change item IDs to be unique + pmcData.Inventory.Items = _itemHelper.ReplaceIDs( + pmcData.Inventory.Items, + pmcData, + null, + pmcData.Inventory.FastPanel + ); + + // Create profile + var profileDetails = new SptProfile { + ProfileInfo= account, + CharacterData= new Characters { PmcData = pmcData, ScavData = new()}, + Suits= profileTemplate.Suits, + UserBuildData= profileTemplate.UserBuilds, + DialogueRecords= profileTemplate.Dialogues, + SptData= _profileHelper.GetDefaultSptDataObject(), + VitalityData= new(), + InraidData= new (), + InsuranceList= [], + TraderPurchases= new(), + PlayerAchievements= new(), + FriendProfileIds= [], + }; + + _profileFixerService.CheckForAndFixPmcProfileIssues(profileDetails.CharacterData.PmcData); + + _saveServer.AddProfile(profileDetails); + + if (profileTemplate.Trader.SetQuestsAvailableForStart ?? false) { + _questHelper.AddAllQuestsToProfile(profileDetails.CharacterData.PmcData, [QuestStatusEnum.AvailableForStart]); + } + + // Profile is flagged as wanting quests set to ready to hand in and collect rewards + if (profileTemplate.Trader.SetQuestsAvailableForFinish ?? false) { + _questHelper.AddAllQuestsToProfile(profileDetails.CharacterData.PmcData, [ + QuestStatusEnum.AvailableForStart, + QuestStatusEnum.Started, + QuestStatusEnum.AvailableForFinish, + ]); + + // Make unused response so applyQuestReward works + ItemEventRouterResponse? response = null; // TODO => _eventOutputHolder.GetOutput(sessionID); + + // Add rewards for starting quests to profile + GivePlayerStartingQuestRewards(profileDetails, sessionID, response); + } + + ResetAllTradersInProfile(sessionID); + + _saveServer.GetProfile(sessionID).CharacterData.ScavData = GeneratePlayerScav(sessionID); + + // Store minimal profile and reload it + _saveServer.SaveProfile(sessionID); + _saveServer.LoadProfile(sessionID); + + // Completed account creation + _saveServer.GetProfile(sessionID).ProfileInfo.IsWiped = false; + _saveServer.SaveProfile(sessionID); + + return pmcData.Id; + } + + /** + * make profiles pmcData.Inventory.equipment unique + * @param pmcData Profile to update + */ + protected void UpdateInventoryEquipmentId(PmcData pmcData) + { + var oldEquipmentId = pmcData.Inventory.Equipment; + pmcData.Inventory.Equipment = _hashUtil.Generate(); + + foreach (var item in pmcData.Inventory.Items) { + if (item.ParentId == oldEquipmentId) { + item.ParentId = pmcData.Inventory.Equipment; + continue; + } + + if (item.Id == oldEquipmentId) { + item.Id = pmcData.Inventory.Equipment; + } + } + } + + /** + * Ensure a profile has the necessary internal containers e.g. questRaidItems / sortingTable + * DOES NOT check that stash exists + * @param pmcData Profile to check + */ + protected void AddMissingInternalContainersToProfile(PmcData pmcData) + { + if (!pmcData.Inventory.Items.Any((item) => item.Id == pmcData.Inventory.HideoutCustomizationStashId)) { + pmcData.Inventory.Items.Add(new (){ + Id = pmcData.Inventory.HideoutCustomizationStashId, + Template = ItemTpl.HIDEOUTAREACONTAINER_CUSTOMIZATION, + }); + } + + if (!pmcData.Inventory.Items.Any((item) => item.Id == pmcData.Inventory.SortingTable)) { + pmcData.Inventory.Items.Add(new (){ + Id = pmcData.Inventory.SortingTable, + Template = ItemTpl.SORTINGTABLE_SORTING_TABLE, + }); + } + + if (!pmcData.Inventory.Items.Any((item) => item.Id == pmcData.Inventory.QuestStashItems)) { + pmcData.Inventory.Items.Add(new (){ + Id = pmcData.Inventory.QuestStashItems, + Template = ItemTpl.STASH_QUESTOFFLINE, + }); + } + + if (!pmcData.Inventory.Items.Any((item) => item.Id == pmcData.Inventory.QuestRaidItems)) { + pmcData.Inventory.Items.Add(new (){ + Id = pmcData.Inventory.QuestRaidItems, + Template = ItemTpl.STASH_QUESTRAID, + }); + } + } + + /** + * Delete a profile + * @param sessionID Id of profile to delete + */ + protected void DeleteProfileBySessionId(string sessionID) + { + if (_saveServer.GetProfiles().ContainsKey(sessionID)) { + _saveServer.DeleteProfileById(sessionID); + } else { + _logger.Warning( + _localisationService.GetText("profile-unable_to_find_profile_by_id_cannot_delete", sessionID) + ); + } + } + + /** + * Iterate over all quests in player profile, inspect rewards for the quests current state (accepted/completed) + * and send rewards to them in mail + * @param profileDetails Player profile + * @param sessionID Session id + * @param response Event router response + */ + protected void GivePlayerStartingQuestRewards( + SptProfile profileDetails, + string sessionID, + ItemEventRouterResponse response + ) + { + foreach (var quest in profileDetails.CharacterData.PmcData.Quests) { + var questFromDb = _questHelper.GetQuestFromDb(quest.QId, profileDetails.CharacterData.PmcData); + + // Get messageId of text to send to player as text message in game + // Copy of code from QuestController.acceptQuest() + var messageId = _questHelper.GetMessageIdForQuestStart( + questFromDb.StartedMessageText, + questFromDb.Description + ); + var itemRewards = _questHelper.ApplyQuestReward( + profileDetails.CharacterData.PmcData, + quest.QId, + QuestStatusEnum.Started, + sessionID, + response + ); + + /* TODO: + _mailSendService.sendLocalisedNpcMessageToPlayer( + sessionID, + this.traderHelper.getTraderById(questFromDb.traderId), + MessageType.QUEST_START, + messageId, + itemRewards, + this.timeUtil.getHoursAsSeconds(100), + ); + */ + } + } + + /** + * For each trader reset their state to what a level 1 player would see + * @param sessionId Session id of profile to reset + */ + protected void ResetAllTradersInProfile(string sessionId) + { + foreach (var traderId in _databaseService.GetTraders().Keys) { + _traderHelper.ResetTrader(sessionId, traderId); + } + } + + /** + * Generate a player scav object + * PMC profile MUST exist first before pscav can be generated + * @param sessionID + * @returns IPmcData object + */ + public PmcData GeneratePlayerScav(string sessionID) + { + return _playerScavGenerator.Generate(sessionID); + } + + /** + * Handle client/game/profile/nickname/validate + */ + public string ValidateNickname(ValidateNicknameRequestData info, string sessionID) + { + if (info.Nickname.Length < 3) { + return "tooshort"; + } + + if (_profileHelper.IsNicknameTaken(info, sessionID)) { + return "taken"; + } + + return "OK"; + } + + /** + * Handle client/game/profile/nickname/change event + * Client allows player to adjust their profile name + */ + public string ChangeNickname(ProfileChangeNicknameRequestData info, string sessionID) + { + var output = ValidateNickname(new ValidateNicknameRequestData(){Nickname = info.Nickname}, sessionID); + + if (output == "OK") { + var pmcData = _profileHelper.GetPmcProfile(sessionID); + + pmcData.Info.Nickname = info.Nickname; + pmcData.Info.LowerNickname = info.Nickname.ToLower(); + } + + return output; + } + + /** + * Handle client/game/profile/voice/change event + */ + public void ChangeVoice(ProfileChangeVoiceRequestData info, string sessionID) + { + var pmcData = _profileHelper.GetPmcProfile(sessionID); + pmcData.Info.Voice = info.Voice; + } + + /** + * Handle client/game/profile/search + */ + public List GetFriends(SearchFriendRequestData info, string sessionID) { + // TODO: We should probably rename this method in the next client update + var result = new List(); + + // Find any profiles with a nickname containing the entered name + var allProfiles = _saveServer.GetProfiles().Values; + + foreach (var profile in allProfiles) { + var pmcProfile = profile?.CharacterData?.PmcData; + + if (!pmcProfile?.Info?.LowerNickname?.Contains(info.Nickname.ToLower()) ?? false) { + continue; + } + + result.Add(_profileHelper.GetChatRoomMemberFromPmcProfile(pmcProfile)); + } + + return result; + } + + /** + * Handle client/profile/status + */ + public GetProfileStatusResponseData GetProfileStatus(string sessionId) + { + var account = _saveServer.GetProfile(sessionId).ProfileInfo; + var response = new GetProfileStatusResponseData() { + MaxPveCountExceeded = false, + Profiles = [ + new (){ ProfileId = account.ScavengerId, ProfileToken = null, Status = "Free",Sid = "", Ip = "", Port = 0 }, + new (){ProfileId = account.ProfileId, ProfileToken = null, Status = "Free",Sid = "", Ip = "", Port = 0 }, + ] + }; + + return response; + } + + /** + * Handle client/profile/view + */ + public GetOtherProfileResponse GetOtherProfile(string sessionId, GetOtherProfileRequest request) + { + // Find the profile by the account ID, fall back to the current player if we can't find the account + var profile = _profileHelper.GetFullProfileByAccountId(request.AccountId); + if (profile?.CharacterData?.PmcData == null || profile?.CharacterData?.ScavData == null) { + profile = _profileHelper.GetFullProfile(sessionId); + } + var playerPmc = profile.CharacterData.PmcData; + var playerScav = profile.CharacterData.ScavData; + + return new GetOtherProfileResponse(){ + Id= playerPmc.Id, + Aid= playerPmc.Aid as int?, + Info= { + Nickname= playerPmc.Info.Nickname, + Side= playerPmc.Info.Side, + Experience= playerPmc.Info.Experience as int?, + MemberCategory= playerPmc.Info.MemberCategory as int?, + BannedState= playerPmc.Info.BannedState, + BannedUntil= playerPmc.Info.BannedUntil, + RegistrationDate= playerPmc.Info.RegistrationDate, + }, + Customization= { + Head= playerPmc.Customization.Head, + Body= playerPmc.Customization.Body, + Feet= playerPmc.Customization.Feet, + Hands= playerPmc.Customization.Hands, + Dogtag= playerPmc.Customization.DogTag, + }, + Skills= playerPmc.Skills, + Equipment= { + Id= playerPmc.Inventory.Equipment, + Items= playerPmc.Inventory.Items, + }, + Achievements= playerPmc.Achievements, + FavoriteItems= _profileHelper.GetOtherProfileFavorites(playerPmc), + PmcStats= { + Eft= { + TotalInGameTime= playerPmc.Stats.Eft.TotalInGameTime as int?, + OverAllCounters= playerPmc.Stats.Eft.OverallCounters, + }, + }, + ScavStats= { + Eft= { + TotalInGameTime= playerScav.Stats.Eft.TotalInGameTime as int?, + OverAllCounters= playerScav.Stats.Eft.OverallCounters, + } + } + }; + } + + /** + * Handle client/profile/settings + */ + public bool SetChosenProfileIcon(string sessionId, GetProfileSettingsRequest request ) + { + var profileToUpdate = _profileHelper.GetPmcProfile(sessionId); + if (profileToUpdate == null) { + return false; + } + + if (request.MemberCategory != null) { + profileToUpdate.Info.SelectedMemberCategory = request.MemberCategory as MemberCategory?; + } + + if (request.SquadInviteRestriction != null) { + profileToUpdate.Info.SquadInviteRestriction = request.SquadInviteRestriction; + } + + return true; + } +} diff --git a/Core/Helpers/QuestHelper.cs b/Core/Helpers/QuestHelper.cs index 24e0b9f0..2d9c27a7 100644 --- a/Core/Helpers/QuestHelper.cs +++ b/Core/Helpers/QuestHelper.cs @@ -3,6 +3,7 @@ using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Hideout; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Quests; +using Core.Models.Enums; namespace Core.Helpers; @@ -355,7 +356,7 @@ public class QuestHelper /// Session id /// Response to send back to client /// Array of reward objects - public Item[] ApplyQuestReward(PmcData profileData, string questId, QuestStatus state, string sessionId, ItemEventRouterResponse questResponse) + public Item[] ApplyQuestReward(PmcData profileData, string questId, QuestStatusEnum state, string sessionId, ItemEventRouterResponse questResponse) { throw new NotImplementedException(); } @@ -427,7 +428,7 @@ public class QuestHelper * @param pmcProfile profile to update * @param statuses statuses quests should have */ - public void AddAllQuestsToProfile(PmcData pmcProfile, List statuses) + public void AddAllQuestsToProfile(PmcData pmcProfile, List statuses) { throw new NotImplementedException(); } diff --git a/Core/Models/Eft/Common/Tables/ProfileTemplate.cs b/Core/Models/Eft/Common/Tables/ProfileTemplate.cs index 9e48cd39..35525c46 100644 --- a/Core/Models/Eft/Common/Tables/ProfileTemplate.cs +++ b/Core/Models/Eft/Common/Tables/ProfileTemplate.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Core.Models.Eft.Profile; +using Core.Utils.Extensions; namespace Core.Models.Eft.Common.Tables; @@ -31,6 +32,14 @@ public class ProfileTemplates [JsonPropertyName("SPT Zero to hero")] public ProfileSides? SPTZeroToHero { get; set; } + + public ProfileSides? this[string? lookupKey] + { + get + { + return (ProfileSides?) GetType().GetProperties().SingleOrDefault(p => p.GetJsonName() == lookupKey)?.GetValue(this); + } + } } public class ProfileSides @@ -43,6 +52,14 @@ public class ProfileSides [JsonPropertyName("bear")] public TemplateSide? Bear { get; set; } + + public TemplateSide this[string toLower] + { + get + { + return (TemplateSide?) GetType().GetProperties().SingleOrDefault(p => p.GetJsonName() == toLower)?.GetValue(this); + } + } } public class TemplateSide diff --git a/Core/Models/Eft/Launcher/GetMiniProfileRequestData.cs b/Core/Models/Eft/Launcher/GetMiniProfileRequestData.cs index 58cdf75c..7ca442eb 100644 --- a/Core/Models/Eft/Launcher/GetMiniProfileRequestData.cs +++ b/Core/Models/Eft/Launcher/GetMiniProfileRequestData.cs @@ -1,12 +1,13 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Launcher; -public class GetMiniProfileRequestData +public class GetMiniProfileRequestData : IRequestData { [JsonPropertyName("username")] public string? Username { get; set; } [JsonPropertyName("password")] public string? Password { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Profile/GetOtherProfileRequest.cs b/Core/Models/Eft/Profile/GetOtherProfileRequest.cs index a3892ce3..20388cf1 100644 --- a/Core/Models/Eft/Profile/GetOtherProfileRequest.cs +++ b/Core/Models/Eft/Profile/GetOtherProfileRequest.cs @@ -1,9 +1,10 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Profile; -public class GetOtherProfileRequest +public class GetOtherProfileRequest : IRequestData { [JsonPropertyName("accountId")] public string? AccountId { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Profile/GetProfileSettingsRequest.cs b/Core/Models/Eft/Profile/GetProfileSettingsRequest.cs index 67eb1793..f14e553c 100644 --- a/Core/Models/Eft/Profile/GetProfileSettingsRequest.cs +++ b/Core/Models/Eft/Profile/GetProfileSettingsRequest.cs @@ -1,8 +1,9 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Profile; -public class GetProfileSettingsRequest +public class GetProfileSettingsRequest : IRequestData { /// /// Chosen value for profile.Info.SelectedMemberCategory @@ -12,4 +13,4 @@ public class GetProfileSettingsRequest [JsonPropertyName("squadInviteRestriction")] public bool? SquadInviteRestriction { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Profile/ProfileChangeNicknameRequestData.cs b/Core/Models/Eft/Profile/ProfileChangeNicknameRequestData.cs index ea99b3d3..06e87f70 100644 --- a/Core/Models/Eft/Profile/ProfileChangeNicknameRequestData.cs +++ b/Core/Models/Eft/Profile/ProfileChangeNicknameRequestData.cs @@ -1,9 +1,10 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Profile; -public class ProfileChangeNicknameRequestData +public class ProfileChangeNicknameRequestData : IRequestData { [JsonPropertyName("nickname")] public string? Nickname { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Profile/ProfileChangeVoiceRequestData.cs b/Core/Models/Eft/Profile/ProfileChangeVoiceRequestData.cs index 3293686b..9dab45c2 100644 --- a/Core/Models/Eft/Profile/ProfileChangeVoiceRequestData.cs +++ b/Core/Models/Eft/Profile/ProfileChangeVoiceRequestData.cs @@ -1,9 +1,10 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Profile; -public class ProfileChangeVoiceRequestData +public class ProfileChangeVoiceRequestData : IRequestData { [JsonPropertyName("voice")] public string? Voice { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Profile/ProfileCreateRequestData.cs b/Core/Models/Eft/Profile/ProfileCreateRequestData.cs index 693cfc6b..92dfe29e 100644 --- a/Core/Models/Eft/Profile/ProfileCreateRequestData.cs +++ b/Core/Models/Eft/Profile/ProfileCreateRequestData.cs @@ -1,8 +1,9 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Profile; -public class ProfileCreateRequestData +public class ProfileCreateRequestData : IRequestData { [JsonPropertyName("side")] public string? Side { get; set; } @@ -15,4 +16,4 @@ public class ProfileCreateRequestData [JsonPropertyName("voiceId")] public string? VoiceId { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Profile/SearchFriendRequestData.cs b/Core/Models/Eft/Profile/SearchFriendRequestData.cs index fd2af942..2de4775d 100644 --- a/Core/Models/Eft/Profile/SearchFriendRequestData.cs +++ b/Core/Models/Eft/Profile/SearchFriendRequestData.cs @@ -1,9 +1,10 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Profile; -public class SearchFriendRequestData +public class SearchFriendRequestData : IRequestData { [JsonPropertyName("nickname")] public string? Nickname { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Profile/SptProfile.cs b/Core/Models/Eft/Profile/SptProfile.cs index ef43a2af..265070eb 100644 --- a/Core/Models/Eft/Profile/SptProfile.cs +++ b/Core/Models/Eft/Profile/SptProfile.cs @@ -16,7 +16,7 @@ public class SptProfile /** Clothing purchases */ [JsonPropertyName("suits")] - public List? ClothingPurchases { get; set; } + public List? Suits { get; set; } [JsonPropertyName("userbuilds")] public UserBuilds? UserBuildData { get; set; } diff --git a/Core/Models/Eft/Profile/ValidateNicknameRequestData.cs b/Core/Models/Eft/Profile/ValidateNicknameRequestData.cs index 46e5e43b..cc09b856 100644 --- a/Core/Models/Eft/Profile/ValidateNicknameRequestData.cs +++ b/Core/Models/Eft/Profile/ValidateNicknameRequestData.cs @@ -1,9 +1,10 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Profile; -public class ValidateNicknameRequestData +public class ValidateNicknameRequestData : IRequestData { [JsonPropertyName("nickname")] public string? Nickname { get; set; } -} \ No newline at end of file +} diff --git a/Core/Routers/Static/ProfileStaticRouter.cs b/Core/Routers/Static/ProfileStaticRouter.cs new file mode 100644 index 00000000..a5cd62f8 --- /dev/null +++ b/Core/Routers/Static/ProfileStaticRouter.cs @@ -0,0 +1,123 @@ +using Core.Callbacks; +using Core.DI; +using Core.Models.Eft.Common; +using Core.Models.Eft.Launcher; +using Core.Models.Eft.Profile; +using Core.Utils; + +namespace Core.Routers.Static; + +public class ProfileStaticRouter : StaticRouter +{ + public ProfileStaticRouter(ProfileCallbacks profileCallbacks, JsonUtil jsonUtil) : base( + jsonUtil, + [ + new RouteAction( + "/client/game/profile/create", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.CreateProfile(url, info as ProfileCreateRequestData, sessionID), + typeof(ProfileCreateRequestData)), + new RouteAction( + "/client/game/profile/list", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.GetProfileData(url, info as EmptyRequestData, sessionID), + typeof(EmptyRequestData)), + new RouteAction( + "/client/game/profile/savage/regenerate", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.RegenerateScav(url, info as EmptyRequestData, sessionID), + typeof(EmptyRequestData)), + new RouteAction( + "/client/game/profile/voice/change", + (url, info, sessionID, output) => + profileCallbacks.ChangeVoice(url, info as ProfileChangeVoiceRequestData, sessionID), + typeof(ProfileChangeVoiceRequestData)), + new RouteAction( + "/client/game/profile/nickname/change", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.ChangeNickname(url, info as ProfileChangeNicknameRequestData, sessionID), + typeof(ProfileChangeNicknameRequestData)), + new RouteAction( + "/client/game/profile/nickname/validate", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.ValidateNickname(url, info as ValidateNicknameRequestData, sessionID), + typeof(ValidateNicknameRequestData)), + new RouteAction( + "/client/game/profile/nickname/reserved", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.GetReservedNickname(url, info as EmptyRequestData, sessionID), + typeof(EmptyRequestData)), + new RouteAction( + "/client/profile/status", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.GetProfileStatus(url, info as EmptyRequestData, sessionID), + typeof(EmptyRequestData)), + new RouteAction( + "/client/profile/view", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.GetOtherProfile(url, info as GetOtherProfileRequest, sessionID), + typeof(GetOtherProfileRequest)), + new RouteAction( + "/client/profile/settings", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.GetProfileSettings(url, info as GetProfileSettingsRequest, sessionID), + typeof(GetProfileSettingsRequest)), + new RouteAction( + "/client/game/profile/search", + ( + url, + info, + sessionID, + output + ) => profileCallbacks.SearchFriend(url, info as SearchFriendRequestData, sessionID), + typeof(SearchFriendRequestData)), + new RouteAction( + "/launcher/profile/info", + (url, info, sessionID, output) => + profileCallbacks.GetMiniProfile(url, info as GetMiniProfileRequestData, sessionID), + typeof(GetMiniProfileRequestData)), + new RouteAction( + "/launcher/profiles", + (url, info, sessionID, output) => + profileCallbacks.GetAllMiniProfiles(url, info as EmptyRequestData, sessionID), + typeof(EmptyRequestData)), + ]) + { + } +} diff --git a/Core/Servers/Http/SptHttpListener.cs b/Core/Servers/Http/SptHttpListener.cs index 5c7cbb73..125973c7 100644 --- a/Core/Servers/Http/SptHttpListener.cs +++ b/Core/Servers/Http/SptHttpListener.cs @@ -60,13 +60,27 @@ public class SptHttpListener : IHttpListener // determine if the payload is compressed. All PUT requests are, and POST requests without // debug = 1 are as well. This should be fixed. // let compressed = req.headers["content-encoding"] === "deflate"; - var requestIsCompressed = req.Headers.TryGetValue("requestcompressed", out var compressHeader) && + var requestIsCompressed = !req.Headers.TryGetValue("requestcompressed", out var compressHeader) || compressHeader != "0"; var requestCompressed = req.Method == "PUT" || requestIsCompressed; - var fullTextBody = new StreamReader(req.Body).ReadToEnd(); - ; - var value = requestCompressed ? new StreamReader(new ZLibStream(req.Body, CompressionLevel.SmallestSize, false)).ReadToEnd() : fullTextBody; + var length = req.Headers.ContentLength; + var memory = new Memory(new byte[(int)length]); + req.Body.ReadAsync(memory).AsTask().Wait(); + string value; + if (requestCompressed) + { + using var uncompressedDataStream = new MemoryStream(); + using var compressedDataStream = new MemoryStream(memory.ToArray()); + using var deflateStream = new ZLibStream(compressedDataStream, CompressionMode.Decompress, true); + deflateStream.CopyTo(uncompressedDataStream); + value = Encoding.UTF8.GetString(uncompressedDataStream.ToArray()); + } + else + { + value = Encoding.UTF8.GetString(memory.ToArray()); + } + if (!requestIsCompressed) { _logger.Debug(value, true); } diff --git a/Core/Utils/Cloners/ICloner.cs b/Core/Utils/Cloners/ICloner.cs new file mode 100644 index 00000000..d8a1978f --- /dev/null +++ b/Core/Utils/Cloners/ICloner.cs @@ -0,0 +1,5 @@ +namespace Core.Utils.Cloners; + +public interface ICloner { + public T Clone(T obj); +} diff --git a/Core/Utils/Cloners/JsonCloner.cs b/Core/Utils/Cloners/JsonCloner.cs new file mode 100644 index 00000000..3950b0b3 --- /dev/null +++ b/Core/Utils/Cloners/JsonCloner.cs @@ -0,0 +1,17 @@ +using Core.Annotations; + +namespace Core.Utils.Cloners; + +[Injectable] +public class JsonCloner : ICloner +{ + protected JsonUtil _jsonUtil; + public JsonCloner(JsonUtil jsonUtil) + { + _jsonUtil = jsonUtil; + } + public T Clone(T obj) + { + return _jsonUtil.Deserialize(_jsonUtil.Serialize(obj)); + } +} diff --git a/Core/Utils/HttpResponseUtil.cs b/Core/Utils/HttpResponseUtil.cs index edb218ae..79c439f3 100644 --- a/Core/Utils/HttpResponseUtil.cs +++ b/Core/Utils/HttpResponseUtil.cs @@ -33,9 +33,9 @@ public class HttpResponseUtil new("[\\t]") ]; - protected string ClearString(string s) + protected string ClearString(string? s) { - var value = s; + var value = s ?? ""; foreach (var regex in _cleanupRegexList) { value = regex.Replace(value, string.Empty);