using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; using Core.Models.Utils; using Core.Utils; using Core.Utils.Cloners; using SptCommon.Annotations; namespace Core.Routers; [Injectable] public class EventOutputHolder { protected Dictionary> _clientActiveSessionStorage = new(); protected ICloner _cloner; protected ISptLogger _logger; protected Dictionary _outputStore = new(); protected ProfileHelper _profileHelper; protected TimeUtil _timeUtil; public EventOutputHolder( ISptLogger logger, ProfileHelper profileHelper, TimeUtil timeUtil, ICloner cloner ) { _logger = logger; _profileHelper = profileHelper; _timeUtil = timeUtil; _cloner = cloner; } public ItemEventRouterResponse GetOutput(string sessionId) { var resultFound = _outputStore.TryGetValue(sessionId, out var result); if (resultFound) { return result; } // Nothing found, reset to default ResetOutput(sessionId); _outputStore.TryGetValue(sessionId, out result!); return result; } public void ResetOutput(string sessionId) { var pmcProfile = _profileHelper.GetPmcProfile(sessionId); if (_outputStore.ContainsKey(sessionId)) { _outputStore.Remove(sessionId); } _outputStore.Add( sessionId, new ItemEventRouterResponse { ProfileChanges = new Dictionary { { sessionId, new ProfileChange { Id = sessionId, Experience = pmcProfile.Info.Experience, Quests = [], RagFairOffers = [], WeaponBuilds = [], EquipmentBuilds = [], Items = new ItemChanges { NewItems = [], ChangedItems = [], DeletedItems = [] }, Production = new Dictionary(), Improvements = new Dictionary(), Skills = new Skills { Common = [], Mastering = [], Points = 0 }, Health = _cloner.Clone(pmcProfile.Health), TraderRelations = new Dictionary(), QuestsStatus = [] } } }, Warnings = [] } ); } /// /// Update output object with most recent values from player profile /// /// Session id public void UpdateOutputProperties(string sessionId) { var pmcData = _profileHelper.GetPmcProfile(sessionId); var profileChanges = _outputStore[sessionId].ProfileChanges[sessionId]; profileChanges.Experience = pmcData.Info.Experience; profileChanges.Health = _cloner.Clone(pmcData.Health); profileChanges.Skills.Common = _cloner.Clone(pmcData.Skills.Common); // Always send skills for Item event route response profileChanges.Skills.Mastering = _cloner.Clone(pmcData.Skills.Mastering); // Clone productions to ensure we preseve the profile jsons data profileChanges.Production = GetProductionsFromProfileAndFlagComplete( _cloner.Clone(pmcData.Hideout.Production), sessionId ); profileChanges.Improvements = _cloner.Clone(GetImprovementsFromProfileAndFlagComplete(pmcData)); profileChanges.TraderRelations = ConstructTraderRelations(pmcData.TradersInfo); ResetMoneyTransferLimit(pmcData.MoneyTransferLimitData); profileChanges.MoneyTransferLimitData = pmcData.MoneyTransferLimitData; // Fixes container craft from water collector not resetting after collection + removed completed normal crafts CleanUpCompleteCraftsInProfile(pmcData.Hideout.Production); } /// /// Required as continuous productions don't reset and stay at 100% completion but client thinks it hasn't started /// /// Productions in a profile private void CleanUpCompleteCraftsInProfile(Dictionary? productions) { foreach (var production in productions) { if ((production.Value.SptIsComplete ?? false) && (production.Value.SptIsContinuous ?? false)) { // Water collector / Bitcoin etc production.Value.SptIsComplete = false; production.Value.Progress = 0; production.Value.StartTimestamp = _timeUtil.GetTimeStamp(); } else if (!production.Value.InProgress ?? false) { // Normal completed craft, delete productions.Remove(production.Key); } } } /// /// Return all hideout Improvements from player profile, adjust completed Improvements' completed property to be true /// /// Player profile /// Dictionary of hideout improvements private Dictionary? GetImprovementsFromProfileAndFlagComplete(PmcData pmcData) { foreach (var improvementKey in pmcData.Hideout.Improvements) { var improvement = pmcData.Hideout.Improvements[improvementKey.Key]; // Skip completed if (improvement.Completed ?? false) { continue; } if (improvement.ImproveCompleteTimestamp < _timeUtil.GetTimeStamp()) { improvement.Completed = true; } } return pmcData.Hideout.Improvements; } /// /// Return productions from player profile except those completed crafts the client has already seen /// /// Productions from player profile /// Player session ID /// Dictionary of hideout productions private Dictionary? GetProductionsFromProfileAndFlagComplete(Dictionary? productions, string sessionId) { foreach (var production in productions) { if (production.Value is null) // Could be cancelled production, skip item to save processing { continue; } // Complete and is Continuous e.g. water collector if ((production.Value.SptIsComplete ?? false) && (production.Value.SptIsContinuous ?? false)) { continue; } // Skip completed if (!production.Value.InProgress ?? false) { continue; } // Client informed of craft, remove from data returned Dictionary? storageForSessionId = null; if (!_clientActiveSessionStorage.TryGetValue(sessionId, out storageForSessionId)) { _clientActiveSessionStorage.Add(sessionId, new Dictionary()); storageForSessionId = _clientActiveSessionStorage[sessionId]; } // Ensure we don't inform client of production again if (storageForSessionId.ContainsKey(production.Key)) { productions.Remove(production.Key); continue; } // Flag started craft as having been seen by client so it won't happen subsequent times if (production.Value.Progress > 0 && !storageForSessionId.ContainsKey(production.Key)) { storageForSessionId.TryAdd(production.Key, true); } } // Return undefined if there's no crafts to send to client to match live behaviour return productions.Keys.Count > 0 ? productions : null; } private void ResetMoneyTransferLimit(MoneyTransferLimits limit) { if (limit.NextResetTime < _timeUtil.GetTimeStamp()) { limit.NextResetTime += limit.ResetInterval; limit.RemainingLimit = limit.TotalLimit; } } /// /// Convert the internal trader data object into an object we can send to the client /// /// Server data for traders /// Dict of trader id + TraderData private Dictionary ConstructTraderRelations(Dictionary traderData) { return traderData.ToDictionary( trader => trader.Key, trader => new TraderData { SalesSum = trader.Value.SalesSum, Disabled = trader.Value.Disabled, Loyalty = trader.Value.LoyaltyLevel, Standing = trader.Value.Standing, Unlocked = trader.Value.Unlocked } ); } }