using Core.Annotations; using Core.Context; using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Game; using Core.Models.Eft.Profile; using Core.Models.Enums; using Core.Models.External; using Core.Models.Spt.Config; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; using ILogger = Core.Models.Utils.ILogger; namespace Core.Controllers; [Injectable] public class GameController { private readonly ILogger _logger; private readonly ConfigServer _configServer; private readonly DatabaseService _databaseService; private readonly TimeUtil _timeUtil; // private readonly PreSptModLoader _preSptModLoader; private readonly HttpServerHelper _httpServerHelper; private readonly InventoryHelper _inventoryHelper; private readonly RandomUtil _randomUtil; private readonly HideoutHelper _hideoutHelper; private readonly ProfileHelper _profileHelper; private readonly ProfileFixerService _profileFixerService; private readonly LocalisationService _localisationService; private readonly PostDbLoadService _postDbLoadService; private readonly CustomLocationWaveService _customLocationWaveService; private readonly OpenZoneService _openZoneService; private readonly SeasonalEventService _seasonalEventService; private readonly ItemBaseClassService _itemBaseClassService; private readonly GiftService _giftService; private readonly RaidTimeAdjustmentService _raidTimeAdjustmentService; private readonly ProfileActivityService _profileActivityService; private readonly ApplicationContext _applicationContext; //private readonly PreSptModLoader preSptModLoader private readonly ICloner _cloner; private readonly CoreConfig _coreConfig; private readonly HttpConfig _httpConfig; private readonly RagfairConfig _ragfairConfig; private readonly HideoutConfig _hideoutConfig; private readonly BotConfig _botConfig; public GameController( ILogger logger, ConfigServer configServer, DatabaseService databaseService, TimeUtil timeUtil, HttpServerHelper httpServerHelper, InventoryHelper inventoryHelper, RandomUtil randomUtil, HideoutHelper hideoutHelper, ProfileHelper profileHelper, ProfileFixerService profileFixerService, LocalisationService localisationService, PostDbLoadService postDbLoadService, CustomLocationWaveService customLocationWaveService, OpenZoneService openZoneService, SeasonalEventService seasonalEventService, ItemBaseClassService itemBaseClassService, GiftService giftService, RaidTimeAdjustmentService raidTimeAdjustmentService, ProfileActivityService profileActivityService, ApplicationContext applicationContext, ICloner cloner ) { _logger = logger; _configServer = configServer; _databaseService = databaseService; _timeUtil = timeUtil; _httpServerHelper = httpServerHelper; _inventoryHelper = inventoryHelper; _randomUtil = randomUtil; _hideoutHelper = hideoutHelper; _profileHelper = profileHelper; _profileFixerService = profileFixerService; _localisationService = localisationService; _postDbLoadService = postDbLoadService; _customLocationWaveService = customLocationWaveService; _openZoneService = openZoneService; _seasonalEventService = seasonalEventService; _itemBaseClassService = itemBaseClassService; _giftService = giftService; _raidTimeAdjustmentService = raidTimeAdjustmentService; _profileActivityService = profileActivityService; _applicationContext = applicationContext; _cloner = cloner; _coreConfig = configServer.GetConfig(ConfigTypes.CORE); _httpConfig = configServer.GetConfig(ConfigTypes.HTTP); _ragfairConfig = configServer.GetConfig(ConfigTypes.RAGFAIR); _hideoutConfig = configServer.GetConfig(ConfigTypes.HIDEOUT); _botConfig = configServer.GetConfig(ConfigTypes.BOT); } /// /// Handle client/game/start /// /// /// /// /// public void GameStart(string url, EmptyRequestData info, string sessionId, long startTimeStampMs) { // Store client start time in app context _applicationContext.AddValue(ContextVariableType.CLIENT_START_TIMESTAMP, $"{sessionId}_{startTimeStampMs}"); _profileActivityService.SetActivityTimestamp(sessionId); // repeatableQuests are stored by in profile.Quests due to the responses of the client (e.g. Quests in // offraidData). Since we don't want to clutter the Quests list, we need to remove all completed (failed or // successful) repeatable quests. We also have to remove the Counters from the repeatableQuests if (sessionId != null) { var fullProfile = _profileHelper.GetFullProfile(sessionId); if (fullProfile.ProfileInfo.IsWiped.Value) return; if (fullProfile.SptData.Migrations == null) fullProfile.SptData.Migrations = new(); if (fullProfile.FriendProfileIds == null) fullProfile.FriendProfileIds = new(); if (fullProfile.SptData.Version.Contains("3.9.") && !fullProfile.SptData.Migrations.Any(m => m.Key == "39x")) { _inventoryHelper.ValidateInventoryUsesMongoIds(fullProfile.CharacterData.PmcData.Inventory.Items); Migrate39xProfile(fullProfile); // flag as migrated fullProfile.SptData.Migrations.Add("39x", _timeUtil.GetTimeStamp()); _logger.Info($"Migration of 3.9.x profile: {fullProfile.ProfileInfo.Username} completed successfully"); } // with our method of converting type from array for this prop, we *might* not need this? // if (Array.isArray(fullProfile.characters.pmc.WishList)) { // fullProfile.characters.pmc.WishList = {}; // } // // if (Array.isArray(fullProfile.characters.scav.WishList)) { // fullProfile.characters.scav.WishList = {}; // } if (fullProfile.DialogueRecords != null) _profileFixerService.CheckForAndFixDialogueAttachments(fullProfile); _logger.Debug($"Started game with session {sessionId} {fullProfile.ProfileInfo.Username}"); var pmcProfile = fullProfile.CharacterData.PmcData; if (_coreConfig.Fixes.FixProfileBreakingInventoryItemIssues) _profileFixerService.FixProfileBreakingInventoryItemIssues(pmcProfile); if (pmcProfile.Health != null) UpdateProfileHealthValues(pmcProfile); if (pmcProfile.Inventory != null) { SendPraporGiftsToNewProfiles(pmcProfile); _profileFixerService.CheckForOrphanedModdedItems(sessionId, fullProfile); } _profileFixerService.CheckForAndRemoveInvalidTraders(fullProfile); _profileFixerService.CheckForAndFixPmcProfileIssues(pmcProfile); if (pmcProfile.Hideout != null) { _profileFixerService.AddMissingHideoutBonusesToProfile(pmcProfile); _hideoutHelper.SetHideoutImprovementsToCompleted(pmcProfile); _hideoutHelper.UnlockHideoutWallInProfile(pmcProfile); } LogProfileDetails(fullProfile); SaveActiveModsToProfile(fullProfile); if (pmcProfile.Info != null) { AddPlayerToPmcNames(pmcProfile); CheckForAndRemoveUndefinedDialogues(fullProfile); } if (pmcProfile.Skills.Common != null) WarnOnActiveBotReloadSkill(pmcProfile); _seasonalEventService.GivePlayerSeasonalGifts(sessionId); } } private void Migrate39xProfile(SptProfile fullProfile) { throw new NotImplementedException(); } /// /// Handles migrating profiles from older SPT versions /// /// /// Formerly migrate39xProfile in node server private void MigrateProfile(SptProfile fullProfile) { throw new NotImplementedException(); } /// /// Handle client/game/config /// /// /// public GameConfigResponse GetGameConfig(string sessionId) { var profile = _profileHelper.GetPmcProfile(sessionId); var gameTime = profile?.Stats?.Eft?.OverallCounters?.Items.FirstOrDefault(c => c.Key.Contains("LifeTime") && c.Key.Contains("Pmc")).Value ?? 0D; var config = new GameConfigResponse { Languages = _databaseService.GetLocales().Languages, IsNdaFree = false, IsReportAvailable = false, IsTwitchEventMember = false, Language = "en", Aid = profile.Aid, Taxonomy = 6, ActiveProfileId = sessionId, Backend = new() { Lobby = _httpServerHelper.GetBackendUrl(), Trading = _httpServerHelper.GetBackendUrl(), Messaging = _httpServerHelper.GetBackendUrl(), Main = _httpServerHelper.GetBackendUrl(), RagFair = _httpServerHelper.GetBackendUrl() }, UseProtobuf = false, UtcTime = _timeUtil.GetTimeStamp(), TotalInGame = gameTime, SessionMode = "pve", PurchasedGames = new() { IsEftPurchased = true, IsArenaPurchased = false } }; return config; } /// /// Handle client/game/mode /// /// /// /// public GameModeResponse GetGameMode( string sessionId, GameModeRequestData requestData) { return new() { GameMode = "pve", BackendUrl = "127.0.0.1:6969" }; } /// /// Handle client/server/list /// /// /// public List GetServer(string sessionId) { return [ new ServerDetails { Ip = _httpConfig.BackendIp, Port = _httpConfig.BackendPort } ]; } /// /// Handle client/match/group/current /// /// /// public CurrentGroupResponse GetCurrentGroup(string sessionId) { return new CurrentGroupResponse { Squad = [] }; } /// /// Handle client/checkVersion /// /// /// public CheckVersionResponse GetValidGameVersion(string sessionId) { return new CheckVersionResponse { IsValid = true, LatestVersion = _coreConfig.CompatibleTarkovVersion }; } /// /// Handle client/game/keepalive /// /// /// public GameKeepAliveResponse GetKeepAlive(string sessionId) { _profileActivityService.SetActivityTimestamp(sessionId); return new GameKeepAliveResponse(){ Message = "OK", UtcTime = _timeUtil.GetTimeStamp() }; } /// /// Handle singleplayer/settings/getRaidTime /// /// /// /// public GetRaidTimeResponse GetRaidTime( string sessionId, GetRaidTimeRequest request) { throw new NotImplementedException(); } /// /// /// /// /// public SurveyResponseData GetSurvey(string sessionId) { return this._coreConfig.Survey; } /// /// Players set botReload to a high value and don't expect the crazy fast reload speeds, give them a warn about it /// /// Player profile private void WarnOnActiveBotReloadSkill(PmcData pmcProfile) { var botReloadSkill = _profileHelper.GetSkillFromProfile(pmcProfile, SkillTypes.BotReload); if (botReloadSkill?.Progress > 0) { _logger.Warning(_localisationService.GetText("server_start_player_active_botreload_skill")); } } /// /// When player logs in, iterate over all active effects and reduce timer /// /// Profile to adjust values for private void UpdateProfileHealthValues(PmcData pmcProfile) { var healthLastUpdated = pmcProfile.Health.UpdateTime; var currentTimeStamp = _timeUtil.GetTimeStamp(); var diffSeconds = currentTimeStamp - healthLastUpdated; // Last update is in past if (healthLastUpdated < currentTimeStamp) { // Base values double energyRegenPerHour = 60; double hydrationRegenPerHour = 60; double hpRegenPerHour = 456.6; // Set new values, whatever is smallest energyRegenPerHour += pmcProfile.Bonuses .Where((bonus) => bonus.Type == BonusType.EnergyRegeneration) .Aggregate(0d, (sum, bonus) => sum + (bonus.Value.Value)); hydrationRegenPerHour += pmcProfile.Bonuses .Where((bonus) => bonus.Type == BonusType.HydrationRegeneration) .Aggregate(0d, (sum, bonus) => sum + (bonus.Value.Value)); hpRegenPerHour += pmcProfile.Bonuses .Where((bonus) => bonus.Type == BonusType.HealthRegeneration) .Aggregate(0d, (sum, bonus) => sum + (bonus.Value.Value)); // Player has energy deficit if (pmcProfile.Health.Energy.Current != pmcProfile.Health.Energy.Maximum) { // Set new value, whatever is smallest pmcProfile.Health.Energy.Current += Math.Round(energyRegenPerHour * (diffSeconds.Value / 3600)); if (pmcProfile.Health.Energy.Current > pmcProfile.Health.Energy.Maximum) { pmcProfile.Health.Energy.Current = pmcProfile.Health.Energy.Maximum; } } // Player has hydration deficit if (pmcProfile.Health.Hydration.Current != pmcProfile.Health.Hydration.Maximum) { pmcProfile.Health.Hydration.Current += Math.Round(hydrationRegenPerHour * (diffSeconds.Value / 3600)); if (pmcProfile.Health.Hydration.Current > pmcProfile.Health.Hydration.Maximum) { pmcProfile.Health.Hydration.Current = pmcProfile.Health.Hydration.Maximum; } } // Check all body parts foreach (var bodyPart in pmcProfile.Health.BodyParts .Select(bodyPartKvP => bodyPartKvP.Value)) { // Check part hp if (bodyPart.Health.Current < bodyPart.Health.Maximum) { bodyPart.Health.Current += Math.Round(hpRegenPerHour * (diffSeconds.Value / 3600)); } if (bodyPart.Health.Current > bodyPart.Health.Maximum) { bodyPart.Health.Current = bodyPart.Health.Maximum; } if (bodyPart.Effects is null || bodyPart.Effects.Count == 0) { continue; } // Look for effects foreach (var effectKvP in bodyPart.Effects) { // remove effects below 1, .e.g. bleeds at -1 if (effectKvP.Value.Time < 1) { // More than 30 mins has passed if (diffSeconds > 1800) { bodyPart.Effects.Remove(effectKvP.Key); } continue; } // Decrement effect time value by difference between current time and time health was last updated effectKvP.Value.Time -= diffSeconds; if (effectKvP.Value.Time < 1) { // Effect time was sub 1, set floor it can be effectKvP.Value.Time = 1; } } } // Update both values as they've both been updated pmcProfile.Health.UpdateTime = currentTimeStamp; } } /// /// Send starting gifts to profile after x days /// /// Profile to add gifts to private void SendPraporGiftsToNewProfiles(PmcData pmcProfile) { var timeStampProfileCreated = pmcProfile.Info.RegistrationDate; var oneDaySeconds = _timeUtil.GetHoursAsSeconds(24); var currentTimeStamp = _timeUtil.GetTimeStamp(); // One day post-profile creation if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds) { _giftService.SendPraporStartingGift(pmcProfile.SessionId, 1); } // Two day post-profile creation if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds * 2) { _giftService.SendPraporStartingGift(pmcProfile.SessionId, 2); } } /// /// Get a list of installed mods and save their details to the profile being used /// /// Profile to add mod details to private void SaveActiveModsToProfile(SptProfile fullProfile) { // Add empty mod array if undefined if (fullProfile.SptData.Mods is null) { fullProfile.SptData.Mods = []; } // Get active mods //var activeMods = _preSptModLoader.GetImportedModDetails(); //TODO IMPLEMENT _preSptModLoader var activeMods = new Dictionary(); foreach (var modKvP in activeMods) { var modDetails = modKvP.Value; if ( fullProfile.SptData.Mods.Any( (mod) => mod.Author == modDetails.Author && mod.Name == modDetails.Name && mod.Version == modDetails.Version)) { // Exists already, skip continue; } fullProfile.SptData.Mods.Add( new ModDetails{ Author = modDetails.Author, DateAdded = _timeUtil.GetTimeStamp(), Name = modDetails.Name, Version = modDetails.Version, Url = modDetails.Url, }); } } /// /// Add the logged in players name to PMC name pool /// /// Profile of player to get name from private void AddPlayerToPmcNames(PmcData pmcProfile) { var playerName = pmcProfile.Info.Nickname; if (playerName is not null) { var bots = _databaseService.GetBots().Types; // Official names can only be 15 chars in length if (playerName.Length > _botConfig.BotNameLengthLimit) { return; } // Skip if player name exists already if (bots.TryGetValue("bear", out var bearBot)) { if (bearBot is not null && bearBot.FirstNames.Any(x => x == playerName)) { bearBot.FirstNames.Add(playerName); } } if (bots.TryGetValue("bear", out var usecBot)) { if (usecBot is not null && usecBot.FirstNames.Any(x => x == playerName)) { usecBot.FirstNames.Add(playerName); } } } } /// /// Check for a dialog with the key 'undefined', and remove it /// /// Profile to check for dialog in private void CheckForAndRemoveUndefinedDialogues(SptProfile fullProfile) { throw new NotImplementedException(); } /// /// /// /// private void LogProfileDetails(SptProfile fullProfile) { throw new NotImplementedException(); } public void Load() { _postDbLoadService.PerformPostDbLoadActions(); } }