using SptCommon.Annotations; 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; namespace Core.Routers; [Injectable] public class EventOutputHolder { protected ISptLogger _logger; protected ProfileHelper _profileHelper; protected TimeUtil _timeUtil; protected ICloner _cloner; protected Dictionary _outputStore = new(); protected Dictionary> _clientActiveSessionStorage = new(); 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 ItemEventRouterResponse? 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(), RecipeUnlocked = { }, QuestsStatus = [] } } }, Warnings = { } } ); } public void UpdateOutputProperties(string sessionId) { PmcData pmcData = _profileHelper.GetPmcProfile(sessionId); ProfileChange 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); } 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); } } } 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; } 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[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[production.Key]) { storageForSessionId[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 < this._timeUtil.GetTimeStamp()) { limit.NextResetTime += limit.ResetInterval; limit.RemainingLimit = limit.TotalLimit; } } 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, } ); } }