using Dalamud.Plugin.Services; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Comparer; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.User; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Factories; using MareSynchronos.Services.Events; using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; namespace MareSynchronos.PlayerData.Pairs; public sealed class PairManager : DisposableMediatorSubscriberBase { private readonly ConcurrentDictionary _allClientPairs = new(UserDataComparer.Instance); private readonly ConcurrentDictionary _allGroups = new(GroupDataComparer.Instance); private readonly MareConfigService _configurationService; private readonly IContextMenu _dalamudContextMenu; private readonly PairFactory _pairFactory; private Lazy> _directPairsInternal; private Lazy>> _groupPairsInternal; public PairManager(ILogger logger, PairFactory pairFactory, MareConfigService configurationService, MareMediator mediator, IContextMenu dalamudContextMenu) : base(logger, mediator) { _pairFactory = pairFactory; _configurationService = configurationService; _dalamudContextMenu = dalamudContextMenu; Mediator.Subscribe(this, (_) => ClearPairs()); Mediator.Subscribe(this, (_) => ReapplyPairData()); _directPairsInternal = DirectPairsLazy(); _groupPairsInternal = GroupPairsLazy(); _dalamudContextMenu.OnMenuOpened += DalamudContextMenuOnOnOpenGameObjectContextMenu; } public List DirectPairs => _directPairsInternal.Value; public Dictionary> GroupPairs => _groupPairsInternal.Value; public Dictionary Groups => _allGroups.ToDictionary(k => k.Key, k => k.Value); public Pair? LastAddedUser { get; internal set; } public void AddGroup(GroupFullInfoDto dto) { _allGroups[dto.Group] = dto; RecreateLazy(); } public void AddGroupPair(GroupPairFullInfoDto dto) { if (!_allClientPairs.ContainsKey(dto.User)) _allClientPairs[dto.User] = _pairFactory.Create(dto.User); var group = _allGroups[dto.Group]; _allClientPairs[dto.User].GroupPair[group] = dto; RecreateLazy(); } public Pair? GetPairByUID(string uid) { var existingPair = _allClientPairs.FirstOrDefault(f => uid.Equals(f.Key.UID, StringComparison.Ordinal)); if (!Equals(existingPair, default(KeyValuePair))) { return existingPair.Value; } return null; } public void AddUserPair(UserPairDto dto, bool addToLastAddedUser = true) { if (!_allClientPairs.ContainsKey(dto.User)) { _allClientPairs[dto.User] = _pairFactory.Create(dto.User); } else { addToLastAddedUser = false; } _allClientPairs[dto.User].UserPair = dto; if (addToLastAddedUser) LastAddedUser = _allClientPairs[dto.User]; _allClientPairs[dto.User].ApplyLastReceivedData(); RecreateLazy(); } public void ClearPairs() { Logger.LogDebug("Clearing all Pairs"); DisposePairs(); _allClientPairs.Clear(); _allGroups.Clear(); RecreateLazy(); } public List GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.GetPlayerNameHash())).Select(p => p.Value).ToList(); public int GetVisibleUserCount() => _allClientPairs.Count(p => p.Value.IsVisible); public List GetVisibleUsers() => _allClientPairs.Where(p => p.Value.IsVisible).Select(p => p.Key).ToList(); public void MarkPairOffline(UserData user) { if (_allClientPairs.TryGetValue(user, out var pair)) { Mediator.Publish(new ClearProfileDataMessage(pair.UserData)); pair.MarkOffline(); } RecreateLazy(); } public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true) { if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto); Mediator.Publish(new ClearProfileDataMessage(dto.User)); var pair = _allClientPairs[dto.User]; if (pair.HasCachedPlayer) { RecreateLazy(); return; } if (sendNotif && _configurationService.Current.ShowOnlineNotifications && (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null || !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs) && (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote()) || !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs)) { string? note = pair.GetNoteOrName(); var msg = !string.IsNullOrEmpty(note) ? $"{note} ({pair.UserData.AliasOrUID}) is now online" : $"{pair.UserData.AliasOrUID} is now online"; Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5))); } pair.CreateCachedPlayer(dto); RecreateLazy(); } public void ReceiveCharaData(OnlineUserCharaDataDto dto) { if (!_allClientPairs.TryGetValue(dto.User, out var pair)) throw new InvalidOperationException("No user found for " + dto.User); Mediator.Publish(new EventMessage(new Event(pair.UserData, nameof(PairManager), EventSeverity.Informational, "Received Character Data"))); _allClientPairs[dto.User].ApplyData(dto); } public void RemoveGroup(GroupData data) { _allGroups.TryRemove(data, out _); foreach (var item in _allClientPairs.ToList()) { foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).Where(grpPair => GroupDataComparer.Instance.Equals(grpPair.Group, data)).ToList()) { _allClientPairs[item.Key].GroupPair.Remove(grpPair); } if (!_allClientPairs[item.Key].HasAnyConnection() && _allClientPairs.TryRemove(item.Key, out var pair)) { pair.MarkOffline(); } } RecreateLazy(); } public void RemoveGroupPair(GroupPairDto dto) { if (_allClientPairs.TryGetValue(dto.User, out var pair)) { var group = _allGroups[dto.Group]; pair.GroupPair.Remove(group); if (!pair.HasAnyConnection()) { pair.MarkOffline(); _allClientPairs.TryRemove(dto.User, out _); } } RecreateLazy(); } public void RemoveUserPair(UserDto dto) { if (_allClientPairs.TryGetValue(dto.User, out var pair)) { pair.UserPair = null; if (!pair.HasAnyConnection()) { pair.MarkOffline(); _allClientPairs.TryRemove(dto.User, out _); } } RecreateLazy(); } public void SetGroupInfo(GroupInfoDto dto) { _allGroups[dto.Group].Group = dto.Group; _allGroups[dto.Group].Owner = dto.Owner; _allGroups[dto.Group].GroupPermissions = dto.GroupPermissions; RecreateLazy(); } public void UpdatePairPermissions(UserPermissionsDto dto) { if (!_allClientPairs.TryGetValue(dto.User, out var pair)) { throw new InvalidOperationException("No such pair for " + dto); } if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused() || pair.UserPair.OtherPermissions.IsPaired() != dto.Permissions.IsPaired()) { Mediator.Publish(new ClearProfileDataMessage(dto.User)); } pair.UserPair.OtherPermissions = dto.Permissions; Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", pair.UserPair.OtherPermissions.IsPaused(), pair.UserPair.OtherPermissions.IsDisableAnimations(), pair.UserPair.OtherPermissions.IsDisableSounds(), pair.UserPair.OtherPermissions.IsDisableVFX()); if (!pair.IsPaused) pair.ApplyLastReceivedData(); RecreateLazy(); } public void UpdateSelfPairPermissions(UserPermissionsDto dto) { if (!_allClientPairs.TryGetValue(dto.User, out var pair)) { throw new InvalidOperationException("No such pair for " + dto); } if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused() || pair.UserPair.OwnPermissions.IsPaired() != dto.Permissions.IsPaired()) { Mediator.Publish(new ClearProfileDataMessage(dto.User)); } pair.UserPair.OwnPermissions = dto.Permissions; Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", pair.UserPair.OwnPermissions.IsPaused(), pair.UserPair.OwnPermissions.IsDisableAnimations(), pair.UserPair.OwnPermissions.IsDisableSounds(), pair.UserPair.OwnPermissions.IsDisableVFX()); if (!pair.IsPaused) pair.ApplyLastReceivedData(); RecreateLazy(); } internal void ReceiveUploadStatus(UserDto dto) { if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible) { existingPair.SetIsUploading(); } } internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto) { var group = _allGroups[dto.Group]; _allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo; RecreateLazy(); } internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto) { var group = _allGroups[dto.Group]; var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions; _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions; if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations() || prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds() || prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX()) { _allClientPairs[dto.User].ApplyLastReceivedData(); } RecreateLazy(); } internal void SetGroupPermissions(GroupPermissionDto dto) { var prevPermissions = _allGroups[dto.Group].GroupPermissions; _allGroups[dto.Group].GroupPermissions = dto.Permissions; if (prevPermissions.IsDisableAnimations() != dto.Permissions.IsDisableAnimations() || prevPermissions.IsDisableSounds() != dto.Permissions.IsDisableSounds() || prevPermissions.IsDisableVFX() != dto.Permissions.IsDisableVFX()) { RecreateLazy(); var group = _allGroups[dto.Group]; GroupPairs[group].ForEach(p => p.ApplyLastReceivedData()); } RecreateLazy(); } internal void SetGroupStatusInfo(GroupPairUserInfoDto dto) { _allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo; RecreateLazy(); } internal void SetGroupUserPermissions(GroupPairUserPermissionDto dto) { var prevPermissions = _allGroups[dto.Group].GroupUserPermissions; _allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions; if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations() || prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds() || prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX()) { RecreateLazy(); var group = _allGroups[dto.Group]; GroupPairs[group].ForEach(p => p.ApplyLastReceivedData()); } RecreateLazy(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); _dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu; DisposePairs(); } private void DalamudContextMenuOnOnOpenGameObjectContextMenu(Dalamud.Game.Gui.ContextMenu.IMenuOpenedArgs args) { if (args.MenuType == Dalamud.Game.Gui.ContextMenu.ContextMenuType.Inventory) return; if (!_configurationService.Current.EnableRightClickMenus) return; foreach (var pair in _allClientPairs.Where((p => p.Value.IsVisible))) { pair.Value.AddContextMenu(args); } } private Lazy> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value) .Where(k => k.UserPair != null).ToList()); private void DisposePairs() { Logger.LogDebug("Disposing all Pairs"); Parallel.ForEach(_allClientPairs, item => { item.Value.MarkOffline(wait: false); }); RecreateLazy(); } private Lazy>> GroupPairsLazy() { return new Lazy>>(() => { Dictionary> outDict = new(); foreach (var group in _allGroups) { outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).ToList(); } return outDict; }); } private void ReapplyPairData() { foreach (var pair in _allClientPairs.Select(k => k.Value)) { pair.ApplyLastReceivedData(forced: true); } } private void RecreateLazy() { _directPairsInternal = DirectPairsLazy(); _groupPairsInternal = GroupPairsLazy(); } }