using System.Collections.Concurrent; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Spt.Services; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Services; [Injectable(InjectionType.Singleton)] public class ProfileActivityService(TimeUtil timeUtil) { private readonly ConcurrentDictionary _activeProfiles = []; public void AddActiveProfile(MongoId sessionId, long clientStartedTimestamp) { _activeProfiles.AddOrUpdate( sessionId, // On add value key => new ProfileActivityData { ClientStartedTimestamp = clientStartedTimestamp, LastActive = timeUtil.GetTimeStamp() }, // On Update value, client was started before but crashed or user restarted (key, existingValue) => { existingValue.ClientStartedTimestamp = clientStartedTimestamp; existingValue.LastActive = timeUtil.GetTimeStamp(); existingValue.RaidData = null; return existingValue; } ); } /// /// Does profile exist in activity cache /// /// Profile id to check for /// True when profile exists in cache public bool ContainsActiveProfile(MongoId sessionId) { return _activeProfiles.ContainsKey(sessionId); } /// /// TODO: Yes this is terrible, the other alternative is re-doing half of bot-gen which is currently doing guess-work anyway /// /// ProfileActivityRaidData public ProfileActivityRaidData? GetFirstProfileActivityRaidData() { return !_activeProfiles.IsEmpty ? _activeProfiles.First().Value.RaidData : null; } public ProfileActivityRaidData GetProfileActivityRaidData(MongoId sessionId) { // Handle edge cases where people might close the server but keep the client alive if (!ContainsActiveProfile(sessionId)) { AddActiveProfile(sessionId, timeUtil.GetTimeStamp()); } if (_activeProfiles.TryGetValue(sessionId, out var currentActiveProfile)) { currentActiveProfile.RaidData ??= new(); return currentActiveProfile.RaidData; } throw new Exception($"Unable to retrieve active profile for session: {sessionId}"); } /// /// Was the requested profile active within the last x minutes /// /// Profile to check /// Minutes to check for activity in /// True when profile was active within past x minutes public bool ActiveWithinLastMinutes(MongoId sessionId, int minutes) { if (!_activeProfiles.TryGetValue(sessionId, out var profileActivity)) { // No record, exit early return false; } return timeUtil.GetTimeStamp() - profileActivity.LastActive < minutes * 60; } /// /// Get a list of profile ids that were active in the last x minutes /// /// How many minutes from now to search for profiles /// List of active profile ids public List GetActiveProfileIdsWithinMinutes(int minutes) { var currentTimestamp = timeUtil.GetTimeStamp(); var result = new List(); foreach (var (sessionId, activeProfile) in _activeProfiles) { // Profile was active in last x minutes, add to return list if (currentTimestamp - activeProfile.LastActive < minutes * 60) { result.Add(sessionId); } } return result; } /// /// Update the timestamp a profile was last observed active /// /// Profile to update public void SetActivityTimestamp(MongoId sessionId) { if (_activeProfiles.TryGetValue(sessionId, out var currentActiveProfile)) { currentActiveProfile.LastActive = timeUtil.GetTimeStamp(); } } public IReadOnlyList GetProfileActiveClientMods(MongoId sessionId) { if (!ContainsActiveProfile(sessionId)) { return []; } if (_activeProfiles.TryGetValue(sessionId, out var currentActiveProfile)) { return currentActiveProfile.ActiveClientMods; } throw new Exception($"Unable to retrieve active client mods for session: {sessionId}"); } public void SetProfileActiveClientMods(MongoId sessionId, IReadOnlyList activeClientMods) { if (!ContainsActiveProfile(sessionId)) { AddActiveProfile(sessionId, timeUtil.GetTimeStamp()); } if (_activeProfiles.TryGetValue(sessionId, out var currentActiveProfile)) { currentActiveProfile.ActiveClientMods = activeClientMods; } } }