diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index ff59716..bc32c9c 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -68,7 +68,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber try { var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false); - return cid.ToString().GetBlake3Hash(); + return cid.ToString().GetHash256(); } catch (Exception ex) { diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index db721a2..9d32883 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -46,7 +46,7 @@ public sealed class XivDataAnalyzer if (handle->FileName.Length > 1024) continue; var skeletonName = handle->FileName.ToString(); if (string.IsNullOrEmpty(skeletonName)) continue; - outputIndices[skeletonName] = new(); + outputIndices[skeletonName] = []; for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++) { var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String; @@ -70,7 +70,7 @@ public sealed class XivDataAnalyzer var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash); if (cacheEntity == null) return null; - using BinaryReader reader = new BinaryReader(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); + using BinaryReader reader = new(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); // most of this shit is from vfxeditor, surely nothing will change in the pap format :copium: reader.ReadInt32(); // ignore @@ -177,17 +177,18 @@ public sealed class XivDataAnalyzer } long tris = 0; - for (int i = 0; i < file.LodCount; i++) + foreach (var lod in file.Lods) { try { - var meshIdx = file.Lods[i].MeshIndex; - var meshCnt = file.Lods[i].MeshCount; + var meshIdx = lod.MeshIndex; + var meshCnt = lod.MeshCount; + tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; } catch (Exception ex) { - _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", i, filePath); + _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", lod.MeshIndex, filePath); continue; } diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 3181513..7034bca 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -3,6 +3,7 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; @@ -29,11 +30,15 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private readonly List _nearbySyncshells = []; private List _currentSyncshells = []; private int _selectedNearbyIndex = -1; + private int _syncshellPageIndex = 0; private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; private DefaultPermissionsDto _ownPermissions = null!; + private const bool UseTestSyncshells = true; + + private bool _compactView = false; public SyncshellFinderUI( ILogger logger, @@ -72,9 +77,21 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase protected override void DrawInternal() { + ImGui.BeginGroup(); _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); - UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + ImGui.SameLine(); + string checkboxLabel = "Compact view"; + float availWidth = ImGui.GetContentRegionAvail().X; + float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); + + float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth; + ImGui.SetCursorPosX(rightX); + ImGui.Checkbox(checkboxLabel, ref _compactView); + ImGui.EndGroup(); + + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); if (_nearbySyncshells.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); @@ -104,106 +121,299 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } - DrawSyncshellTable(); + // Build card data (same as you had) + var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); + var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); + + foreach (var shell in _nearbySyncshells) + { + string broadcasterName; + + if (UseTestSyncshells) + { + // Fake broadcaster for test mode + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) + ? shell.Group.Alias + : shell.Group.GID; + + broadcasterName = $"Tester of {displayName}"; + } + else + { + var broadcast = broadcasts + .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); + + if (broadcast == null) + continue; + + var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); + if (string.IsNullOrEmpty(name)) + continue; + + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); + broadcasterName = !string.IsNullOrEmpty(worldName) + ? $"{name} ({worldName})" + : name; + } + + cardData.Add((shell, broadcasterName)); + } + + if (cardData.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); + return; + } + + if (_compactView) + { + DrawSyncshellGrid(cardData); + } + else + { + DrawSyncshellList(cardData); + } + if (_joinDto != null && _joinInfo != null && _joinInfo.Success) DrawConfirmation(); } - private void DrawSyncshellTable() + private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData) { - if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) + const int shellsPerPage = 3; + var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage); + if (totalPages <= 0) + totalPages = 1; + + _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); + + var firstIndex = _syncshellPageIndex * shellsPerPage; + var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count); + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); + + for (int index = firstIndex; index < lastExclusive; index++) { - ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); - ImGui.TableHeadersRow(); + var (shell, broadcasterName) = listData[index]; - 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)); + ImGui.PushID(shell.Group.GID); + float rowHeight = 90f * ImGuiHelpers.GlobalScale; - if (broadcast == null) - continue; // no active broadcasts + ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); - var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); - if (string.IsNullOrEmpty(Name)) - continue; // broadcaster not found in area, skipping + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); + var style = ImGui.GetStyle(); + float startX = ImGui.GetCursorPosX(); + float regionW = ImGui.GetContentRegionAvail().X; + float rightTxtW = ImGui.CalcTextSize(broadcasterName).X; - var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - ImGui.TextUnformatted(displayName); + _uiSharedService.MediumText(displayName, UIColors.Get("PairBlue")); - ImGui.TableNextColumn(); - var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address); - var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name; - ImGui.TextUnformatted(broadcasterName); + float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; + ImGui.SameLine(); + ImGui.SetCursorPosX(rightX); + ImGui.TextUnformatted(broadcasterName); - ImGui.TableNextColumn(); + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); - 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)); + ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); - 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})"); + DrawJoinButton(shell); - _ = Task.Run(async () => - { - try - { - var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( - shell.Group, - shell.Password, - shell.GroupUserPreferredPermissions - )).ConfigureAwait(false); + ImGui.EndChild(); + ImGui.PopID(); - if (info != null && info.Success) - { - _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); - _joinInfo = info; - _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + } - _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.PopStyleVar(2); - ImGui.EndTable(); + DrawPagination(totalPages); + } + + private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData) + { + const int shellsPerPage = 4; + var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage); + if (totalPages <= 0) + totalPages = 1; + + _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); + + var firstIndex = _syncshellPageIndex * shellsPerPage; + var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count); + + var avail = ImGui.GetContentRegionAvail(); + var spacing = ImGui.GetStyle().ItemSpacing; + + var cardWidth = (avail.X - spacing.X) / 2.0f; + var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f; + cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight); + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); + + for (int index = firstIndex; index < lastExclusive; index++) + { + var localIndex = index - firstIndex; + var (shell, broadcasterName) = cardData[index]; + + if (localIndex % 2 != 0) + ImGui.SameLine(); + + ImGui.PushID(shell.Group.GID); + + ImGui.BeginGroup(); + _ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, + new Vector2(cardWidth, cardHeight), + true); + + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) + ? shell.Group.Alias + : shell.Group.GID; + + _uiSharedService.MediumText(displayName + "(200/250)", UIColors.Get("PairBlue")); + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + + ImGui.TextColored(ImGuiColors.DalamudGrey, "Broadcaster"); + ImGui.TextUnformatted(broadcasterName); + + ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); + + var buttonHeight = ImGui.GetFrameHeightWithSpacing(); + var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight; + if (remainingY > 0) + ImGui.Dummy(new Vector2(0, remainingY)); + + DrawJoinButton(shell); + + ImGui.EndChild(); + ImGui.EndGroup(); + + ImGui.PopID(); + } + + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + ImGui.PopStyleVar(2); + + DrawPagination(totalPages); + } + + private void DrawPagination(int totalPages) + { + if (totalPages > 1) + { + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + + var style = ImGui.GetStyle(); + string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}"; + + float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2; + float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2; + float textWidth = ImGui.CalcTextSize(pageLabel).X; + + float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2; + + float availWidth = ImGui.GetContentRegionAvail().X; + float offsetX = (availWidth - totalWidth) * 0.5f; + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX); + + if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0) + _syncshellPageIndex--; + + ImGui.SameLine(); + ImGui.Text(pageLabel); + + ImGui.SameLine(); + if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1) + _syncshellPageIndex++; } } + private void DrawJoinButton(dynamic shell) + { + const string visibleLabel = "Join"; + var label = $"{visibleLabel}##{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); + + Vector2 buttonSize; + + if (!_compactView) + { + var style = ImGui.GetStyle(); + var textSize = ImGui.CalcTextSize(visibleLabel); + + var width = textSize.X + style.FramePadding.X * 20f; + buttonSize = new Vector2(width, 0); + + float availX = ImGui.GetContentRegionAvail().X; + float curX = ImGui.GetCursorPosX(); + float newX = curX + (availX - buttonSize.X); + ImGui.SetCursorPosX(newX); + } + else + { + buttonSize = new Vector2(-1, 0); + } + + if (!isAlreadyMember && !isRecentlyJoined) + { + if (ImGui.Button(label, buttonSize)) + { + _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, buttonSize); + } + + UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); + } + + ImGui.PopStyleColor(3); + } private void DrawConfirmation() { if (_joinDto != null && _joinInfo != null) @@ -267,48 +477,89 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase { 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) + _recentlyJoined.RemoveWhere(gid => + _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); + + List? updatedList = []; + + if (UseTestSyncshells) + { + // ---- TEST DATA PATH ---- + updatedList = BuildTestSyncshells(); + } + else + { + // ---- NORMAL BEHAVIOUR ---- + if (syncshellBroadcasts.Count == 0) + { + ClearSyncshells(); + return; + } + + try + { + var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts) + .ConfigureAwait(false); + updatedList = groups?.ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); + return; + } + } + + if (updatedList == null || updatedList.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 previousGid = GetSelectedGid(); - if (updatedList != null) + _nearbySyncshells.Clear(); + _nearbySyncshells.AddRange(updatedList); + + if (previousGid != null) { - var previousGid = GetSelectedGid(); + var newIndex = _nearbySyncshells.FindIndex(s => + string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - _nearbySyncshells.Clear(); - _nearbySyncshells.AddRange(updatedList); - - if (previousGid != null) + if (newIndex >= 0) { - var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - if (newIndex >= 0) - { - _selectedNearbyIndex = newIndex; - return; - } + _selectedNearbyIndex = newIndex; + return; } } ClearSelection(); } + private List BuildTestSyncshells() + { + var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell"); + var testGroup2 = new GroupData("TEST-BETA", "Beta Shell"); + var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell"); + var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell"); + var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell"); + var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell"); + var testGroup7 = new GroupData("TEST-POINT", "Point Shell"); + var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell"); + + return + [ + new(testGroup1, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup2, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup3, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup4, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup5, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup6, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup7, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup8, "", GroupUserPreferredPermissions.NoneSet), + ]; + } + private void ClearSyncshells() { if (_nearbySyncshells.Count == 0) @@ -321,6 +572,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private void ClearSelection() { _selectedNearbyIndex = -1; + _syncshellPageIndex = 0; _joinDto = null; _joinInfo = null; }