using System.Security.Cryptography; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Generators; using SPTarkov.Server.Core.Helpers; 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.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 SPTarkov.Server.Core.Utils.Json; namespace SPTarkov.Server.Core.Services; [Injectable] public class CreateProfileService( ISptLogger logger, TimeUtil timeUtil, DatabaseService databaseService, ServerLocalisationService serverLocalisationService, 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( MongoId 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.ToLowerInvariant(); 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(); pmcData.WishList = new DictionaryOrList(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(), InraidData = new Inraid(), InsuranceList = [], BtrDeliveryList = [], TraderPurchases = new Dictionary?>(), FriendProfileIds = [], CustomisationUnlocks = [], }; profileDetails.AddCustomisationUnlocksToProfile(); profileDetails.AddSuitsToProfile(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.Value, "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(MongoId sessionID) { if (saveServer.GetProfiles().ContainsKey(sessionID)) { saveServer.DeleteProfileById(sessionID); } else { logger.Warning( serverLocalisationService.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 = new MongoId(); 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.Value; } } } /// /// For each trader reset their state to what a level 1 player would see /// /// Session ID of profile to reset protected void ResetAllTradersInProfile(MongoId 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.Value, 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.Value, 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.Value, 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.Value, Template = ItemTpl.STASH_QUESTRAID, } ); } } /// /// 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, MongoId 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) ); } } }