using System.Security.Cryptography; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Generators; using SPTarkov.Server.Core.Helpers; 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.Eft.Profile; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Routers; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; using Vitality = SPTarkov.Server.Core.Models.Eft.Profile.Vitality; namespace SPTarkov.Server.Core.Services; [Injectable] public class CreateProfileService( ISptLogger _logger, TimeUtil _timeUtil, HashUtil _hashUtil, DatabaseService _databaseService, LocalisationService _localisationService, ProfileHelper _profileHelper, ItemHelper _itemHelper, TraderHelper _traderHelper, QuestHelper _questHelper, QuestRewardHelper _questRewardHelper, PrestigeHelper _prestigeHelper, RewardHelper _rewardHelper, ProfileFixerService _profileFixerService, SaveServer _saveServer, EventOutputHolder _eventOutputHolder, PlayerScavGenerator _playerScavGenerator, ICloner _cloner, MailSendService _mailSendService ) { public async ValueTask CreateProfile(string sessionId, ProfileCreateRequestData request) { var account = _cloner.Clone(_saveServer.GetProfile(sessionId)); var profileTemplateClone = _cloner.Clone( _profileHelper.GetProfileTemplateForSide(account.ProfileInfo.Edition, request.Side) ); var pmcData = profileTemplateClone.Character; // Delete existing profile DeleteProfileBySessionId(sessionId); // PMC pmcData.Id = account.ProfileInfo.ProfileId; pmcData.Aid = account.ProfileInfo.Aid; pmcData.Savage = account.ProfileInfo.ScavengerId; pmcData.SessionId = sessionId; pmcData.Info.Nickname = request.Nickname; pmcData.Info.LowerNickname = request.Nickname.ToLower(); pmcData.Info.RegistrationDate = (int)_timeUtil.GetTimeStamp(); pmcData.Info.Voice = _databaseService.GetCustomization()[request.VoiceId].Name; pmcData.Stats = _profileHelper.GetDefaultCounters(); pmcData.Info.NeedWipeOptions = []; pmcData.Customization.Head = request.HeadId; pmcData.Health.UpdateTime = _timeUtil.GetTimeStamp(); pmcData.Quests = []; pmcData.Hideout.Seed = Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(16)); pmcData.RepeatableQuests = []; pmcData.CarExtractCounts = new Dictionary(); pmcData.CoopExtractCounts = new Dictionary(); pmcData.Achievements = new Dictionary(); // Process handling if the account has been forced to wipe // BSG keeps both the achievements, prestige level and the total in-game time in a wipe if (account.CharacterData.PmcData.Achievements is not null) { pmcData.Achievements = account.CharacterData.PmcData.Achievements; } if (account.CharacterData.PmcData.Prestige is not null) { pmcData.Prestige = account.CharacterData.PmcData.Prestige; pmcData.Info.PrestigeLevel = account.CharacterData.PmcData.Info.PrestigeLevel; } if (account.CharacterData?.PmcData?.Stats?.Eft is not null) { if (pmcData.Stats.Eft is not null) { pmcData.Stats.Eft.TotalInGameTime = account .CharacterData .PmcData .Stats .Eft .TotalInGameTime; } } UpdateInventoryEquipmentId(pmcData); pmcData.UnlockedInfo ??= new UnlockedInfo { UnlockedProductionRecipe = [] }; // Add required items to pmc stash AddMissingInternalContainersToProfile(pmcData); // Change item IDs to be unique _itemHelper.ReplaceProfileInventoryIds(pmcData.Inventory); // Create profile var profileDetails = new SptProfile { ProfileInfo = account.ProfileInfo, CharacterData = new Characters { PmcData = pmcData, ScavData = new PmcData() }, UserBuildData = profileTemplateClone.UserBuilds, DialogueRecords = profileTemplateClone.Dialogues, SptData = _profileHelper.GetDefaultSptDataObject(), VitalityData = new Vitality(), InraidData = new Inraid(), InsuranceList = [], BtrDeliveryList = [], TraderPurchases = new Dictionary?>(), FriendProfileIds = [], CustomisationUnlocks = [], }; AddCustomisationUnlocksToProfile(profileDetails); _traderHelper.AddSuitsToProfile(profileDetails, profileTemplateClone.Suits); _profileFixerService.CheckForAndFixPmcProfileIssues(profileDetails.CharacterData.PmcData); if (profileDetails.CharacterData.PmcData.Achievements.Count > 0) { var achievementsDb = _databaseService.GetTemplates().Achievements; var achievementRewardItemsToSend = new List(); foreach (var (achievementId, _) in profileDetails.CharacterData.PmcData.Achievements) { var rewards = achievementsDb .FirstOrDefault(achievementDb => achievementDb.Id == achievementId) ?.Rewards; if (rewards is null) { continue; } achievementRewardItemsToSend.AddRange( _rewardHelper.ApplyRewards( rewards, CustomisationSource.ACHIEVEMENT, profileDetails, profileDetails.CharacterData.PmcData, achievementId ) ); } if (achievementRewardItemsToSend.Count > 0) { _mailSendService.SendLocalisedSystemMessageToPlayer( profileDetails.ProfileInfo.ProfileId, "670547bb5fa0b1a7c30d5836 0", achievementRewardItemsToSend, [], 31536000 ); } } // Process handling if the account is forced to prestige, or if the account currently has any pending prestiges if ( request.SptForcePrestigeLevel is not null || account.SptData?.PendingPrestige is not null ) { var pendingPrestige = account.SptData.PendingPrestige is not null ? account.SptData.PendingPrestige : new PendingPrestige { PrestigeLevel = request.SptForcePrestigeLevel }; _prestigeHelper.ProcessPendingPrestige(account, profileDetails, pendingPrestige); } _saveServer.AddProfile(profileDetails); if (profileTemplateClone.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 (profileTemplateClone.Trader.SetQuestsAvailableForFinish ?? false) { _questHelper.AddAllQuestsToProfile( profileDetails.CharacterData.PmcData, [ QuestStatusEnum.AvailableForStart, QuestStatusEnum.Started, QuestStatusEnum.AvailableForFinish, ] ); // Make unused response so applyQuestReward works var response = _eventOutputHolder.GetOutput(sessionId); // Add rewards for starting quests to profile GivePlayerStartingQuestRewards(profileDetails, sessionId, response); } ResetAllTradersInProfile(sessionId); _saveServer.GetProfile(sessionId).CharacterData.ScavData = _playerScavGenerator.Generate( sessionId ); // Store minimal profile and reload it await _saveServer.SaveProfileAsync(sessionId); await _saveServer.LoadProfileAsync(sessionId); // Completed account creation _saveServer.GetProfile(sessionId).ProfileInfo.IsWiped = false; await _saveServer.SaveProfileAsync(sessionId); return pmcData.Id; } /// /// Delete a profile /// /// 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 ) ); } } /// /// Make profiles pmcData.Inventory.equipment unique /// /// 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; } } } /// /// For each trader reset their state to what a level 1 player would see /// /// Session ID of profile to reset protected void ResetAllTradersInProfile(string sessionId) { foreach (var traderId in _databaseService.GetTraders().Keys) { _traderHelper.ResetTrader(sessionId, traderId); } } /// /// Ensure a profile has the necessary internal containers e.g. questRaidItems / sortingTable
/// DOES NOT check that stash exists ///
/// Profile to check protected void AddMissingInternalContainersToProfile(PmcData pmcData) { if ( !pmcData.Inventory.Items.Any(item => item.Id == pmcData.Inventory.HideoutCustomizationStashId ) ) { pmcData.Inventory.Items.Add( new Item { Id = pmcData.Inventory.HideoutCustomizationStashId, Template = ItemTpl.HIDEOUTAREACONTAINER_CUSTOMIZATION, } ); } if (!pmcData.Inventory.Items.Any(item => item.Id == pmcData.Inventory.SortingTable)) { pmcData.Inventory.Items.Add( new Item { 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 Item { Id = pmcData.Inventory.QuestStashItems, Template = ItemTpl.STASH_QUESTOFFLINE, } ); } if (!pmcData.Inventory.Items.Any(item => item.Id == pmcData.Inventory.QuestRaidItems)) { pmcData.Inventory.Items.Add( new Item { Id = pmcData.Inventory.QuestRaidItems, Template = ItemTpl.STASH_QUESTRAID, } ); } } /// /// Add customisations to game profiles based on game edition /// /// Profile to add customisations to public void AddCustomisationUnlocksToProfile(SptProfile fullProfile) { // Some game versions have additional customisation unlocks var gameEdition = GetGameEdition(fullProfile); if (gameEdition is null) { _logger.Error( $"Unable to get Game edition of profile: {fullProfile.ProfileInfo.ProfileId}, skipping customisation unlocks" ); return; } 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, } ); 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 = "67471928d17d6431550563b5", 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 background fullProfile.CustomisationUnlocks.Add( new CustomisationStorage { Id = "675850ba33627edb710b0592", Source = CustomisationSource.DEFAULT, Type = CustomisationType.ENVIRONMENT, } ); break; } var pretigeLevel = fullProfile?.CharacterData?.PmcData?.Info?.PrestigeLevel; if (pretigeLevel is not null) { if (pretigeLevel >= 1) { fullProfile.CustomisationUnlocks.Add( new CustomisationStorage { Id = "674dbf593bee1152d407f005", Source = CustomisationSource.DEFAULT, Type = CustomisationType.DOG_TAG, } ); } if (pretigeLevel >= 2) { fullProfile.CustomisationUnlocks.Add( new CustomisationStorage { Id = "675dcfea7ae1a8792107ca99", Source = CustomisationSource.DEFAULT, Type = CustomisationType.DOG_TAG, } ); } } // Dev profile additions if (fullProfile.ProfileInfo.Edition.ToLower().Contains("developer")) // CyberTark background { fullProfile.CustomisationUnlocks.Add( new CustomisationStorage { Id = "67585108def253bd97084552", Source = CustomisationSource.DEFAULT, Type = CustomisationType.ENVIRONMENT, } ); } } /// /// Get the game edition of a profile chosen on creation in Launcher /// private string? GetGameEdition(SptProfile profile) { var edition = profile.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 = profile.ProfileInfo.Edition; switch (launcherEdition.ToLower()) { case "edge of darkness": return GameEditions.EDGE_OF_DARKNESS; case "unheard": return GameEditions.UNHEARD; default: return GameEditions.STANDARD; } } /// /// Iterate over all quests in player profile, inspect rewards for the quests current state (accepted/completed) /// and send rewards to them in mail /// /// Player profile /// Session ID /// 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 = _questRewardHelper .ApplyQuestReward( profileDetails.CharacterData.PmcData, quest.QId, QuestStatusEnum.Started, sessionID, response ) .ToList(); _mailSendService.SendLocalisedNpcMessageToPlayer( sessionID, questFromDb.TraderId, MessageType.QuestStart, messageId, itemRewards, _timeUtil.GetHoursAsSeconds(100) ); } } }