using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; using LightlessSync.API.Dto.Group; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using System.Numerics; namespace LightlessSync.UI; public class SyncshellFinderUI : WindowMediatorSubscriberBase { private readonly ApiController _apiController; private readonly BroadcastService _broadcastService; private readonly UiSharedService _uiSharedService; private readonly BroadcastScannerService _broadcastScannerService; private readonly PairManager _pairManager; private readonly DalamudUtilService _dalamudUtilService; private readonly List _nearbySyncshells = []; private List _currentSyncshells = []; private int _selectedNearbyIndex = -1; private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; private DefaultPermissionsDto _ownPermissions = null!; public SyncshellFinderUI( ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollectorService, BroadcastService broadcastService, UiSharedService uiShared, ApiController apiController, BroadcastScannerService broadcastScannerService, PairManager pairManager, DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; _apiController = apiController; _broadcastScannerService = broadcastScannerService; _pairManager = pairManager; _dalamudUtilService = dalamudUtilService; IsOpen = false; SizeConstraints = new() { MinimumSize = new(600, 400), MaximumSize = new(600, 550) }; Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); } public override async void OnOpen() { _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; await RefreshSyncshellsAsync().ConfigureAwait(false); } protected override void DrawInternal() { _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); _uiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); if (_nearbySyncshells.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); if (!_broadcastService.IsBroadcasting) { _uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active."); ImGuiHelpers.ScaledDummy(0.5f); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessYellow2")); if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) { Mediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); } ImGui.PopStyleColor(); ImGui.PopStyleVar(); return; } return; } DrawSyncshellTable(); if (_joinDto != null && _joinInfo != null && _joinInfo.Success) DrawConfirmation(); } private void DrawSyncshellTable() { if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) { ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); ImGui.TableHeadersRow(); foreach (var shell in _nearbySyncshells) { // Check if there is an active broadcast for this syncshell, if not, skipping this syncshell var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts() .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); if (broadcast == null) continue; // no active broadcasts var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); if (string.IsNullOrEmpty(Name)) continue; // broadcaster not found in area, skipping ImGui.TableNextRow(); ImGui.TableNextColumn(); var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; ImGui.TextUnformatted(displayName); ImGui.TableNextColumn(); var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address); var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name; ImGui.TextUnformatted(broadcasterName); ImGui.TableNextColumn(); var label = $"Join##{shell.Group.GID}"; ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); var isRecentlyJoined = _recentlyJoined.Contains(shell.GID); if (!isAlreadyMember && !isRecentlyJoined) { if (ImGui.Button(label)) { _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); _ = Task.Run(async () => { try { var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( shell.Group, shell.Password, shell.GroupUserPreferredPermissions )).ConfigureAwait(false); if (info != null && info.Success) { _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); _joinInfo = info; _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; _logger.LogInformation($"Fetched join info for {shell.Group.GID}"); } else { _logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful"); } } catch (Exception ex) { _logger.LogError(ex, $"Join failed for {shell.Group.GID}"); } }); } } else { using (ImRaii.Disabled()) { ImGui.Button(label); } UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); } ImGui.PopStyleColor(3); } ImGui.EndTable(); } } private void DrawConfirmation() { if (_joinDto != null && _joinInfo != null) { ImGui.Separator(); ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); ImGuiHelpers.ScaledDummy(2f); ImGui.TextUnformatted("Suggested Syncshell Permissions:"); DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v); DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v); DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v); ImGui.NewLine(); ImGui.NewLine(); if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}")) { var finalPermissions = GroupUserPreferredPermissions.NoneSet; finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); _recentlyJoined.Add(_joinDto.Group.GID); _joinDto = null; _joinInfo = null; } } } private void DrawPermissionRow(string label, bool suggested, bool current, Action apply) { ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted($"- {label}"); ImGui.SameLine(150 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted("Current:"); ImGui.SameLine(); _uiSharedService.BooleanToColoredIcon(!current); ImGui.SameLine(300 * ImGuiHelpers.GlobalScale); ImGui.TextUnformatted("Suggested:"); ImGui.SameLine(); _uiSharedService.BooleanToColoredIcon(!suggested); ImGui.SameLine(450 * ImGuiHelpers.GlobalScale); using var id = ImRaii.PushId(label); if (current != suggested) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply")) apply(suggested); } ImGui.NewLine(); } private async Task RefreshSyncshellsAsync() { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; _recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); if (syncshellBroadcasts.Count == 0) { ClearSyncshells(); return; } List? updatedList = []; try { var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); updatedList = groups?.ToList(); } catch (Exception ex) { _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); return; } var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal); if (updatedList != null) { var previousGid = GetSelectedGid(); _nearbySyncshells.Clear(); _nearbySyncshells.AddRange(updatedList); if (previousGid != null) { var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); if (newIndex >= 0) { _selectedNearbyIndex = newIndex; return; } } } ClearSelection(); } private void ClearSyncshells() { if (_nearbySyncshells.Count == 0) return; _nearbySyncshells.Clear(); ClearSelection(); } private void ClearSelection() { _selectedNearbyIndex = -1; _joinDto = null; _joinInfo = null; } private string? GetSelectedGid() { if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count) return null; return _nearbySyncshells[_selectedNearbyIndex].Group.GID; } }