From 38a360bfeefea46d657d66ead48eee06e47382bf Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Tue, 16 Sep 2025 21:06:15 +0200 Subject: [PATCH 01/69] Fixed some issues with folders not showing, adding empty folders to the list. --- LightlessSync/UI/CompactUI.cs | 8 ++------ .../UI/Components/DrawGroupedGroupFolder.cs | 2 -- LightlessSync/UI/SettingsUi.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 679c5f2..25632a2 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -549,7 +549,7 @@ public class CompactUi : WindowMediatorSubscriberBase bool FilterNotTaggedUsers(KeyValuePair> u) => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyPairTag(u.Key.UserData.UID); bool FilterNotTaggedSyncshells(GroupFullInfoDto group) - => (!_tagHandler.HasAnySyncshellTag(group.GID) && !_configService.Current.ShowGroupedSyncshellsInAll) || true; + => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll; bool FilterOfflineUsers(KeyValuePair> u) => ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) @@ -607,11 +607,7 @@ public class CompactUi : WindowMediatorSubscriberBase syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs)); } } - - if (syncshellFolderTags.Count > 0) - { - drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshelltag)); - } + drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshelltag)); } var allOnlineNotTaggedPairs = ImmutablePairList(allPairs diff --git a/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs b/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs index d1876ac..2aa3d5c 100644 --- a/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs +++ b/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs @@ -34,8 +34,6 @@ public class DrawGroupedGroupFolder : IDrawFolder public void Draw() { - if (!_groups.Any()) return; - string _id = "__folder_syncshells"; if (_tag != "") { diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 3c392ba..2b9de1b 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1145,12 +1145,12 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); - //if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) - //{ - // _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; - // _configService.Save(); - // Mediator.Publish(new RefreshUiMessage()); - //} + if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) + { + _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) -- 2.49.1 From fe898cdc2b7dee6d33a9262c3b13accf29ce8831 Mon Sep 17 00:00:00 2001 From: defnotken Date: Wed, 17 Sep 2025 19:58:25 -0500 Subject: [PATCH 02/69] Start 1.11.13 --- LightlessSync/FileCache/FileCacheManager.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 34e5a5b..51487f6 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -189,7 +189,14 @@ public sealed class FileCacheManager : IHostedService Parallel.ForEach(allEntities, entity => { - cacheDict[entity.PrefixedFilePath] = entity; + if (entity != null && entity.PrefixedFilePath != null) + { + cacheDict[entity.PrefixedFilePath] = entity; + } + else + { + _logger.LogWarning("Null FileCacheEntity or PrefixedFilePath encountered in cache population: {entity}", entity); + } }); var cleanedPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); -- 2.49.1 From bd2c3a4a8010cde7c5e8a4a61d1d588476a5d684 Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 18 Sep 2025 22:03:40 -0500 Subject: [PATCH 03/69] add cache logging to 1.11.13 --- LightlessSync/FileCache/FileCacheManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 51487f6..17f22de 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -260,6 +260,7 @@ public sealed class FileCacheManager : IHostedService { if (_fileCaches.TryGetValue(hash, out var caches)) { + _logger.LogTrace("Removing from DB: {hash} => {path}", hash, prefixedFilePath); var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal)); _logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath); @@ -404,6 +405,12 @@ public sealed class FileCacheManager : IHostedService private FileCacheEntity? Validate(FileCacheEntity fileCache) { + if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath)) + { + _logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath); + RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + return null; + } var file = new FileInfo(fileCache.ResolvedFilepath); if (!file.Exists) { -- 2.49.1 From 9eb2309018bd3b3d4ed7d2381856baa4657cf2c7 Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 24 Sep 2025 05:53:22 +0900 Subject: [PATCH 04/69] lightfinder! --- LightlessAPI | 2 +- .../Configurations/LightlessConfig.cs | 5 + LightlessSync/LightlessSync.csproj | 2 +- LightlessSync/Plugin.cs | 21 +- LightlessSync/Services/BroadcastService.cs | 375 +++++++++++++++++ LightlessSync/Services/DalamudUtilService.cs | 12 +- LightlessSync/Services/Mediator/Messages.cs | 4 +- .../Mediator/WindowMediatorSubscriberBase.cs | 13 + LightlessSync/Services/NameplateHandler.cs | 297 ++++++++++++++ LightlessSync/Services/NameplateService.cs | 252 +++++++++++- LightlessSync/UI/BroadcastUI.cs | 384 ++++++++++++++++++ LightlessSync/UI/CompactUI.cs | 119 +++++- LightlessSync/UI/Components/DrawUserPair.cs | 27 +- LightlessSync/UI/ContextMenu.cs | 151 +++++++ LightlessSync/UI/Handlers/IdDisplayHandler.cs | 32 +- LightlessSync/UI/SettingsUi.cs | 7 + LightlessSync/UI/Style/MainStyle.cs | 169 ++++++++ LightlessSync/UI/SyncshellFinderUI.cs | 297 ++++++++++++++ LightlessSync/UI/UIColors.cs | 12 + LightlessSync/UI/UISharedService.cs | 19 + LightlessSync/Utils/AtkNodeHelpers.cs | 99 +++++ LightlessSync/Utils/SeStringUtils.cs | 171 ++++++++ .../SignalR/ApIController.Functions.Users.cs | 31 ++ .../SignalR/ApiController.Functions.Groups.cs | 18 + LightlessSync/WebAPI/SignalR/ApiController.cs | 4 + 25 files changed, 2497 insertions(+), 26 deletions(-) create mode 100644 LightlessSync/Services/BroadcastService.cs create mode 100644 LightlessSync/Services/NameplateHandler.cs create mode 100644 LightlessSync/UI/BroadcastUI.cs create mode 100644 LightlessSync/UI/ContextMenu.cs create mode 100644 LightlessSync/UI/Style/MainStyle.cs create mode 100644 LightlessSync/UI/SyncshellFinderUI.cs create mode 100644 LightlessSync/Utils/AtkNodeHelpers.cs create mode 100644 LightlessSync/Utils/SeStringUtils.cs diff --git a/LightlessAPI b/LightlessAPI index a337481..fd4cd52 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit a337481243a11490f3a115ca1ac0abfdd62c0554 +Subproject commit fd4cd52d2e78c8a621e6b06149e69842bb5ff255 diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 002bf1a..235f0c5 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -20,6 +20,7 @@ public class LightlessConfig : ILightlessConfiguration public DtrEntry.Colors DtrColorsDefault { get; set; } = default; public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu); public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u); + public bool UseLightlessRedesign { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; public string ExportFolder { get; set; } = string.Empty; @@ -66,4 +67,8 @@ public class LightlessConfig : ILightlessConfiguration public bool UseFocusTarget { get; set; } = false; public bool overrideFriendColor { get; set; } = false; public bool overridePartyColor { get; set; } = false; + public bool BroadcastEnabled { get; set; } = false; + public DateTime BroadcastTtl { get; set; } = DateTime.MinValue; + public bool SyncshellFinderEnabled { get; set; } = false; + public string? SelectedFinderSyncshell { get; set; } = null; } \ No newline at end of file diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 21304eb..344e968 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 1.11.12 + 1.12.0 https://github.com/Light-Public-Syncshells/LightlessClient diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 4f704f5..1cab3dd 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -41,7 +41,7 @@ public sealed class Plugin : IDalamudPlugin IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui, IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager, ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig, - ISigScanner sigScanner, INamePlateGui namePlateGui) + ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle) { if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName)) Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName); @@ -90,6 +90,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(new WindowSystem("LightlessSync")); collection.AddSingleton(); collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true)); + collection.AddSingleton(gameGui); // add lightless related singletons collection.AddSingleton(); @@ -144,6 +145,9 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(s => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), contextMenu)); collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(addonLifecycle); + collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable)); collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService>(), pluginInterface, @@ -174,7 +178,12 @@ public sealed class Plugin : IDalamudPlugin httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); return httpClient; }); - collection.AddSingleton((s) => new LightlessConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => + { + var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName); + LightlessSync.UI.Style.MainStyle.Init(cfg); + return cfg; + }); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new PairTagConfigService(pluginInterface.ConfigDirectory.FullName)); @@ -194,8 +203,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton(); + collection.AddSingleton(); // add scoped services collection.AddScoped(); @@ -218,6 +227,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); + collection.AddScoped((s) => new BroadcastUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); @@ -235,7 +246,7 @@ public sealed class Plugin : IDalamudPlugin pluginInterface, textureProvider, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), namePlateGui, clientState, - s.GetRequiredService(), s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); @@ -248,6 +259,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); }) .Build(); diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs new file mode 100644 index 0000000..7e887ed --- /dev/null +++ b/LightlessSync/Services/BroadcastService.cs @@ -0,0 +1,375 @@ +using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; +using LightlessSync.LightlessConfiguration; +using LightlessSync.Services.Mediator; +using LightlessSync.Utils; +using LightlessSync.WebAPI; +using LightlessSync.WebAPI.SignalR; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services; +public class BroadcastService : IHostedService, IMediatorSubscriber +{ + private readonly ILogger _logger; + private readonly ApiController _apiController; + private readonly LightlessMediator _mediator; + private readonly HubFactory _hubFactory; + private readonly LightlessConfigService _config; + private readonly DalamudUtilService _dalamudUtil; + public LightlessMediator Mediator => _mediator; + + public bool IsLightFinderAvailable { get; private set; } = true; + + public bool IsBroadcasting => _config.Current.BroadcastEnabled; + private bool _syncedOnStartup = false; + private bool _waitingForTtlFetch = false; + private TimeSpan? _remainingTtl = null; + private DateTime _lastTtlCheck = DateTime.MinValue; + private DateTime _lastForcedDisableTime = DateTime.MinValue; + private static readonly TimeSpan DisableCooldown = TimeSpan.FromSeconds(5); + public TimeSpan? RemainingTtl => _remainingTtl; + public TimeSpan? RemainingCooldown + { + get + { + var elapsed = DateTime.UtcNow - _lastForcedDisableTime; + if (elapsed >= DisableCooldown) return null; + return DisableCooldown - elapsed; + } + } + public BroadcastService(ILogger logger, LightlessMediator mediator, HubFactory hubFactory, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController) + { + _logger = logger; + _mediator = mediator; + _hubFactory = hubFactory; + _config = config; + _dalamudUtil = dalamudUtil; + _apiController = apiController; + } + private async Task RequireConnectionAsync(string context, Func action) + { + if (!_apiController.IsConnected) + { + _logger.LogDebug($"{context} skipped, not connected"); + return; + } + await action().ConfigureAwait(false); + } + public async Task StartAsync(CancellationToken cancellationToken) + { + _mediator.Subscribe(this, OnEnableBroadcast); + _mediator.Subscribe(this, OnBroadcastStatusChanged); + _mediator.Subscribe(this, OnTick); + + _apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken); + _ = CheckLightfinderSupportAsync(cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _mediator.UnsubscribeAll(this); + _apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken); + return Task.CompletedTask; + } + + // need to rework this, this is cooked + private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken) + { + try + { + while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested) + await Task.Delay(250, cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + return; + + var hub = _hubFactory.GetOrCreate(CancellationToken.None); + var dummy = "0".PadLeft(64, '0'); + + await hub.InvokeAsync("IsUserBroadcasting", dummy, cancellationToken); + await hub.InvokeAsync("SetBroadcastStatus", dummy, true, null, cancellationToken); + await hub.InvokeAsync("GetBroadcastTtl", dummy, cancellationToken); + await hub.InvokeAsync>("AreUsersBroadcasting", new[] { dummy }, cancellationToken); + + IsLightFinderAvailable = true; + _logger.LogInformation("Lightfinder is available."); + } + catch (HubException ex) when (ex.Message.Contains("Method does not exist")) + { + _logger.LogWarning("Lightfinder unavailable: required method missing."); + IsLightFinderAvailable = false; + + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + _config.Save(); + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + } + catch (OperationCanceledException) + { + _logger.LogInformation("Lightfinder check was canceled."); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Lightfinder check failed."); + IsLightFinderAvailable = false; + + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + _config.Save(); + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + } + } + + private void OnEnableBroadcast(EnableBroadcastMessage msg) + { + _ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () => + { + try + { + GroupBroadcastRequestDto? groupDto = null; + if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null) + { + groupDto = new GroupBroadcastRequestDto + { + HashedCID = msg.HashedCid, + GID = _config.Current.SelectedFinderSyncshell, + Enabled = msg.Enabled, + }; + } + + _ = _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false); + + _logger.LogInformation("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); + + if (!msg.Enabled) + { + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + _config.Save(); + + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + return; + } + + _waitingForTtlFetch = true; + + var ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); + + if (ttl is { } remaining && remaining > TimeSpan.Zero) + { + _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; + _config.Current.BroadcastEnabled = true; + _config.Save(); + + _logger.LogInformation("Fetched TTL from server: {TTL}", remaining); + _mediator.Publish(new BroadcastStatusChangedMessage(true, remaining)); + } + else + { + _logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling."); + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + _config.Save(); + + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + } + + _waitingForTtlFetch = false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid); + _waitingForTtlFetch = false; + } + }); + } + + private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg) + { + _config.Current.BroadcastEnabled = msg.Enabled; + _config.Save(); + } + + public async Task CheckIfBroadcastingAsync(string targetCid) + { + bool result = false; + await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () => + { + try + { + _logger.LogInformation("[BroadcastCheck] Checking CID: {cid}", targetCid); + + var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false); + result = info?.TTL > TimeSpan.Zero; + + + _logger.LogInformation("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid); + } + }).ConfigureAwait(false); + + + return result; + } + + public async Task GetBroadcastTtlAsync(string cid) + { + TimeSpan? ttl = null; + await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => { + try + { + ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid); + } + }).ConfigureAwait(false); + return ttl; + } + + public async Task> AreUsersBroadcastingAsync(List hashedCids) + { + Dictionary result = new(); + + await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () => + { + try + { + var batch = await _apiController.AreUsersBroadcasting(hashedCids).ConfigureAwait(false); + + if (batch?.Results != null) + { + foreach (var kv in batch.Results) + result[kv.Key] = kv.Value; + } + + _logger.LogInformation("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to batch check broadcast status"); + } + }).ConfigureAwait(false); + + return result; + } + + + + public async void ToggleBroadcast() + { + if (!IsLightFinderAvailable) + { + _logger.LogWarning("ToggleBroadcast - Lightfinder is not available."); + return; + } + + await RequireConnectionAsync(nameof(ToggleBroadcast), async () => + { + var cooldown = RemainingCooldown; + if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero) + { + _logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds); + return; + } + + var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + + try + { + var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false); + var newStatus = !isCurrentlyBroadcasting; + + if (!newStatus) + { + _lastForcedDisableTime = DateTime.UtcNow; + _logger.LogInformation("Manual disable: cooldown timer started."); + } + + _logger.LogInformation("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus); + + _mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to determine current broadcast status for toggle"); + } + }).ConfigureAwait(false); + } + + private async void OnTick(PriorityFrameworkUpdateMessage _) + { + if (!IsLightFinderAvailable) + return; + + if (_config?.Current == null) + return; + + if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1) + return; + + _lastTtlCheck = DateTime.UtcNow; + + await RequireConnectionAsync(nameof(OnTick), async () => { + if (!_syncedOnStartup && _config.Current.BroadcastEnabled) + { + _syncedOnStartup = true; + try + { + var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + var ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); + if (ttl is { } + remaining && remaining > TimeSpan.Zero) + { + _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; + _config.Current.BroadcastEnabled = true; + _config.Save(); + _logger.LogInformation("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining); + } + else + { + _logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state."); + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + _config.Save(); + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh TTL in OnTick"); + } + } + if (_config.Current.BroadcastEnabled) + { + if (_waitingForTtlFetch) + { + _logger.LogDebug("OnTick skipped: waiting for TTL fetch"); + return; + } + var expiry = _config.Current.BroadcastTtl; + var remaining = expiry - DateTime.UtcNow; + _remainingTtl = remaining > TimeSpan.Zero ? remaining : null; + if (_remainingTtl == null) + { + _logger.LogInformation("Broadcast TTL expired. Disabling broadcast locally."); + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + _config.Save(); + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + } + } + else + { + _remainingTtl = null; + } + }).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index ea21af7..ed66506 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -313,7 +313,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false); } - private unsafe static string GetHashedCIDFromPlayerPointer(nint ptr) + public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr) { return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256(); } @@ -421,6 +421,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false); } + public IPlayerCharacter? GetPlayerByNameAndWorld(string name, ushort homeWorldId) + { + EnsureIsOnFramework(); + return _objectTable + .OfType() + .FirstOrDefault(p => + string.Equals(p.Name.TextValue, name, StringComparison.Ordinal) && + p.HomeWorld.RowId == homeWorldId); + } + public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0) { var fileName = Path.GetFileNameWithoutExtension(callerFilePath); diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 9018fe2..2f7d8d2 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -97,7 +97,9 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase; public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase; public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase; - +public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; +public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; +public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record VisibilityChange : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/WindowMediatorSubscriberBase.cs b/LightlessSync/Services/Mediator/WindowMediatorSubscriberBase.cs index c29192d..8d7aceb 100644 --- a/LightlessSync/Services/Mediator/WindowMediatorSubscriberBase.cs +++ b/LightlessSync/Services/Mediator/WindowMediatorSubscriberBase.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Windowing; +using LightlessSync.UI.Style; using Microsoft.Extensions.Logging; namespace LightlessSync.Services.Mediator; @@ -33,6 +34,18 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber GC.SuppressFinalize(this); } + public override void PreDraw() + { + base.PreDraw(); + MainStyle.PushStyle(); // internally checks ShouldUseTheme + } + + public override void PostDraw() + { + MainStyle.PopStyle(); // always attempts to pop if pushed + base.PostDraw(); + } + public override void Draw() { _performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal); diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs new file mode 100644 index 0000000..cbb4777 --- /dev/null +++ b/LightlessSync/Services/NameplateHandler.cs @@ -0,0 +1,297 @@ +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using LightlessSync.Services.Mediator; +using LightlessSync.Utils; +// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you! + +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services; + +public unsafe class NameplateHandler : IMediatorSubscriber +{ + private readonly ILogger _logger; + private readonly IAddonLifecycle _addonLifecycle; + private readonly IGameGui _gameGui; + private readonly DalamudUtilService _dalamudUtil; + private readonly LightlessMediator _mediator; + public LightlessMediator Mediator => _mediator; + + private bool mEnabled = false; + private bool _needsLabelRefresh = false; + private AddonNamePlate* mpNameplateAddon = null; + private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects]; + + internal const uint mNameplateNodeIDBase = 0x7D99D500; + + private volatile HashSet _activeBroadcastingCids = new(); + + public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator) + { + _logger = logger; + _addonLifecycle = addonLifecycle; + _gameGui = gameGui; + _dalamudUtil = dalamudUtil; + _mediator = mediator; + } + + internal void Init() + { + EnableNameplate(); + _mediator.Subscribe(this, OnTick); + } + + internal void Uninit() + { + DisableNameplate(); + DestroyNameplateNodes(); + _mediator.Unsubscribe(this); + mpNameplateAddon = null; + } + + internal void EnableNameplate() + { + if (!mEnabled) + { + try + { + _addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour); + mEnabled = true; + } + catch (Exception e) + { + _logger.LogError($"Unknown error while trying to enable nameplate distances:\n{e}"); + DisableNameplate(); + } + } + } + + internal void DisableNameplate() + { + if (mEnabled) + { + try + { + _addonLifecycle.UnregisterListener(NameplateDrawDetour); + } + catch (Exception e) + { + _logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}"); + } + + mEnabled = false; + HideAllNameplateNodes(); + } + } + + private void NameplateDrawDetour(AddonEvent type, AddonArgs args) + { + var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; + + if (mpNameplateAddon != pNameplateAddon) + { + for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null; + mpNameplateAddon = pNameplateAddon; + if (mpNameplateAddon != null) CreateNameplateNodes(); + } + + UpdateNameplateNodes(); + } + + private void CreateNameplateNodes() + { + for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) + { + var nameplateObject = GetNameplateObject(i); + if (nameplateObject == null) + continue; + + var pNameplateResNode = nameplateObject.Value.NameContainer; + var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare); + + if (pNewNode != null) + { + var pLastChild = pNameplateResNode->ChildNode; + while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode; + pNewNode->AtkResNode.NextSiblingNode = pLastChild; + pNewNode->AtkResNode.ParentNode = pNameplateResNode; + pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode; + nameplateObject.Value.RootComponentNode->Component->UldManager.UpdateDrawNodeList(); + pNewNode->AtkResNode.SetUseDepthBasedPriority(true); + mTextNodes[i] = pNewNode; + } + } + } + + private void DestroyNameplateNodes() + { + var pCurrentNameplateAddon = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address; + if (mpNameplateAddon == null || mpNameplateAddon != pCurrentNameplateAddon) + return; + + for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) + { + var pTextNode = mTextNodes[i]; + var pNameplateNode = GetNameplateComponentNode(i); + if (pTextNode != null && pNameplateNode != null) + { + try + { + if (pTextNode->AtkResNode.PrevSiblingNode != null) + pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode; + if (pTextNode->AtkResNode.NextSiblingNode != null) + pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; + pNameplateNode->Component->UldManager.UpdateDrawNodeList(); + pTextNode->AtkResNode.Destroy(true); + mTextNodes[i] = null; + } + catch (Exception e) + { + _logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}"); + } + } + } + } + + private void HideAllNameplateNodes() + { + for (int i = 0; i < mTextNodes.Length; ++i) + { + HideNameplateTextNode(i); + } + } + + private void UpdateNameplateNodes() + { + var framework = Framework.Instance(); + var ui3DModule = framework->GetUIModule()->GetUI3DModule(); + + if (ui3DModule == null) + return; + + for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i) + { + var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value; + if (objectInfo == null || objectInfo->GameObject == null) + continue; + + var nameplateIndex = objectInfo->NamePlateIndex; + if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) + continue; + + var pNode = mTextNodes[nameplateIndex]; + if (pNode == null) + continue; + + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject); + + //_logger.LogInformation($"checking cid: {cid}", cid); + + if (cid == null || !_activeBroadcastingCids.Contains(cid)) + { + pNode->AtkResNode.ToggleVisibility(false); + continue; + } + + pNode->AtkResNode.ToggleVisibility(true); + + var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; + nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList(); + + var nameContainer = nameplateObject.NameContainer; + var nameText = nameplateObject.NameText; + + var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY); + + pNode->AtkResNode.SetPositionShort(58, (short)labelY); + pNode->AtkResNode.SetUseDepthBasedPriority(true); + pNode->AtkResNode.SetScale(0.5f, 0.5f); + + pNode->AtkResNode.Color.A = 255; + + pNode->TextColor.A = 255; + pNode->TextColor.R = 173; + pNode->TextColor.G = 138; + pNode->TextColor.B = 245; + + pNode->EdgeColor.A = 255; + pNode->EdgeColor.R = 0; + pNode->EdgeColor.G = 0; + pNode->EdgeColor.B = 0; + + pNode->FontSize = 24; + pNode->AlignmentType = AlignmentType.Center; + pNode->FontType = FontType.MiedingerMed; + pNode->LineSpacing = 24; + pNode->CharSpacing = 1; + + pNode->TextFlags = TextFlags.Edge | TextFlags.Glare; + + pNode->SetText("Lightfinder"); + } + } + + private void HideNameplateTextNode(int i) + { + var pNode = mTextNodes[i]; + if (pNode != null) + { + pNode->AtkResNode.ToggleVisibility(false); + } + } + + private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) + { + if (i < AddonNamePlate.NumNamePlateObjects && + mpNameplateAddon != null && + mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) + { + return mpNameplateAddon->NamePlateObjectArray[i]; + } + else + { + return null; + } + } + + private AtkComponentNode* GetNameplateComponentNode(int i) + { + var nameplateObject = GetNameplateObject(i); + return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; + } + + public void FlagRefresh() + { + _needsLabelRefresh = true; + } + + public void OnTick(PriorityFrameworkUpdateMessage _) + { + if (_needsLabelRefresh) + { + UpdateNameplateNodes(); + _needsLabelRefresh = false; + } + } + + public void UpdateBroadcastingCids(IEnumerable cids) + { + var newSet = cids.ToHashSet(); + + var changed = !_activeBroadcastingCids.SetEquals(newSet); + if (!changed) + return; + + _activeBroadcastingCids.Clear(); + foreach (var cid in newSet) + _activeBroadcastingCids.Add(cid); + + _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(",", _activeBroadcastingCids)); + + FlagRefresh(); + } +} diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 400ae23..73c8dd1 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -3,47 +3,120 @@ using Dalamud.Game.Gui.NamePlate; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using Dalamud.Utility; +using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.UI; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + namespace LightlessSync.Services; public class NameplateService : DisposableMediatorSubscriberBase { + private readonly ILogger _logger; private readonly LightlessConfigService _configService; private readonly IClientState _clientState; private readonly INamePlateGui _namePlateGui; private readonly PairManager _pairManager; + private readonly BroadcastService _broadcastService; + private readonly DalamudUtilService _dalamudUtil; + private readonly NameplateHandler _nameplatehandler; + private readonly IFramework _framework; + private readonly IGameGui _gameGui; + private readonly ConcurrentDictionary _broadcastCache = new(); + private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(5); + private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); + private readonly Queue _lookupQueue = new(); + private readonly HashSet _lookupQueuedCids = new(); + private readonly HashSet _syncshellCids = new(); + + private readonly CancellationTokenSource _cleanupCts = new(); + private Task? _cleanupTask; + + private const int MaxLookupsPerFrame = 15; + private const int MaxQueueSize = 100; + + private int _lookupsThisFrame = 0; + private int _frameCounter = 0; + + public IReadOnlyDictionary BroadcastCache => _broadcastCache; + + public readonly struct BroadcastEntry + { + public readonly bool IsBroadcasting; + public readonly DateTime ExpiryTime; + public readonly bool PrefixApplied; + public readonly string? GID; + + public BroadcastEntry(bool isBroadcasting, DateTime expiryTime, bool prefixApplied, string? gid = null) + { + IsBroadcasting = isBroadcasting; + ExpiryTime = expiryTime; + PrefixApplied = prefixApplied; + GID = gid; + } + } public NameplateService(ILogger logger, LightlessConfigService configService, INamePlateGui namePlateGui, IClientState clientState, PairManager pairManager, - LightlessMediator lightlessMediator) : base(logger, lightlessMediator) + BroadcastService broadcastService, + LightlessMediator lightlessMediator, + DalamudUtilService dalamudUtil, + NameplateHandler nameplatehandler, + IGameGui gameGui) : base(logger, lightlessMediator) { + _logger = logger; _configService = configService; _namePlateGui = namePlateGui; _clientState = clientState; _pairManager = pairManager; + _broadcastService = broadcastService; + _dalamudUtil = dalamudUtil; + _nameplatehandler = nameplatehandler; + _gameGui = gameGui; + _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; _namePlateGui.RequestRedraw(); Mediator.Subscribe(this, (_) => _namePlateGui.RequestRedraw()); + Mediator.Subscribe(this, OnBroadcastStatusChanged); + _nameplatehandler.Init(); + _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); } private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) { - if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) return; - var visibleUsersIds = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue).Select(u => (ulong)u.PlayerCharacterId).ToHashSet(); + _frameCounter++; + _lookupsThisFrame = 0; + + if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) + return; + + var visibleUsersIds = _pairManager.GetOnlineUserPairs() + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId) + .ToHashSet(); + + var now = DateTime.UtcNow; var colors = _configService.Current.NameplateColors; + foreach (var handler in handlers) { var playerCharacter = handler.PlayerCharacter; - if (playerCharacter == null) { continue; } + if (playerCharacter == null) + continue; + + + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(playerCharacter.Address); + var hasEntry = _broadcastCache.TryGetValue(cid, out var entry); + var isEntryStale = !hasEntry || entry.ExpiryTime <= now; + var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember); var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend); bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty); @@ -55,9 +128,171 @@ public class NameplateService : DisposableMediatorSubscriberBase (isFriend && !friendColorAllowed) )) { + _logger.LogInformation("added nameplate color to {Name}", playerCharacter.Name.TextValue); handler.NameParts.TextWrap = CreateTextWrap(colors); } + + if (!_broadcastService.IsBroadcasting) + continue; + + if (isEntryStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize) + { + _lookupQueue.Enqueue(cid); + } } + + if (_broadcastService.IsBroadcasting && _frameCounter % 2 == 0) + { + var cidsToLookup = new List(); + while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame) + { + var nextCid = _lookupQueue.Dequeue(); + _lookupQueuedCids.Remove(nextCid); + cidsToLookup.Add(nextCid); + _lookupsThisFrame++; + } + + if (cidsToLookup.Count > 0) + _ = BatchUpdateBroadcastCacheAsync(cidsToLookup); + } + } + + private async Task BatchUpdateBroadcastCacheAsync(List cidList) + { + var results = await _broadcastService.AreUsersBroadcastingAsync(cidList).ConfigureAwait(false); + var now = DateTime.UtcNow; + + foreach (var (cid, info) in results) + { + if (string.IsNullOrWhiteSpace(cid) || info == null) + { + _logger.LogWarning("Skipping broadcast entry: cid={Cid}, info=null or empty", cid); + continue; + } + + bool isBroadcasting = info.IsBroadcasting; + TimeSpan effectiveTtl = isBroadcasting && info.TTL.HasValue + ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks)) + : RetryDelay; + + var expiryTime = now + effectiveTtl; + + _broadcastCache.AddOrUpdate(cid, + new BroadcastEntry(isBroadcasting, expiryTime, false, info.GID), + (_, old) => new BroadcastEntry(isBroadcasting, expiryTime, old.PrefixApplied, info.GID)); + } + + var activeCids = _broadcastCache + .Where(kvp => kvp.Value.IsBroadcasting) + .Select(kvp => kvp.Key) + .ToList(); + + _nameplatehandler.UpdateBroadcastingCids(activeCids); + _namePlateGui.RequestRedraw(); + + UpdateSyncshellBroadcasts(); + } + + private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg) + { + if (!msg.Enabled) + { + _logger.LogInformation("Broadcast disabled, clearing prefix cache and queue"); + + _broadcastCache.Clear(); + _lookupQueue.Clear(); + _lookupQueuedCids.Clear(); + _syncshellCids.Clear(); + + _nameplatehandler.UpdateBroadcastingCids(Enumerable.Empty()); + _namePlateGui.RequestRedraw(); + } + } + + public List GetActiveSyncshellBroadcasts() + { + var now = DateTime.UtcNow; + + return _broadcastCache + .Where(kvp => + kvp.Value.IsBroadcasting && + kvp.Value.ExpiryTime > now && + !string.IsNullOrEmpty(kvp.Value.GID)) + .Select(kvp => new BroadcastStatusInfoDto + { + HashedCID = kvp.Key, + IsBroadcasting = true, + TTL = kvp.Value.ExpiryTime - now, + GID = kvp.Value.GID + }) + .ToList(); + } + + private void UpdateSyncshellBroadcasts() + { + var now = DateTime.UtcNow; + + var newSet = _broadcastCache + .Where(kvp => kvp.Value.IsBroadcasting && kvp.Value.ExpiryTime > now && !string.IsNullOrEmpty(kvp.Value.GID)) + .Select(kvp => kvp.Key) + .ToHashSet(); + + if (!_syncshellCids.SetEquals(newSet)) + { + _syncshellCids.Clear(); + foreach (var cid in newSet) + _syncshellCids.Add(cid); + + _logger.LogInformation("Syncshell broadcast entries changed, sending update lol"); + Mediator.Publish(new SyncshellBroadcastsUpdatedMessage()); + } + } + + public bool IsBroadcastingKnown(string cidHash, out bool isBroadcasting) + { + if (_broadcastCache.TryGetValue(cidHash, out var entry)) + { + isBroadcasting = entry.IsBroadcasting; + return true; + } + + isBroadcasting = false; + return false; + } + + private async Task ExpiredBroadcastCleanupLoop() + { + var token = _cleanupCts.Token; + + try + { + while (!token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(10), token); + + var now = DateTime.UtcNow; + foreach (var (cid, entry) in _broadcastCache.ToArray()) + { + if (entry.ExpiryTime <= now) + { + if (_broadcastCache.TryRemove(cid, out _)) + { + _logger.LogInformation("Removed expired broadcast entry: {Cid}", cid); + } + } + } + } + } + catch (OperationCanceledException) + { + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in ExpiredBroadcastCleanupLoop"); + } + + UpdateSyncshellBroadcasts(); } public void RequestRedraw() @@ -80,12 +315,15 @@ public class NameplateService : DisposableMediatorSubscriberBase return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString()); } - protected override void Dispose(bool disposing) { base.Dispose(disposing); + + _cleanupCts.Cancel(); + _cleanupTask?.Wait(100); + _namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; _namePlateGui.RequestRedraw(); + _nameplatehandler.Uninit(); } -} - +} \ No newline at end of file diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs new file mode 100644 index 0000000..7d60e58 --- /dev/null +++ b/LightlessSync/UI/BroadcastUI.cs @@ -0,0 +1,384 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility; +using Dalamud.Utility; +using LightlessSync.API.Dto.Group; +using LightlessSync.LightlessConfiguration; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace LightlessSync.UI +{ + public class BroadcastUI : WindowMediatorSubscriberBase + { + private readonly ApiController _apiController; + private readonly LightlessConfigService _configService; + private readonly BroadcastService _broadcastService; + private readonly UiSharedService _uiSharedService; + private readonly NameplateService _nameplateService; + + private IReadOnlyList _allSyncshells; + private string _userUid = string.Empty; + + private List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); + + public BroadcastUI( + ILogger logger, + LightlessMediator mediator, + PerformanceCollectorService performanceCollectorService, + BroadcastService broadcastService, + LightlessConfigService configService, + UiSharedService uiShared, + ApiController apiController, + NameplateService nameplateService + ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) + { + _broadcastService = broadcastService; + _uiSharedService = uiShared; + _configService = configService; + _apiController = apiController; + _nameplateService = nameplateService; + + IsOpen = false; + this.SizeConstraints = new() + { + MinimumSize = new(590, 340), + MaximumSize = new(590, 340) + }; + + mediator.Subscribe(this, async _ => await RefreshSyncshells()); + } + + private void RebuildSyncshellDropdownOptions() + { + var selectedGid = _configService.Current.SelectedFinderSyncshell; + var allSyncshells = _allSyncshells ?? Array.Empty(); + var ownedSyncshells = allSyncshells + .Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal)) + .ToList(); + + _syncshellOptions.Clear(); + _syncshellOptions.Add(("None", null, true)); + + var addedGids = new HashSet(); + + foreach (var shell in ownedSyncshells) + { + var label = shell.GroupAliasOrGID ?? shell.GID; + _syncshellOptions.Add((label, shell.GID, true)); + addedGids.Add(shell.GID); + } + + if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid)) + { + var matching = allSyncshells.FirstOrDefault(g => g.GID == selectedGid); + if (matching != null) + { + var label = matching.GroupAliasOrGID ?? matching.GID; + _syncshellOptions.Add((label, matching.GID, true)); + addedGids.Add(matching.GID); + } + } + + if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid)) + { + _syncshellOptions.Add(($"[Unavailable] {selectedGid}", selectedGid, false)); + } + } + + public Task RefreshSyncshells() + { + return RefreshSyncshellsInternal(); + } + + private async Task RefreshSyncshellsInternal() + { + if (!_apiController.IsConnected) + { + _allSyncshells = Array.Empty(); + RebuildSyncshellDropdownOptions(); + return; + } + + try + { + _allSyncshells = await _apiController.GroupsGetAll(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to fetch Syncshells."); + _allSyncshells = Array.Empty(); + } + + RebuildSyncshellDropdownOptions(); + } + + + public override void OnOpen() + { + _userUid = _apiController.UID; + _ = RefreshSyncshellsInternal(); + } + + protected override void DrawInternal() + { + if (!_broadcastService.IsLightFinderAvailable) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); + ImGui.TextWrapped("This server doesn't support LightFinder."); + ImGui.PopStyleColor(); + + ImGuiHelpers.ScaledDummy(0.25f); + } + + if (ImGui.BeginTabBar("##MyTabBar")) + { + if (ImGui.BeginTabItem("Lightfinder")) + { + _uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue")); + + ImGui.PushTextWrapPos(); + ImGui.Text("This lets other Lightless users know you use Lightless."); + ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); + ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); + ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); + ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); + ImGui.PopTextWrapPos(); + + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("Use it only when you want to be visible."); + ImGui.PopStyleColor(); + + ImGuiHelpers.ScaledDummy(0.2f); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + + if (_broadcastService.IsBroadcasting) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen")); + ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe.. + ImGui.PopStyleColor(); + + var ttl = _broadcastService.RemainingTtl; + if (ttl is { } remaining && remaining > TimeSpan.Zero) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); + ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}"); + ImGui.PopStyleColor(); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. + ImGui.PopStyleColor(); + } + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe.. + ImGui.PopStyleColor(); + } + + var cooldown = _broadcastService.RemainingCooldown; + if (cooldown is { } cd) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)"); + ImGui.PopStyleColor(); + } + + ImGuiHelpers.ScaledDummy(0.5f); + + bool isBroadcasting = _broadcastService.IsBroadcasting; + bool isOnCooldown = cooldown.HasValue && cooldown.Value.TotalSeconds > 0; + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); + + if (isOnCooldown) + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed")); + else if (isBroadcasting) + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); + else + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue")); + + if (isOnCooldown) + ImGui.BeginDisabled(); + + string buttonText = isBroadcasting ? "Disable Lightfinder" : "Enable Lightfinder"; + + if (ImGui.Button(buttonText, new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) + { + _broadcastService.ToggleBroadcast(); + } + + if (isOnCooldown) + ImGui.EndDisabled(); + + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Syncshell Finder")) + { + if (_allSyncshells == null) + { + ImGui.Text("Loading Syncshells..."); + return; + } + + _uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue")); + + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + + ImGui.PushTextWrapPos(); + ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder."); + ImGui.Text("To enable this, select one of your owned Syncshells from the dropdown menu below and ensure that \"Toggle Syncshell Finder\" is enabled. Your Syncshell will be visible in the Nearby Syncshell Finder as long as Lightfinder is active."); + ImGui.PopTextWrapPos(); + + ImGuiHelpers.ScaledDummy(0.2f); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + + bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled; + bool isBroadcasting = _broadcastService.IsBroadcasting; + + if (isBroadcasting) + ImGui.BeginDisabled(); + + if (ImGui.Checkbox("Toggle Syncshell Finder", ref ShellFinderEnabled)) + { + _configService.Current.SyncshellFinderEnabled = ShellFinderEnabled; + _configService.Save(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text("Toggle to broadcast specified Syncshell."); + ImGui.EndTooltip(); + } + + var selectedGid = _configService.Current.SelectedFinderSyncshell; + var currentOption = _syncshellOptions.FirstOrDefault(o => o.GID == selectedGid); + var preview = currentOption.Label ?? "Select a Syncshell..."; + + if (ImGui.BeginCombo("##SyncshellDropdown", preview)) + { + foreach (var (label, gid, available) in _syncshellOptions) + { + bool isSelected = gid == selectedGid; + + if (!available) + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + + if (ImGui.Selectable(label, isSelected)) + { + _configService.Current.SelectedFinderSyncshell = gid; + _configService.Save(); + } + + if (!available && ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text("This Syncshell is not available on the current service."); + ImGui.EndTooltip(); + } + + if (!available) + ImGui.PopStyleColor(); + + if (isSelected) + ImGui.SetItemDefaultFocus(); + } + + ImGui.EndCombo(); + } + + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text("Choose one of the available options."); + ImGui.EndTooltip(); + } + + + if (isBroadcasting) + ImGui.EndDisabled(); + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Debug")) + { + ImGui.Text("Broadcast Cache"); + + if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 200f))) + { + ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + var now = DateTime.UtcNow; + + foreach (var (cid, entry) in _nameplateService.BroadcastCache) + { + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(cid.Truncate(12)); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted(cid); + ImGui.EndTooltip(); + } + + ImGui.TableNextColumn(); + var colorBroadcast = entry.IsBroadcasting + ? UIColors.Get("LightlessGreen") + : UIColors.Get("DimRed"); + + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast)); + ImGui.TextUnformatted(entry.IsBroadcasting.ToString()); + + ImGui.TableNextColumn(); + var remaining = entry.ExpiryTime - now; + var colorTtl = + remaining <= TimeSpan.Zero ? UIColors.Get("DimRed") : + remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow") : + (Vector4?)null; + + if (colorTtl != null) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value)); + + ImGui.TextUnformatted(remaining > TimeSpan.Zero + ? remaining.ToString("hh\\:mm\\:ss") + : "Expired"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.GID ?? "-"); + } + + ImGui.EndTable(); + } + + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } + } +} diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 679c5f2..1d2105a 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -51,6 +51,8 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly TopTabMenu _tabMenu; private readonly TagHandler _tagHandler; private readonly UiSharedService _uiSharedService; + private readonly BroadcastService _broadcastService; + private List _drawFolders; private Dictionary>? _cachedAnalysis; private Pair? _lastAddedUser; @@ -62,13 +64,28 @@ public class CompactUi : WindowMediatorSubscriberBase private bool _wasOpen; private float _windowContentWidth; - public CompactUi(ILogger logger, UiSharedService uiShared, LightlessConfigService configService, ApiController apiController, PairManager pairManager, - ServerConfigurationManager serverManager, LightlessMediator mediator, FileUploadManager fileTransferManager, - TagHandler tagHandler, DrawEntityFactory drawEntityFactory, - SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, - SelectTagForSyncshellUi selectTagForSyncshellUi, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, - PerformanceCollectorService performanceCollectorService, IpcManager ipcManager, CharacterAnalyzer characterAnalyzer, PlayerPerformanceConfigService playerPerformanceConfig, LightlessMediator lightlessMediator) - : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + public CompactUi( + ILogger logger, + UiSharedService uiShared, + LightlessConfigService configService, + ApiController apiController, + PairManager pairManager, + ServerConfigurationManager serverManager, + LightlessMediator mediator, + FileUploadManager fileTransferManager, + TagHandler tagHandler, + DrawEntityFactory drawEntityFactory, + SelectTagForPairUi selectTagForPairUi, + SelectPairForTagUi selectPairForTagUi, + RenamePairTagUi renameTagUi, + SelectTagForSyncshellUi selectTagForSyncshellUi, + SelectSyncshellForTagUi selectSyncshellForTagUi, + RenameSyncshellTagUi renameSyncshellTagUi, + PerformanceCollectorService performanceCollectorService, + IpcManager ipcManager, + BroadcastService broadcastService, + CharacterAnalyzer characterAnalyzer, + PlayerPerformanceConfigService playerPerformanceConfig) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; @@ -85,6 +102,7 @@ public class CompactUi : WindowMediatorSubscriberBase _selectPairsForGroupUi = selectPairForTagUi; _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; + _broadcastService = broadcastService; _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService); AllowPinning = true; @@ -120,6 +138,89 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.Text("Open Lightless Event Viewer"); ImGui.EndTooltip(); } + }, + new TitleBarButton() + { + Icon = FontAwesomeIcon.TowerCell, + Click = (_) => + { + Mediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); + }, + IconOffset = new(2, 1), + + ShowTooltip = () => + { + ImGui.BeginTooltip(); + + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue")); + ImGui.Text("Lightfinder"); + ImGui.PopStyleColor(); + + ImGui.Text("This lets other Lightless users know you use Lightless."); + ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); + ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); + ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); + ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); + + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("Use it only when you want to be visible."); + ImGui.PopStyleColor(); + + ImGuiHelpers.ScaledDummy(0.2f); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + + if (_broadcastService.IsBroadcasting) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen")); + ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe.. + ImGui.PopStyleColor(); + + var ttl = _broadcastService.RemainingTtl; + if (ttl is { } remaining && remaining > TimeSpan.Zero) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); + ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}"); + ImGui.PopStyleColor(); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("The Lightfinder's light wanes, but not in vain."); // cringe.. + ImGui.PopStyleColor(); + } + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe.. + ImGui.PopStyleColor(); + } + + var cooldown = _broadcastService.RemainingCooldown; + if (cooldown is { } cd) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)"); + ImGui.PopStyleColor(); + } + + ImGui.EndTooltip(); + } + }, + new TitleBarButton() + { + Icon = FontAwesomeIcon.NetworkWired, + Click = (msg) => + { + Mediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI))); + }, + IconOffset = new(2,1), + ShowTooltip = () => + { + ImGui.BeginTooltip(); + ImGui.Text("Nearby Syncshells"); + ImGui.EndTooltip(); + } } }; @@ -151,7 +252,7 @@ public class CompactUi : WindowMediatorSubscriberBase }; _characterAnalyzer = characterAnalyzer; _playerPerformanceConfig = playerPerformanceConfig; - _lightlessMediator = lightlessMediator; + _lightlessMediator = mediator; } protected override void DrawInternal() @@ -202,7 +303,7 @@ public class CompactUi : WindowMediatorSubscriberBase } using (ImRaii.PushId("header")) DrawUIDHeader(); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + _uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f); using (ImRaii.PushId("serverstatus")) DrawServerStatus(); ImGui.Separator(); diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 398af1a..fdd06e8 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -1,6 +1,5 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Extensions; @@ -12,6 +11,7 @@ using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Handlers; +using LightlessSync.Utils; using LightlessSync.WebAPI; namespace LightlessSync.UI.Components; @@ -295,6 +295,31 @@ public class DrawUserPair } ImGui.SameLine(); + + if (_pair.UserData.IsAdmin || _pair.UserData.IsModerator) + { + ImGui.SameLine(); + + var iconId = _pair.UserData.IsAdmin ? 67 : 68; + var colorKey = _pair.UserData.IsAdmin ? "LightlessAdminText" : "LightlessModeratorText"; + var roleColor = UIColors.Get(colorKey); + + var iconPos = ImGui.GetCursorScreenPos(); + SeStringUtils.RenderIconWithHitbox(iconId, iconPos); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + using (ImRaii.PushColor(ImGuiCol.Text, roleColor)) + { + ImGui.TextUnformatted(_pair.UserData.IsAdmin + ? "Official Lightless Admin" + : "Official Lightless Moderator"); + } + ImGui.EndTooltip(); + } + } + } private void DrawName(float leftSide, float rightSide) diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs new file mode 100644 index 0000000..1e1cfc3 --- /dev/null +++ b/LightlessSync/UI/ContextMenu.cs @@ -0,0 +1,151 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using LightlessSync.Services; +using LightlessSync.Utils; +using LightlessSync.WebAPI; +using Lumina.Excel.Sheets; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.UI; + +internal class ContextMenu : IHostedService +{ + private readonly IContextMenu _contextMenu; + private readonly IDalamudPluginInterface _pluginInterface; + private readonly IDataManager _gameData; + private readonly ILogger _logger; + private readonly DalamudUtilService _dalamudUtil; + private readonly ApiController _apiController; + private readonly IObjectTable _objectTable; + + private static readonly string[] ValidAddons = new[] + { + null, + "PartyMemberList", "FriendList", "FreeCompany", "LinkShell", "CrossWorldLinkshell", + "_PartyList", "ChatLog", "LookingForGroup", "BlackList", "ContentMemberList", + "SocialList", "ContactList", "BeginnerChatList", "MuteList" + }; + + public ContextMenu( + IContextMenu contextMenu, + IDalamudPluginInterface pluginInterface, + IDataManager gameData, + ILogger logger, + DalamudUtilService dalamudUtil, + ApiController apiController, + IObjectTable objectTable) + { + _contextMenu = contextMenu; + _pluginInterface = pluginInterface; + _gameData = gameData; + _logger = logger; + _dalamudUtil = dalamudUtil; + _apiController = apiController; + _objectTable = objectTable; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _contextMenu.OnMenuOpened += OnMenuOpened; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _contextMenu.OnMenuOpened -= OnMenuOpened; + return Task.CompletedTask; + } + + public void Enable() + { + _contextMenu.OnMenuOpened += OnMenuOpened; + _logger.LogDebug("Context menu enabled."); + } + + public void Disable() + { + _contextMenu.OnMenuOpened -= OnMenuOpened; + _logger.LogDebug("Context menu disabled."); + } + + private void OnMenuOpened(IMenuOpenedArgs args) + { + if (!_pluginInterface.UiBuilder.ShouldModifyUi) + return; + + if (!ValidAddons.Contains(args.AddonName)) + return; + + if (args.Target is not MenuTargetDefault target) + return; + + if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0) + return; + + var world = GetWorld(target.TargetHomeWorld.RowId); + if (!IsWorldValid(world)) + return; + + args.AddMenuItem(new MenuItem + { + Name = "Send Pair Request", + PrefixChar = 'L', + UseDefaultPrefix = false, + PrefixColor = 708, + OnClicked = async _ => await HandleSelection(args) + }); + } + + private async Task HandleSelection(IMenuArgs args) + { + if (args.Target is not MenuTargetDefault target) + return; + + var world = GetWorld(target.TargetHomeWorld.RowId); + if (!IsWorldValid(world)) + return; + + try + { + var targetData = _objectTable + .OfType() + .FirstOrDefault(p => + string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) && + p.HomeWorld.RowId == target.TargetHomeWorld.RowId); + + if (targetData == null || targetData.Address == IntPtr.Zero) + { + _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name); + return; + } + + var senderCid = (await _dalamudUtil.GetCIDAsync()).ToString().GetHash256(); + var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); + + _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); + await _apiController.TryPairWithContentId(receiverCid, senderCid); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending pair request."); + } + } + + private World GetWorld(uint worldId) + { + var sheet = _gameData.GetExcelSheet()!; + return sheet.TryGetRow(worldId, out var world) ? world : sheet.First(); + } + + public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId)); + + public static bool IsWorldValid(World world) + { + var name = world.Name.ToString(); + return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]); + } +} diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index e77093b..81ed337 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -6,6 +6,8 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.Utils; +using System.Numerics; namespace LightlessSync.UI.Handlers; @@ -89,11 +91,37 @@ public class IdDisplayHandler { ImGui.SameLine(textPosX); (bool textIsUid, string playerText) = GetPlayerText(pair); + if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal)) { ImGui.AlignTextToFramePadding(); - using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) ImGui.TextUnformatted(playerText); + var font = UiBuilder.MonoFont; + + var isAdmin = pair.UserData.IsAdmin; + var isModerator = pair.UserData.IsModerator; + + Vector4? textColor = isAdmin + ? UIColors.Get("LightlessAdminText") + : isModerator + ? UIColors.Get("LightlessModeratorText") + : null; + + Vector4? glowColor = isAdmin + ? UIColors.Get("LightlessAdminGlow") + : isModerator + ? UIColors.Get("LightlessModeratorGlow") + : null; + + var seString = (textColor != null || glowColor != null) + ? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor) + : SeStringUtils.BuildPlain(playerText); + + using (ImRaii.PushFont(font, textIsUid)) + { + var pos = ImGui.GetCursorScreenPos(); + SeStringUtils.RenderSeStringWithHitbox(seString, pos, font); + } if (ImGui.IsItemHovered()) { @@ -173,10 +201,12 @@ public class IdDisplayHandler { _editEntry = string.Empty; } + UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); } } + public (bool isGid, string text) GetGroupText(GroupFullInfoDto group) { var textIsGid = true; diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 3c392ba..a973c34 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -921,6 +921,7 @@ public class SettingsUi : WindowMediatorSubscriberBase var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip; var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip; var useColorsInDtr = _configService.Current.UseColorsInDtr; + var useLightlessRedesign = _configService.Current.UseLightlessRedesign; var dtrColorsDefault = _configService.Current.DtrColorsDefault; var dtrColorsNotConnected = _configService.Current.DtrColorsNotConnected; var dtrColorsPairsInRange = _configService.Current.DtrColorsPairsInRange; @@ -1091,6 +1092,12 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + if (ImGui.Checkbox("Use the complete redesign of the UI for Lightless client.", ref useLightlessRedesign)) + { + _configService.Current.UseLightlessRedesign = useLightlessRedesign; + _configService.Save(); + } + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } diff --git a/LightlessSync/UI/Style/MainStyle.cs b/LightlessSync/UI/Style/MainStyle.cs new file mode 100644 index 0000000..d40ed2e --- /dev/null +++ b/LightlessSync/UI/Style/MainStyle.cs @@ -0,0 +1,169 @@ +// inspiration: brio because it's style is fucking amazing + +using Dalamud.Bindings.ImGui; +using LightlessSync.LightlessConfiguration; +using System.Numerics; + +namespace LightlessSync.UI.Style +{ + internal static class MainStyle + { + private static LightlessConfigService? _config; + public static void Init(LightlessConfigService config) => _config = config; + public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false; + + private static bool _hasPushed; + private static int _pushedColorCount; + private static int _pushedStyleVarCount; + + public static void PushStyle() + { + if (_hasPushed) + PopStyle(); + + if (!ShouldUseTheme) + { + _hasPushed = false; + return; + } + + _hasPushed = true; + _pushedColorCount = 0; + _pushedStyleVarCount = 0; + + Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255)); + Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255)); + + Push(ImGuiCol.WindowBg, new Vector4(23, 23, 23, 248)); + Push(ImGuiCol.ChildBg, new Vector4(23, 23, 23, 66)); + Push(ImGuiCol.PopupBg, new Vector4(23, 23, 23, 248)); + + Push(ImGuiCol.Border, new Vector4(65, 65, 65, 255)); + Push(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 150)); + + Push(ImGuiCol.FrameBg, new Vector4(40, 40, 40, 255)); + Push(ImGuiCol.FrameBgHovered, new Vector4(50, 50, 50, 255)); + Push(ImGuiCol.FrameBgActive, new Vector4(30, 30, 30, 255)); + + Push(ImGuiCol.TitleBg, new Vector4(24, 24, 24, 232)); + Push(ImGuiCol.TitleBgActive, new Vector4(30, 30, 30, 255)); + Push(ImGuiCol.TitleBgCollapsed, new Vector4(27, 27, 27, 255)); + + Push(ImGuiCol.MenuBarBg, new Vector4(36, 36, 36, 255)); + Push(ImGuiCol.ScrollbarBg, new Vector4(0, 0, 0, 0)); + Push(ImGuiCol.ScrollbarGrab, new Vector4(62, 62, 62, 255)); + Push(ImGuiCol.ScrollbarGrabHovered, new Vector4(70, 70, 70, 255)); + Push(ImGuiCol.ScrollbarGrabActive, new Vector4(70, 70, 70, 255)); + + Push(ImGuiCol.CheckMark, UIColors.Get("LightlessPurple")); + + Push(ImGuiCol.SliderGrab, new Vector4(101, 101, 101, 255)); + Push(ImGuiCol.SliderGrabActive, new Vector4(123, 123, 123, 255)); + + Push(ImGuiCol.Button, UIColors.Get("ButtonDefault")); + Push(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple")); + Push(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive")); + + Push(ImGuiCol.Header, new Vector4(0, 0, 0, 60)); + Push(ImGuiCol.HeaderHovered, new Vector4(0, 0, 0, 90)); + Push(ImGuiCol.HeaderActive, new Vector4(0, 0, 0, 120)); + + Push(ImGuiCol.Separator, new Vector4(75, 75, 75, 121)); + Push(ImGuiCol.SeparatorHovered, UIColors.Get("LightlessPurple")); + Push(ImGuiCol.SeparatorActive, UIColors.Get("LightlessPurpleActive")); + + Push(ImGuiCol.ResizeGrip, new Vector4(0, 0, 0, 0)); + Push(ImGuiCol.ResizeGripHovered, new Vector4(0, 0, 0, 0)); + Push(ImGuiCol.ResizeGripActive, UIColors.Get("LightlessPurpleActive")); + + Push(ImGuiCol.Tab, new Vector4(40, 40, 40, 255)); + Push(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple")); + Push(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive")); + Push(ImGuiCol.TabUnfocused, new Vector4(40, 40, 40, 255)); + Push(ImGuiCol.TabUnfocusedActive, UIColors.Get("LightlessPurpleActive")); + + Push(ImGuiCol.DockingPreview, UIColors.Get("LightlessPurpleActive")); + Push(ImGuiCol.DockingEmptyBg, new Vector4(50, 50, 50, 255)); + + Push(ImGuiCol.PlotLines, new Vector4(150, 150, 150, 255)); + + Push(ImGuiCol.TableHeaderBg, new Vector4(48, 48, 48, 255)); + Push(ImGuiCol.TableBorderStrong, new Vector4(79, 79, 89, 255)); + Push(ImGuiCol.TableBorderLight, new Vector4(59, 59, 64, 255)); + Push(ImGuiCol.TableRowBg, new Vector4(0, 0, 0, 0)); + Push(ImGuiCol.TableRowBgAlt, new Vector4(255, 255, 255, 15)); + + Push(ImGuiCol.TextSelectedBg, new Vector4(98, 75, 224, 255)); + Push(ImGuiCol.DragDropTarget, new Vector4(98, 75, 224, 255)); + + Push(ImGuiCol.NavHighlight, new Vector4(98, 75, 224, 179)); + Push(ImGuiCol.NavWindowingDimBg, new Vector4(204, 204, 204, 51)); + Push(ImGuiCol.NavWindowingHighlight, new Vector4(204, 204, 204, 89)); + + PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6)); + PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(4, 3)); + PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(4, 4)); + PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4)); + PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, new Vector2(4, 4)); + + PushStyleVar(ImGuiStyleVar.IndentSpacing, 21.0f); + PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10.0f); + PushStyleVar(ImGuiStyleVar.GrabMinSize, 20.0f); + + PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1.5f); + PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1.5f); + PushStyleVar(ImGuiStyleVar.PopupBorderSize, 1.5f); + PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0f); + + PushStyleVar(ImGuiStyleVar.WindowRounding, 7f); + PushStyleVar(ImGuiStyleVar.ChildRounding, 4f); + PushStyleVar(ImGuiStyleVar.FrameRounding, 4f); + PushStyleVar(ImGuiStyleVar.PopupRounding, 4f); + PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 4f); + PushStyleVar(ImGuiStyleVar.GrabRounding, 4f); + PushStyleVar(ImGuiStyleVar.TabRounding, 4f); + } + + public static void PopStyle() + { + if (!_hasPushed) + return; + + if (_pushedStyleVarCount > 0) + ImGui.PopStyleVar(_pushedStyleVarCount); + if (_pushedColorCount > 0) + ImGui.PopStyleColor(_pushedColorCount); + + _hasPushed = false; + _pushedColorCount = 0; + _pushedStyleVarCount = 0; + } + + private static void Push(ImGuiCol col, Vector4 rgba) + { + if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f) + rgba /= 255f; + + ImGui.PushStyleColor(col, rgba); + _pushedColorCount++; + } + + private static void Push(ImGuiCol col, uint packedRgba) + { + ImGui.PushStyleColor(col, packedRgba); + _pushedColorCount++; + } + + private static void PushStyleVar(ImGuiStyleVar var, float value) + { + ImGui.PushStyleVar(var, value); + _pushedStyleVarCount++; + } + + private static void PushStyleVar(ImGuiStyleVar var, Vector2 value) + { + ImGui.PushStyleVar(var, value); + _pushedStyleVarCount++; + } + } +} diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs new file mode 100644 index 0000000..cd6983c --- /dev/null +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -0,0 +1,297 @@ +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.Dto; +using LightlessSync.API.Dto.Group; +using LightlessSync.LightlessConfiguration; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using LightlessSync.Utils; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Logging; +using LightlessSync.API.Data.Extensions; +using System.Numerics; + +namespace LightlessSync.UI; + +public class SyncshellFinderUI : WindowMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly LightlessConfigService _configService; + private readonly BroadcastService _broadcastService; + private readonly UiSharedService _uiSharedService; + private readonly NameplateService _nameplateService; + + private readonly List _nearbySyncshells = new(); + private int _selectedNearbyIndex = -1; + + private GroupJoinDto? _joinDto; + private GroupJoinInfoDto? _joinInfo; + private DefaultPermissionsDto _ownPermissions = null!; + + public SyncshellFinderUI( + ILogger logger, + LightlessMediator mediator, + PerformanceCollectorService performanceCollectorService, + BroadcastService broadcastService, + LightlessConfigService configService, + UiSharedService uiShared, + ApiController apiController, + NameplateService nameplateService + ) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) + { + _broadcastService = broadcastService; + _uiSharedService = uiShared; + _configService = configService; + _apiController = apiController; + _nameplateService = nameplateService; + + IsOpen = false; + SizeConstraints = new() + { + MinimumSize = new(600, 400), + MaximumSize = new(600, 400) + }; + + Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync()); + } + + public override async void OnOpen() + { + _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + await RefreshSyncshellsAsync(); + } + + 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; + } + + if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("GID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); + ImGui.TableHeadersRow(); + + for (int i = 0; i < _nearbySyncshells.Count; i++) + { + var shell = _nearbySyncshells[i]; + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(shell.Group.Alias ?? "(No Alias)"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(shell.Group.GID); + 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)); + + 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}"); + } + }); + } + + + ImGui.PopStyleColor(3); + } + + ImGui.EndTable(); + } + + if (_joinDto != null && _joinInfo != null && _joinInfo.Success) + DrawConfirmation(); + } + + private void DrawConfirmation() + { + 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)); + _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 = _nameplateService.GetActiveSyncshellBroadcasts(); + + if (syncshellBroadcasts.Count == 0) + { + ClearSyncshells(); + return; + } + + List updatedList; + try + { + var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts); + updatedList = groups?.ToList() ?? new(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); + return; + } + + var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(); + var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(); + + if (currentGids.SetEquals(newGids)) + return; + + var previousGid = GetSelectedGid(); + + _nearbySyncshells.Clear(); + _nearbySyncshells.AddRange(updatedList); + + if (previousGid != null) + { + var newIndex = _nearbySyncshells.FindIndex(s => s.Group.GID == previousGid); + 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; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } +} diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index 1d56167..8fceb33 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -9,10 +9,22 @@ namespace LightlessSync.UI private static readonly Dictionary DefaultHexColors = new(StringComparer.OrdinalIgnoreCase) { { "LightlessPurple", "#ad8af5" }, + { "LightlessPurpleActive", "#be9eff" }, + { "LightlessPurpleDefault", "#9375d1" }, + + { "ButtonDefault", "#323232" }, + { "LightlessBlue", "#a6c2ff" }, { "LightlessYellow", "#ffe97a" }, + { "LightlessYellow2", "#cfbd63" }, + { "LightlessGreen", "#7cd68a" }, { "PairBlue", "#88a2db" }, { "DimRed", "#d44444" }, + + { "LightlessAdminText", "#ffd663" }, + { "LightlessAdminGlow", "#b09343" }, + { "LightlessModeratorText", "#94ffda" }, + { "LightlessModeratorGlow", "#599c84" }, }; private static LightlessConfigService? _configService; diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index ffca698..9a45d15 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -491,6 +491,25 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Dummy(new Vector2(0, thickness * ImGuiHelpers.GlobalScale)); } + public void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f) + { + float scale = ImGuiHelpers.GlobalScale; + + var drawList = ImGui.GetWindowDrawList(); + var min = ImGui.GetCursorScreenPos(); + var contentWidth = ImGui.GetContentRegionAvail().X; + + min.X += indent * scale; + var max = new Vector2(min.X + (contentWidth - indent * 2f) * scale, min.Y + thickness * scale); + + var col = ImGui.GetColorU32(color ?? ImGuiColors.DalamudGrey); + + + drawList.AddRectFilled(min, max, col, rounding); + + ImGui.Dummy(new Vector2(0, thickness * scale)); + } + public void MediumText(string text, Vector4? color = null) { FontText(text, MediumFont, color); diff --git a/LightlessSync/Utils/AtkNodeHelpers.cs b/LightlessSync/Utils/AtkNodeHelpers.cs new file mode 100644 index 0000000..02ed007 --- /dev/null +++ b/LightlessSync/Utils/AtkNodeHelpers.cs @@ -0,0 +1,99 @@ +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace LightlessSync.Utils; + +internal static unsafe class AtkNodeHelpers +{ + internal const ushort DefaultTextNodeWidth = 200; + internal const ushort DefaultTextNodeHeight = 14; + + internal static AtkTextNode* CreateNewTextNode(AtkUnitBase* pAddon, uint nodeID) + { + if (pAddon == null) return null; + var pNewNode = CreateOrphanTextNode(nodeID); + if (pNewNode != null) AttachTextNode(pAddon, pNewNode); + return pNewNode; + } + + internal static void HideNode(AtkUnitBase* pAddon, uint nodeID) + { + var pNode = GetTextNodeByID(pAddon, nodeID); + if (pNode != null) ((AtkResNode*)pNode)->ToggleVisibility(false); + } + + internal static AtkTextNode* GetTextNodeByID(AtkUnitBase* pAddon, uint nodeID) + { + if (pAddon == null) return null; + for (var i = 0; i < pAddon->UldManager.NodeListCount; ++i) + { + if (pAddon->UldManager.NodeList[i] == null) continue; + if (pAddon->UldManager.NodeList[i]->NodeId == nodeID) + { + return (AtkTextNode*)pAddon->UldManager.NodeList[i]; + } + } + return null; + } + + internal static void AttachTextNode(AtkUnitBase* pAddon, AtkTextNode* pNode) + { + if (pAddon == null) return; + + if (pNode != null) + { + var lastNode = pAddon->RootNode; + if (lastNode->ChildNode != null) + { + lastNode = lastNode->ChildNode; + while (lastNode->PrevSiblingNode != null) + { + lastNode = lastNode->PrevSiblingNode; + } + + pNode->AtkResNode.NextSiblingNode = lastNode; + pNode->AtkResNode.ParentNode = pAddon->RootNode; + lastNode->PrevSiblingNode = (AtkResNode*)pNode; + } + else + { + lastNode->ChildNode = (AtkResNode*)pNode; + pNode->AtkResNode.ParentNode = lastNode; + } + + pAddon->UldManager.UpdateDrawNodeList(); + } + } + + internal static AtkTextNode* CreateOrphanTextNode(uint nodeID, TextFlags textFlags = TextFlags.Edge) + { + var pNewNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8); + if (pNewNode != null) + { + IMemorySpace.Memset(pNewNode, 0, (ulong)sizeof(AtkTextNode)); + pNewNode->Ctor(); + + pNewNode->AtkResNode.Type = NodeType.Text; + pNewNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop; + pNewNode->AtkResNode.DrawFlags = 0; + pNewNode->AtkResNode.SetPositionShort(0, 0); + pNewNode->AtkResNode.SetWidth(DefaultTextNodeWidth); + pNewNode->AtkResNode.SetHeight(DefaultTextNodeHeight); + + pNewNode->LineSpacing = 24; + pNewNode->CharSpacing = 1; + pNewNode->AlignmentFontType = (byte)AlignmentType.BottomLeft; + pNewNode->FontSize = 12; + pNewNode->TextFlags = textFlags; + + pNewNode->AtkResNode.NodeId = nodeID; + + pNewNode->AtkResNode.Color.A = 0xFF; + pNewNode->AtkResNode.Color.R = 0xFF; + pNewNode->AtkResNode.Color.G = 0xFF; + pNewNode->AtkResNode.Color.B = 0xFF; + } + + return pNewNode; + } +} \ No newline at end of file diff --git a/LightlessSync/Utils/SeStringUtils.cs b/LightlessSync/Utils/SeStringUtils.cs new file mode 100644 index 0000000..837d13d --- /dev/null +++ b/LightlessSync/Utils/SeStringUtils.cs @@ -0,0 +1,171 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiSeStringRenderer; +using Dalamud.Interface.Utility; +using System.Numerics; + +namespace LightlessSync.Utils; + +public static class SeStringUtils +{ + public static SeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) + { + var b = new SeStringBuilder(); + + if (glowColor is Vector4 glow) + b.Add(new GlowPayload(glow)); + + if (textColor is Vector4 color) + b.Add(new ColorPayload(color)); + + b.AddText(text ?? string.Empty); + + if (textColor is not null) + b.Add(new ColorEndPayload()); + + if (glowColor is not null) + b.Add(new GlowEndPayload()); + + return b.Build(); + } + + public static SeString BuildPlain(string text) + { + var b = new SeStringBuilder(); + b.AddText(text ?? string.Empty); + return b.Build(); + } + + public static void RenderSeString(SeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null) + { + drawList ??= ImGui.GetWindowDrawList(); + + var drawParams = new SeStringDrawParams + { + Font = font ?? UiBuilder.MonoFont, + Color = 0xFFFFFFFF, + WrapWidth = float.MaxValue, + TargetDrawList = drawList + }; + + ImGui.SetCursorScreenPos(position); + ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + } + + public static Vector2 RenderSeStringWithHitbox(SeString seString, Vector2 position, ImFontPtr? font = null) + { + var drawList = ImGui.GetWindowDrawList(); + + var drawParams = new SeStringDrawParams + { + Font = font ?? UiBuilder.MonoFont, + Color = 0xFFFFFFFF, + WrapWidth = float.MaxValue, + TargetDrawList = drawList + }; + + ImGui.SetCursorScreenPos(position); + ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + + var textSize = ImGui.CalcTextSize(seString.TextValue); + + ImGui.SetCursorScreenPos(position); + ImGui.InvisibleButton($"##hitbox_{Guid.NewGuid()}", textSize); + + return textSize; + } + + public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null) + { + var drawList = ImGui.GetWindowDrawList(); + + var drawParams = new SeStringDrawParams + { + Font = font ?? UiBuilder.MonoFont, + Color = 0xFFFFFFFF, + WrapWidth = float.MaxValue, + TargetDrawList = drawList + }; + + var iconMacro = $""; + var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams); + + ImGui.SetCursorScreenPos(position); + ImGui.InvisibleButton($"##iconHitbox_{Guid.NewGuid()}", drawResult.Size); + + return drawResult.Size; + } + + #region Internal Payloads + + private abstract class AbstractColorPayload : Payload + { + protected byte Red { get; init; } + protected byte Green { get; init; } + protected byte Blue { get; init; } + + protected override byte[] EncodeImpl() + { + return new byte[] { 0x02, ChunkType, 0x05, 0xF6, Red, Green, Blue, 0x03 }; + } + + protected override void DecodeImpl(BinaryReader reader, long endOfStream) { } + + public override PayloadType Type => PayloadType.Unknown; + protected abstract byte ChunkType { get; } + } + + private abstract class AbstractColorEndPayload : Payload + { + protected override byte[] EncodeImpl() + { + return new byte[] { 0x02, ChunkType, 0x02, 0xEC, 0x03 }; + } + + protected override void DecodeImpl(BinaryReader reader, long endOfStream) { } + + public override PayloadType Type => PayloadType.Unknown; + protected abstract byte ChunkType { get; } + } + + private class ColorPayload : AbstractColorPayload + { + protected override byte ChunkType => 0x13; + + public ColorPayload(Vector3 color) + { + Red = Math.Max((byte)1, (byte)(color.X * 255f)); + Green = Math.Max((byte)1, (byte)(color.Y * 255f)); + Blue = Math.Max((byte)1, (byte)(color.Z * 255f)); + } + + public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } + } + + private class ColorEndPayload : AbstractColorEndPayload + { + protected override byte ChunkType => 0x13; + } + + private class GlowPayload : AbstractColorPayload + { + protected override byte ChunkType => 0x14; + + public GlowPayload(Vector3 color) + { + Red = Math.Max((byte)1, (byte)(color.X * 255f)); + Green = Math.Max((byte)1, (byte)(color.Y * 255f)); + Blue = Math.Max((byte)1, (byte)(color.Z * 255f)); + } + + public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } + } + + private class GlowEndPayload : AbstractColorEndPayload + { + protected override byte ChunkType => 0x14; + } + + #endregion +} diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index 5f860bf..95f1de8 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -1,5 +1,6 @@ using LightlessSync.API.Data; using LightlessSync.API.Dto; +using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; @@ -34,6 +35,36 @@ public partial class ApiController await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false); } + public async Task TryPairWithContentId(string otherCid, string myCid) + { + if (!IsConnected) return; + await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false); + } + + public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) + { + CheckConnection(); + await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, enabled, groupDto).ConfigureAwait(false); + } + + public async Task IsUserBroadcasting(string hashedCid) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync(nameof(IsUserBroadcasting), hashedCid).ConfigureAwait(false); + } + + public async Task AreUsersBroadcasting(List hashedCids) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false); + } + + public async Task GetBroadcastTtl(string hashedCid) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false); + } + public async Task UserDelete() { CheckConnection(); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs index 6b7c785..cd17ea8 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -1,4 +1,5 @@ using LightlessSync.API.Dto.Group; +using LightlessSync.API.Dto.User; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.AspNetCore.SignalR.Client; @@ -80,6 +81,11 @@ public partial class ApiController CheckConnection(); return await _lightlessHub!.InvokeAsync(nameof(GroupJoinFinalize), passwordedGroup).ConfigureAwait(false); } + public async Task GroupJoinHashed(GroupJoinHashedDto dto) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync("GroupJoinHashed", dto).ConfigureAwait(false); + } public async Task GroupLeave(GroupDto group) { @@ -116,6 +122,18 @@ public partial class ApiController CheckConnection(); await _lightlessHub!.SendAsync(nameof(GroupUnbanUser), groupPair).ConfigureAwait(false); } + public async Task SetGroupBroadcastStatus(GroupBroadcastRequestDto dto) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync(nameof(SetGroupBroadcastStatus), dto).ConfigureAwait(false); + } + public async Task> GetBroadcastedGroups(List broadcastEntries) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync>(nameof(GetBroadcastedGroups), broadcastEntries) + .ConfigureAwait(false); + } + private void CheckConnection() { diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 8f569a6..7653529 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -100,6 +100,8 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL public string UID => _connectionDto?.User.UID ?? string.Empty; + public event Action? OnConnected; + public async Task CheckClientHealth() { return await _lightlessHub!.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); @@ -230,6 +232,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL _connectionDto = await GetConnectionDto().ConfigureAwait(false); ServerState = ServerState.Connected; + OnConnected?.Invoke(); var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!; @@ -517,6 +520,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL return; } ServerState = ServerState.Connected; + OnConnected?.Invoke(); await LoadIninitialPairsAsync().ConfigureAwait(false); await LoadOnlinePairsAsync().ConfigureAwait(false); Mediator.Publish(new ConnectedMessage(_connectionDto)); -- 2.49.1 From 9d850f8fa6ec8c6c69c6d4d6144b1de565a12bb2 Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 24 Sep 2025 06:57:01 +0900 Subject: [PATCH 05/69] quick fix --- LightlessSync/UI/BroadcastUI.cs | 2 +- LightlessSync/UI/CompactUI.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 7d60e58..7799601 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -154,7 +154,7 @@ namespace LightlessSync.UI ImGuiHelpers.ScaledDummy(0.2f); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); - if (_broadcastService.IsBroadcasting) + if (_configService.Current.BroadcastEnabled) { ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen")); ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe.. diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 1d2105a..cd920b8 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -169,7 +169,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGuiHelpers.ScaledDummy(0.2f); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); - if (_broadcastService.IsBroadcasting) + if (_configService.Current.BroadcastEnabled) { ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen")); ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe.. -- 2.49.1 From 0c38b9397a41d1a81236c487ef67bc95a13a8eee Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 24 Sep 2025 07:18:08 +0900 Subject: [PATCH 06/69] i'm a genius 2 --- LightlessSync/Services/BroadcastService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index 7e887ed..1d50499 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -140,7 +140,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber }; } - _ = _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false); + await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false); _logger.LogInformation("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); @@ -354,6 +354,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _logger.LogDebug("OnTick skipped: waiting for TTL fetch"); return; } + var expiry = _config.Current.BroadcastTtl; var remaining = expiry - DateTime.UtcNow; _remainingTtl = remaining > TimeSpan.Zero ? remaining : null; -- 2.49.1 From d91f1a3356b2ef9b7fc6f7a30cbafdfd8cb4a892 Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 24 Sep 2025 07:19:16 +0900 Subject: [PATCH 07/69] and genius again --- LightlessSync/Services/NameplateService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 73c8dd1..cb2d978 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -128,7 +128,7 @@ public class NameplateService : DisposableMediatorSubscriberBase (isFriend && !friendColorAllowed) )) { - _logger.LogInformation("added nameplate color to {Name}", playerCharacter.Name.TextValue); + //_logger.LogInformation("added nameplate color to {Name}", playerCharacter.Name.TextValue); handler.NameParts.TextWrap = CreateTextWrap(colors); } -- 2.49.1 From 7569b15993a8d24de573eecf59af5bd896885052 Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 24 Sep 2025 22:28:32 +0900 Subject: [PATCH 08/69] seperate scanning service not relying on nameplate updates & other improvements/fixes --- LightlessSync/Plugin.cs | 8 +- .../Services/BroadcastScanningService.cs | 216 +++++++++++++++++ LightlessSync/Services/BroadcastService.cs | 2 + LightlessSync/Services/NameplateHandler.cs | 20 +- LightlessSync/Services/NameplateService.cs | 225 +----------------- LightlessSync/UI/BroadcastUI.cs | 22 +- LightlessSync/UI/SyncshellFinderUI.cs | 11 +- LightlessSync/UI/UIColors.cs | 1 + 8 files changed, 253 insertions(+), 252 deletions(-) create mode 100644 LightlessSync/Services/BroadcastScanningService.cs diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 1cab3dd..b9a70fe 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -205,6 +205,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService>(), clientState, objectTable, framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + // add scoped services collection.AddScoped(); @@ -227,8 +229,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); - collection.AddScoped((s) => new BroadcastUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new BroadcastUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); @@ -246,7 +248,7 @@ public sealed class Plugin : IDalamudPlugin pluginInterface, textureProvider, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), namePlateGui, clientState, - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService())); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScanningService.cs new file mode 100644 index 0000000..313abb8 --- /dev/null +++ b/LightlessSync/Services/BroadcastScanningService.cs @@ -0,0 +1,216 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Plugin.Services; +using LightlessSync.API.Dto.User; +using LightlessSync.LightlessConfiguration; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace LightlessSync.Services; + +public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable +{ + private readonly ILogger _logger; + private readonly IObjectTable _objectTable; + private readonly IFramework _framework; + + private readonly BroadcastService _broadcastService; + private readonly NameplateHandler _nameplateHandler; + + private readonly ConcurrentDictionary _broadcastCache = new(); + private readonly Queue _lookupQueue = new(); + private readonly HashSet _lookupQueuedCids = new(); + private readonly HashSet _syncshellCids = new(); + + private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4); + private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); + + private readonly CancellationTokenSource _cleanupCts = new(); + private Task? _cleanupTask; + + private int _frameCounter = 0; + private int _lookupsThisFrame = 0; + private const int MaxLookupsPerFrame = 15; + private const int MaxQueueSize = 100; + + public IReadOnlyDictionary BroadcastCache => _broadcastCache; + public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); + + public BroadcastScannerService(ILogger logger, + IClientState clientState, + IObjectTable objectTable, + IFramework framework, + BroadcastService broadcastService, + LightlessMediator mediator, + NameplateHandler nameplateHandler, + DalamudUtilService dalamudUtil, + LightlessConfigService configService) : base(logger, mediator) + { + _logger = logger; + _objectTable = objectTable; + _broadcastService = broadcastService; + _nameplateHandler = nameplateHandler; + + _logger = logger; + _framework = framework; + _framework.Update += OnFrameworkUpdate; + + Mediator.Subscribe(this, OnBroadcastStatusChanged); + _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); + + _nameplateHandler.Init(); + } + + private void OnFrameworkUpdate(IFramework framework) => Update(); + + public void Update() + { + _frameCounter++; + _lookupsThisFrame = 0; + + if (!_broadcastService.IsBroadcasting) + return; + + var now = DateTime.UtcNow; + + foreach (var obj in _objectTable) + { + if (obj is not IPlayerCharacter player || player.Address == IntPtr.Zero) + continue; + + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address); + var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now; + + if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize) + _lookupQueue.Enqueue(cid); + } + + if (_frameCounter % 2 == 0 && _lookupQueue.Count > 0) + { + var cidsToLookup = new List(); + while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame) + { + var cid = _lookupQueue.Dequeue(); + _lookupQueuedCids.Remove(cid); + cidsToLookup.Add(cid); + _lookupsThisFrame++; + } + + if (cidsToLookup.Count > 0) + _ = BatchUpdateBroadcastCacheAsync(cidsToLookup); + } + } + + private async Task BatchUpdateBroadcastCacheAsync(List cids) + { + var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false); + var now = DateTime.UtcNow; + + foreach (var (cid, info) in results) + { + if (string.IsNullOrWhiteSpace(cid) || info == null) + continue; + + var ttl = info.IsBroadcasting && info.TTL.HasValue + ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks)) + : RetryDelay; + + var expiry = now + ttl; + + _broadcastCache.AddOrUpdate(cid, + new BroadcastEntry(info.IsBroadcasting, expiry, info.GID), + (_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID)); + } + + var activeCids = _broadcastCache + .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now) + .Select(e => e.Key) + .ToList(); + + _nameplateHandler.UpdateBroadcastingCids(activeCids); + UpdateSyncshellBroadcasts(); + } + + private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg) + { + if (!msg.Enabled) + { + _broadcastCache.Clear(); + _lookupQueue.Clear(); + _lookupQueuedCids.Clear(); + _syncshellCids.Clear(); + + _nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty()); + } + } + + private void UpdateSyncshellBroadcasts() + { + var now = DateTime.UtcNow; + var newSet = _broadcastCache + .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) + .Select(e => e.Key) + .ToHashSet(); + + if (!_syncshellCids.SetEquals(newSet)) + { + _syncshellCids.Clear(); + foreach (var cid in newSet) + _syncshellCids.Add(cid); + + Mediator.Publish(new SyncshellBroadcastsUpdatedMessage()); + } + } + + public List GetActiveSyncshellBroadcasts() + { + var now = DateTime.UtcNow; + + return _broadcastCache + .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) + .Select(e => new BroadcastStatusInfoDto + { + HashedCID = e.Key, + IsBroadcasting = true, + TTL = e.Value.ExpiryTime - now, + GID = e.Value.GID + }) + .ToList(); + } + + private async Task ExpiredBroadcastCleanupLoop() + { + var token = _cleanupCts.Token; + + try + { + while (!token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(10), token); + + var now = DateTime.UtcNow; + foreach (var (cid, entry) in _broadcastCache.ToArray()) + { + if (entry.ExpiryTime <= now) + _broadcastCache.TryRemove(cid, out _); + } + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + _logger.LogError(ex, "Broadcast cleanup loop crashed"); + } + + UpdateSyncshellBroadcasts(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _framework.Update -= OnFrameworkUpdate; + _cleanupCts.Cancel(); + _cleanupTask?.Wait(100); + _nameplateHandler.Uninit(); + } +} diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index 1d50499..09a9674 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -151,6 +151,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational,$"Disabled Lightfinder for Player: {msg.HashedCid}"))); return; } @@ -166,6 +167,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _logger.LogInformation("Fetched TTL from server: {TTL}", remaining); _mediator.Publish(new BroadcastStatusChangedMessage(true, remaining)); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); } else { diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs index cbb4777..762bc52 100644 --- a/LightlessSync/Services/NameplateHandler.cs +++ b/LightlessSync/Services/NameplateHandler.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using LightlessSync.Services.Mediator; +using LightlessSync.UI; using LightlessSync.Utils; // Created using https://github.com/PunishedPineapple/Distance as a reference, thank you! @@ -205,6 +206,9 @@ public unsafe class NameplateHandler : IMediatorSubscriber var nameContainer = nameplateObject.NameContainer; var nameText = nameplateObject.NameText; + var labelColor = UIColors.Get("LightlessPurple"); + var edgeColor = UIColors.Get("FullBlack"); + var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY); pNode->AtkResNode.SetPositionShort(58, (short)labelY); @@ -213,15 +217,15 @@ public unsafe class NameplateHandler : IMediatorSubscriber pNode->AtkResNode.Color.A = 255; - pNode->TextColor.A = 255; - pNode->TextColor.R = 173; - pNode->TextColor.G = 138; - pNode->TextColor.B = 245; + pNode->TextColor.R = (byte)(labelColor.X * 255); + pNode->TextColor.G = (byte)(labelColor.Y * 255); + pNode->TextColor.B = (byte)(labelColor.Z * 255); + pNode->TextColor.A = (byte)(labelColor.W * 255); - pNode->EdgeColor.A = 255; - pNode->EdgeColor.R = 0; - pNode->EdgeColor.G = 0; - pNode->EdgeColor.B = 0; + pNode->EdgeColor.R = (byte)(edgeColor.X * 255); + pNode->EdgeColor.G = (byte)(edgeColor.Y * 255); + pNode->EdgeColor.B = (byte)(edgeColor.Z * 255); + pNode->EdgeColor.A = (byte)(edgeColor.W * 255); pNode->FontSize = 24; pNode->AlignmentType = AlignmentType.Center; diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index cb2d978..9de944e 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -3,13 +3,11 @@ using Dalamud.Game.Gui.NamePlate; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using Dalamud.Utility; -using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.UI; using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; namespace LightlessSync.Services; @@ -20,80 +18,28 @@ public class NameplateService : DisposableMediatorSubscriberBase private readonly IClientState _clientState; private readonly INamePlateGui _namePlateGui; private readonly PairManager _pairManager; - private readonly BroadcastService _broadcastService; - private readonly DalamudUtilService _dalamudUtil; - private readonly NameplateHandler _nameplatehandler; - private readonly IFramework _framework; - private readonly IGameGui _gameGui; - - private readonly ConcurrentDictionary _broadcastCache = new(); - private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(5); - private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); - private readonly Queue _lookupQueue = new(); - private readonly HashSet _lookupQueuedCids = new(); - private readonly HashSet _syncshellCids = new(); - - private readonly CancellationTokenSource _cleanupCts = new(); - private Task? _cleanupTask; - - private const int MaxLookupsPerFrame = 15; - private const int MaxQueueSize = 100; - - private int _lookupsThisFrame = 0; - private int _frameCounter = 0; - - public IReadOnlyDictionary BroadcastCache => _broadcastCache; - - public readonly struct BroadcastEntry - { - public readonly bool IsBroadcasting; - public readonly DateTime ExpiryTime; - public readonly bool PrefixApplied; - public readonly string? GID; - - public BroadcastEntry(bool isBroadcasting, DateTime expiryTime, bool prefixApplied, string? gid = null) - { - IsBroadcasting = isBroadcasting; - ExpiryTime = expiryTime; - PrefixApplied = prefixApplied; - GID = gid; - } - } public NameplateService(ILogger logger, LightlessConfigService configService, INamePlateGui namePlateGui, IClientState clientState, PairManager pairManager, - BroadcastService broadcastService, - LightlessMediator lightlessMediator, - DalamudUtilService dalamudUtil, - NameplateHandler nameplatehandler, - IGameGui gameGui) : base(logger, lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator) { _logger = logger; _configService = configService; _namePlateGui = namePlateGui; _clientState = clientState; _pairManager = pairManager; - _broadcastService = broadcastService; - _dalamudUtil = dalamudUtil; - _nameplatehandler = nameplatehandler; - _gameGui = gameGui; _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; _namePlateGui.RequestRedraw(); Mediator.Subscribe(this, (_) => _namePlateGui.RequestRedraw()); - Mediator.Subscribe(this, OnBroadcastStatusChanged); - _nameplatehandler.Init(); - _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); } private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) { - _frameCounter++; - _lookupsThisFrame = 0; if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) return; @@ -112,11 +58,6 @@ public class NameplateService : DisposableMediatorSubscriberBase if (playerCharacter == null) continue; - - var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(playerCharacter.Address); - var hasEntry = _broadcastCache.TryGetValue(cid, out var entry); - var isEntryStale = !hasEntry || entry.ExpiryTime <= now; - var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember); var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend); bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty); @@ -132,167 +73,7 @@ public class NameplateService : DisposableMediatorSubscriberBase handler.NameParts.TextWrap = CreateTextWrap(colors); } - if (!_broadcastService.IsBroadcasting) - continue; - - if (isEntryStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize) - { - _lookupQueue.Enqueue(cid); - } } - - if (_broadcastService.IsBroadcasting && _frameCounter % 2 == 0) - { - var cidsToLookup = new List(); - while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame) - { - var nextCid = _lookupQueue.Dequeue(); - _lookupQueuedCids.Remove(nextCid); - cidsToLookup.Add(nextCid); - _lookupsThisFrame++; - } - - if (cidsToLookup.Count > 0) - _ = BatchUpdateBroadcastCacheAsync(cidsToLookup); - } - } - - private async Task BatchUpdateBroadcastCacheAsync(List cidList) - { - var results = await _broadcastService.AreUsersBroadcastingAsync(cidList).ConfigureAwait(false); - var now = DateTime.UtcNow; - - foreach (var (cid, info) in results) - { - if (string.IsNullOrWhiteSpace(cid) || info == null) - { - _logger.LogWarning("Skipping broadcast entry: cid={Cid}, info=null or empty", cid); - continue; - } - - bool isBroadcasting = info.IsBroadcasting; - TimeSpan effectiveTtl = isBroadcasting && info.TTL.HasValue - ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks)) - : RetryDelay; - - var expiryTime = now + effectiveTtl; - - _broadcastCache.AddOrUpdate(cid, - new BroadcastEntry(isBroadcasting, expiryTime, false, info.GID), - (_, old) => new BroadcastEntry(isBroadcasting, expiryTime, old.PrefixApplied, info.GID)); - } - - var activeCids = _broadcastCache - .Where(kvp => kvp.Value.IsBroadcasting) - .Select(kvp => kvp.Key) - .ToList(); - - _nameplatehandler.UpdateBroadcastingCids(activeCids); - _namePlateGui.RequestRedraw(); - - UpdateSyncshellBroadcasts(); - } - - private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg) - { - if (!msg.Enabled) - { - _logger.LogInformation("Broadcast disabled, clearing prefix cache and queue"); - - _broadcastCache.Clear(); - _lookupQueue.Clear(); - _lookupQueuedCids.Clear(); - _syncshellCids.Clear(); - - _nameplatehandler.UpdateBroadcastingCids(Enumerable.Empty()); - _namePlateGui.RequestRedraw(); - } - } - - public List GetActiveSyncshellBroadcasts() - { - var now = DateTime.UtcNow; - - return _broadcastCache - .Where(kvp => - kvp.Value.IsBroadcasting && - kvp.Value.ExpiryTime > now && - !string.IsNullOrEmpty(kvp.Value.GID)) - .Select(kvp => new BroadcastStatusInfoDto - { - HashedCID = kvp.Key, - IsBroadcasting = true, - TTL = kvp.Value.ExpiryTime - now, - GID = kvp.Value.GID - }) - .ToList(); - } - - private void UpdateSyncshellBroadcasts() - { - var now = DateTime.UtcNow; - - var newSet = _broadcastCache - .Where(kvp => kvp.Value.IsBroadcasting && kvp.Value.ExpiryTime > now && !string.IsNullOrEmpty(kvp.Value.GID)) - .Select(kvp => kvp.Key) - .ToHashSet(); - - if (!_syncshellCids.SetEquals(newSet)) - { - _syncshellCids.Clear(); - foreach (var cid in newSet) - _syncshellCids.Add(cid); - - _logger.LogInformation("Syncshell broadcast entries changed, sending update lol"); - Mediator.Publish(new SyncshellBroadcastsUpdatedMessage()); - } - } - - public bool IsBroadcastingKnown(string cidHash, out bool isBroadcasting) - { - if (_broadcastCache.TryGetValue(cidHash, out var entry)) - { - isBroadcasting = entry.IsBroadcasting; - return true; - } - - isBroadcasting = false; - return false; - } - - private async Task ExpiredBroadcastCleanupLoop() - { - var token = _cleanupCts.Token; - - try - { - while (!token.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromSeconds(10), token); - - var now = DateTime.UtcNow; - foreach (var (cid, entry) in _broadcastCache.ToArray()) - { - if (entry.ExpiryTime <= now) - { - if (_broadcastCache.TryRemove(cid, out _)) - { - _logger.LogInformation("Removed expired broadcast entry: {Cid}", cid); - } - } - } - } - } - catch (OperationCanceledException) - { - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in ExpiredBroadcastCleanupLoop"); - } - - UpdateSyncshellBroadcasts(); } public void RequestRedraw() @@ -319,11 +100,7 @@ public class NameplateService : DisposableMediatorSubscriberBase { base.Dispose(disposing); - _cleanupCts.Cancel(); - _cleanupTask?.Wait(100); - _namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; _namePlateGui.RequestRedraw(); - _nameplatehandler.Uninit(); } } \ No newline at end of file diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 7799601..6f9cdc8 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -17,7 +17,7 @@ namespace LightlessSync.UI private readonly LightlessConfigService _configService; private readonly BroadcastService _broadcastService; private readonly UiSharedService _uiSharedService; - private readonly NameplateService _nameplateService; + private readonly BroadcastScannerService _broadcastScannerService; private IReadOnlyList _allSyncshells; private string _userUid = string.Empty; @@ -32,20 +32,20 @@ namespace LightlessSync.UI LightlessConfigService configService, UiSharedService uiShared, ApiController apiController, - NameplateService nameplateService + BroadcastScannerService broadcastScannerService ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; _configService = configService; _apiController = apiController; - _nameplateService = nameplateService; + _broadcastScannerService = broadcastScannerService; IsOpen = false; this.SizeConstraints = new() { - MinimumSize = new(590, 340), - MaximumSize = new(590, 340) + MinimumSize = new(600, 340), + MaximumSize = new(750, 400) }; mediator.Subscribe(this, async _ => await RefreshSyncshells()); @@ -126,9 +126,7 @@ namespace LightlessSync.UI { if (!_broadcastService.IsLightFinderAvailable) { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); - ImGui.TextWrapped("This server doesn't support LightFinder."); - ImGui.PopStyleColor(); + _uiSharedService.MediumText("This server doesn't support Lightfinder.", UIColors.Get("LightlessYellow")); ImGuiHelpers.ScaledDummy(0.25f); } @@ -203,7 +201,7 @@ namespace LightlessSync.UI else ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue")); - if (isOnCooldown) + if (isOnCooldown || !_broadcastService.IsLightFinderAvailable) ImGui.BeginDisabled(); string buttonText = isBroadcasting ? "Disable Lightfinder" : "Enable Lightfinder"; @@ -213,7 +211,7 @@ namespace LightlessSync.UI _broadcastService.ToggleBroadcast(); } - if (isOnCooldown) + if (isOnCooldown || !_broadcastService.IsLightFinderAvailable) ImGui.EndDisabled(); ImGui.PopStyleColor(); @@ -316,7 +314,7 @@ namespace LightlessSync.UI { ImGui.Text("Broadcast Cache"); - if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 200f))) + if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f))) { ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch); @@ -326,7 +324,7 @@ namespace LightlessSync.UI var now = DateTime.UtcNow; - foreach (var (cid, entry) in _nameplateService.BroadcastCache) + foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache) { ImGui.TableNextRow(); diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index cd6983c..6f4050e 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -23,7 +23,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private readonly LightlessConfigService _configService; private readonly BroadcastService _broadcastService; private readonly UiSharedService _uiSharedService; - private readonly NameplateService _nameplateService; + private readonly BroadcastScannerService _broadcastScannerService; private readonly List _nearbySyncshells = new(); private int _selectedNearbyIndex = -1; @@ -40,23 +40,24 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase LightlessConfigService configService, UiSharedService uiShared, ApiController apiController, - NameplateService nameplateService + BroadcastScannerService broadcastScannerService ) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; _configService = configService; _apiController = apiController; - _nameplateService = nameplateService; + _broadcastScannerService = broadcastScannerService; IsOpen = false; SizeConstraints = new() { MinimumSize = new(600, 400), - MaximumSize = new(600, 400) + MaximumSize = new(600, 550) }; Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync()); + Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync()); } public override async void OnOpen() @@ -222,7 +223,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private async Task RefreshSyncshellsAsync() { - var syncshellBroadcasts = _nameplateService.GetActiveSyncshellBroadcasts(); + var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); if (syncshellBroadcasts.Count == 0) { diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index 8fceb33..3bd288f 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -13,6 +13,7 @@ namespace LightlessSync.UI { "LightlessPurpleDefault", "#9375d1" }, { "ButtonDefault", "#323232" }, + { "FullBlack", "#000000" }, { "LightlessBlue", "#a6c2ff" }, { "LightlessYellow", "#ffe97a" }, -- 2.49.1 From e8f8512cddb555bc2916d92d4c0ca220f20fab46 Mon Sep 17 00:00:00 2001 From: azyges Date: Thu, 25 Sep 2025 06:06:19 +0900 Subject: [PATCH 09/69] updated layout and adjusted scanning --- .../Services/BroadcastScanningService.cs | 14 +- LightlessSync/UI/CompactUI.cs | 179 +++++++++--------- LightlessSync/UI/TopTabMenu.cs | 40 ++-- LightlessSync/UI/UISharedService.cs | 26 ++- 4 files changed, 148 insertions(+), 111 deletions(-) diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScanningService.cs index 313abb8..4619a5e 100644 --- a/LightlessSync/Services/BroadcastScanningService.cs +++ b/LightlessSync/Services/BroadcastScanningService.cs @@ -28,11 +28,14 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos private readonly CancellationTokenSource _cleanupCts = new(); private Task? _cleanupTask; + private int _checkEveryFrames = 20; private int _frameCounter = 0; private int _lookupsThisFrame = 0; - private const int MaxLookupsPerFrame = 15; + private const int MaxLookupsPerFrame = 30; private const int MaxQueueSize = 100; + private volatile bool _batchRunning = false; + public IReadOnlyDictionary BroadcastCache => _broadcastCache; public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); @@ -85,7 +88,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos _lookupQueue.Enqueue(cid); } - if (_frameCounter % 2 == 0 && _lookupQueue.Count > 0) + if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0) { var cidsToLookup = new List(); while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame) @@ -96,8 +99,11 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos _lookupsThisFrame++; } - if (cidsToLookup.Count > 0) - _ = BatchUpdateBroadcastCacheAsync(cidsToLookup); + if (cidsToLookup.Count > 0 && !_batchRunning) + { + _batchRunning = true; + _ = BatchUpdateBroadcastCacheAsync(cidsToLookup).ContinueWith(_ => _batchRunning = false); + } } } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index cd920b8..5a7439a 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -139,89 +139,6 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.EndTooltip(); } }, - new TitleBarButton() - { - Icon = FontAwesomeIcon.TowerCell, - Click = (_) => - { - Mediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); - }, - IconOffset = new(2, 1), - - ShowTooltip = () => - { - ImGui.BeginTooltip(); - - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue")); - ImGui.Text("Lightfinder"); - ImGui.PopStyleColor(); - - ImGui.Text("This lets other Lightless users know you use Lightless."); - ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); - ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); - ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); - ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); - - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("Use it only when you want to be visible."); - ImGui.PopStyleColor(); - - ImGuiHelpers.ScaledDummy(0.2f); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); - - if (_configService.Current.BroadcastEnabled) - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen")); - ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe.. - ImGui.PopStyleColor(); - - var ttl = _broadcastService.RemainingTtl; - if (ttl is { } remaining && remaining > TimeSpan.Zero) - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); - ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}"); - ImGui.PopStyleColor(); - } - else - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder's light wanes, but not in vain."); // cringe.. - ImGui.PopStyleColor(); - } - } - else - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe.. - ImGui.PopStyleColor(); - } - - var cooldown = _broadcastService.RemainingCooldown; - if (cooldown is { } cd) - { - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)"); - ImGui.PopStyleColor(); - } - - ImGui.EndTooltip(); - } - }, - new TitleBarButton() - { - Icon = FontAwesomeIcon.NetworkWired, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI))); - }, - IconOffset = new(2,1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("Nearby Syncshells"); - ImGui.EndTooltip(); - } - } }; _drawFolders = [.. GetDrawFolders()]; @@ -518,18 +435,102 @@ public class CompactUi : WindowMediatorSubscriberBase //Getting information of character and triangles threshold to show overlimit status in UID bar. _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); + Vector2 uidTextSize, iconSize; using (_uiSharedService.UidFont.Push()) + uidTextSize = ImGui.CalcTextSize(uidText); + + using (_uiSharedService.IconFont.Push()) + iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString()); + + float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + float uidStartX = (contentWidth - uidTextSize.X) / 2f; + float cursorY = ImGui.GetCursorPosY(); + + if (_configService.Current.BroadcastEnabled) { - var uidTextSize = ImGui.CalcTextSize(uidText); - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (uidTextSize.X / 2)); - ImGui.TextColored(GetUidColor(), uidText); + float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f; + var buttonSize = new Vector2(iconSize.X, uidTextSize.Y); + + ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY)); + ImGui.InvisibleButton("BroadcastIcon", buttonSize); + + var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset); + using (_uiSharedService.IconFont.Push()) + ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString()); + + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue")); + ImGui.Text("Lightfinder"); + ImGui.PopStyleColor(); + + ImGui.Text("This lets other Lightless users know you use Lightless."); + ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); + ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); + ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); + ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); + + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("Use it only when you want to be visible."); + ImGui.PopStyleColor(); + + ImGuiHelpers.ScaledDummy(0.2f); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + + if (_configService.Current.BroadcastEnabled) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen")); + ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe.. + ImGui.PopStyleColor(); + + var ttl = _broadcastService.RemainingTtl; + if (ttl is { } remaining && remaining > TimeSpan.Zero) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); + ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}"); + ImGui.PopStyleColor(); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("The Lightfinder's light wanes, but not in vain."); // cringe.. + ImGui.PopStyleColor(); + } + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe.. + ImGui.PopStyleColor(); + } + + var cooldown = _broadcastService.RemainingCooldown; + if (cooldown is { } cd) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)"); + ImGui.PopStyleColor(); + } + + ImGui.EndTooltip(); + } + + if (ImGui.IsItemClicked()) + _lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); } - UiSharedService.AttachToolTip("Click to copy"); - if (ImGui.IsItemClicked()) + ImGui.SetCursorPosY(cursorY); + ImGui.SetCursorPosX(uidStartX); + using (_uiSharedService.UidFont.Push()) { - ImGui.SetClipboardText(uidText); + ImGui.TextColored(GetUidColor(), uidText); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(uidText); } + UiSharedService.AttachToolTip("Click to copy"); if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected) { diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index f5269a7..c7f5035 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -39,7 +39,7 @@ public class TopTabMenu None, Individual, Syncshell, - Filter, + Lightfinder, UserConfig } @@ -60,11 +60,6 @@ public class TopTabMenu { get => _selectedTab; set { - if (_selectedTab == SelectedTab.Filter && value != SelectedTab.Filter) - { - Filter = string.Empty; - } - _selectedTab = value; } } @@ -76,7 +71,7 @@ public class TopTabMenu var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y; var buttonSize = new Vector2(buttonX, buttonY); var drawList = ImGui.GetWindowDrawList(); - var underlineColor = ImGui.GetColorU32(ImGuiCol.Separator); + var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator); var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))); ImGuiHelpers.ScaledDummy(spacing.Y / 2f); @@ -117,19 +112,19 @@ public class TopTabMenu using (ImRaii.PushFont(UiBuilder.IconFont)) { var x = ImGui.GetCursorScreenPos(); - if (ImGui.Button(FontAwesomeIcon.Filter.ToIconString(), buttonSize)) + if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Filter ? SelectedTab.None : SelectedTab.Filter; + TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder; } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Filter) + if (TabSelection == SelectedTab.Lightfinder) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); } - UiSharedService.AttachToolTip("Filter"); + UiSharedService.AttachToolTip("Lightfinder"); ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) @@ -164,9 +159,9 @@ public class TopTabMenu DrawSyncshellMenu(availableWidth, spacing.X); DrawGlobalSyncshellButtons(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.Filter) + else if (TabSelection == SelectedTab.Lightfinder) { - DrawFilter(availableWidth, spacing.X); + DrawLightfinderMenu(availableWidth, spacing.X); } else if (TabSelection == SelectedTab.UserConfig) { @@ -175,6 +170,8 @@ public class TopTabMenu if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); ImGui.Separator(); + + DrawFilter(availableWidth, spacing.X); } private void DrawAddPair(float availableXWidth, float spacingX) @@ -483,6 +480,23 @@ public class TopTabMenu } } + private void DrawLightfinderMenu(float availableWidth, float spacingX) + { + var buttonX = (availableWidth - (spacingX)) / 2f; + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true)) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); + } + + ImGui.SameLine(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, "Syncshell Finder", buttonX, center: true)) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI))); + } + } + private void DrawUserConfig(float availableWidth, float spacingX) { var buttonX = (availableWidth - spacingX) / 2f; diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 9a45d15..e4753dc 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -1144,11 +1144,11 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase IconText(icon, color == null ? ImGui.GetColorU32(ImGuiCol.Text) : ImGui.GetColorU32(color.Value)); } - public bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false) + public bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false, bool? center = null) { return IconTextButtonInternal(icon, text, isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null, - width <= 0 ? null : width); + width <= 0 ? null : width, center); } public IDalamudTextureWrap LoadImage(byte[] imageData) @@ -1212,7 +1212,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.TextUnformatted(text); } - private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null) + private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null, bool? center = null) { int num = 0; if (defaultColor.HasValue) @@ -1222,19 +1222,35 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase } ImGui.PushID(text); + Vector2 vector; using (IconFont.Push()) vector = ImGui.CalcTextSize(icon.ToIconString()); + Vector2 vector2 = ImGui.CalcTextSize(text); ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList(); Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); float num2 = 3f * ImGuiHelpers.GlobalScale; - float x = width ?? vector.X + vector2.X + ImGui.GetStyle().FramePadding.X * 2f + num2; + + float totalTextWidth = vector.X + num2 + vector2.X; + float x = width ?? totalTextWidth + ImGui.GetStyle().FramePadding.X * 2f; float frameHeight = ImGui.GetFrameHeight(); + bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight)); - Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y); + + bool shouldCenter = center == true; + + Vector2 pos = shouldCenter + ? new Vector2( + cursorScreenPos.X + (x - totalTextWidth) / 2f, + cursorScreenPos.Y + (frameHeight - vector.Y) / 2f) + : new Vector2( + cursorScreenPos.X + ImGui.GetStyle().FramePadding.X, + cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y); + using (IconFont.Push()) windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); + Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y); windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text); ImGui.PopID(); -- 2.49.1 From 4060ba96f1618cd825a87a98ba627568595fc9a7 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 25 Sep 2025 00:15:42 +0200 Subject: [PATCH 10/69] moved settings button from compact UI to top tab menu --- LightlessSync/UI/CompactUI.cs | 17 +---------------- LightlessSync/UI/TopTabMenu.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 5a7439a..a14be42 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -109,21 +109,6 @@ public class CompactUi : WindowMediatorSubscriberBase AllowClickthrough = false; TitleBarButtons = new() { - new TitleBarButton() - { - Icon = FontAwesomeIcon.Cog, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); - }, - IconOffset = new(2,1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("Open Lightless Settings"); - ImGui.EndTooltip(); - } - }, new TitleBarButton() { Icon = FontAwesomeIcon.Book, diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index c7f5035..9aa112c 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -40,7 +40,8 @@ public class TopTabMenu Individual, Syncshell, Lightfinder, - UserConfig + UserConfig, + Settings } public string Filter @@ -67,7 +68,7 @@ public class TopTabMenu { var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; var spacing = ImGui.GetStyle().ItemSpacing; - var buttonX = (availableWidth - (spacing.X * 3)) / 4f; + var buttonX = (availableWidth - (spacing.X * 4)) / 5f; var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y; var buttonSize = new Vector2(buttonX, buttonY); var drawList = ImGui.GetWindowDrawList(); @@ -144,6 +145,17 @@ public class TopTabMenu } UiSharedService.AttachToolTip("Your User Menu"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + } + + } + UiSharedService.AttachToolTip("Lightless Sync Settings"); + ImGui.NewLine(); btncolor.Dispose(); -- 2.49.1 From 37c11e9d73a01f7cd2ec9c6bd4763a2f208167bf Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 03:34:59 +0200 Subject: [PATCH 11/69] Added tasks and added await on get groups --- LightlessSync/UI/BroadcastUI.cs | 2 +- LightlessSync/WebAPI/SignalR/ApiController.cs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 6f9cdc8..90b19ad 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -104,7 +104,7 @@ namespace LightlessSync.UI try { - _allSyncshells = await _apiController.GroupsGetAll(); + _allSyncshells = await _apiController.GroupsGetAll().ConfigureAwait(false); } catch (Exception ex) { diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 7653529..f341d5d 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -2,6 +2,7 @@ using LightlessSync.API.Data; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; +using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.API.SignalR; using LightlessSync.LightlessConfiguration; @@ -596,5 +597,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL ServerState = state; } + + public Task Client_GroupSendProfile(GroupProfileDto groupInfo) + { + throw new NotImplementedException(); + } + + public Task GroupGetProfile(GroupDto dto) + { + throw new NotImplementedException(); + } + + public Task GroupSetProfile(GroupProfileDto dto) + { + throw new NotImplementedException(); + } } #pragma warning restore MA0040 \ No newline at end of file -- 2.49.1 From 777e6b9d2799c6d6cb8fa396d21402eab112773a Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 25 Sep 2025 10:25:12 -0500 Subject: [PATCH 12/69] remove created at for now --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index fd4cd52..69055b0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit fd4cd52d2e78c8a621e6b06149e69842bb5ff255 +Subproject commit 69055b0f323e6d35f55750fd1dc5659a8e36b085 -- 2.49.1 From b0b149d8bca94e7d0d3ccbb07e4b63f8519fdf0d Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 25 Sep 2025 10:25:55 -0500 Subject: [PATCH 13/69] submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 69055b0..aec2a50 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 69055b0f323e6d35f55750fd1dc5659a8e36b085 +Subproject commit aec2a5023e8a513b63dc03d59a70aa51ab61941c -- 2.49.1 From 6bb379ebad4bd6b4ef9de82a69ca5421c07f8756 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 17:56:30 +0200 Subject: [PATCH 14/69] Returning finder on joining syncshell, added functions for syncshell profiles that would be needed later --- LightlessAPI | 2 +- LightlessSync/UI/SyncshellFinderUI.cs | 2 +- .../WebAPI/SignalR/ApiController.Functions.Callbacks.cs | 6 ++++++ LightlessSync/WebAPI/SignalR/ApiController.cs | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index aec2a50..d62adbb 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit aec2a5023e8a513b63dc03d59a70aa51ab61941c +Subproject commit d62adbb5b61ee12fc62e43cd70fb679eb2bcd1e4 diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 6f4050e..291cce6 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -189,7 +189,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); - _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); + _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions, true)); _joinDto = null; _joinInfo = null; } diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 154dbe6..457361a 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -277,6 +277,12 @@ public partial class ApiController _lightlessHub!.On(nameof(Client_GroupSendInfo), act); } + public void OnGroupUpdateProfile(Action act) + { + if (_initialized) return; + _lightlessHub!.On(nameof(Client_GroupSendProfile), act); + } + public void OnReceiveServerMessage(Action act) { if (_initialized) return; diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index f341d5d..4a4071b 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -445,6 +445,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto)); OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto)); OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); + OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto)); OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto)); OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto)); -- 2.49.1 From ab3ca78c7f09dcfd8a07f1e611e4db1594f8cb8c Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Thu, 25 Sep 2025 18:53:20 +0200 Subject: [PATCH 15/69] Fixed typo in setup --- LightlessSync/UI/IntroUI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/UI/IntroUI.cs b/LightlessSync/UI/IntroUI.cs index bc32128..470cadb 100644 --- a/LightlessSync/UI/IntroUI.cs +++ b/LightlessSync/UI/IntroUI.cs @@ -167,7 +167,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase } else { - UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " + + UiSharedService.TextWrapped("To not unnecessarily download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " + "Additionally, a local storage folder must be set where Lightless Sync will download other character files to. " + "Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service."); UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); -- 2.49.1 From 864a2e66771c1a7d5ad452abe39cf02ae8bf540c Mon Sep 17 00:00:00 2001 From: azyges Date: Fri, 26 Sep 2025 03:29:28 +0900 Subject: [PATCH 16/69] slight clean up and minor spelling mistake --- LightlessSync/Services/BroadcastService.cs | 41 +++++++++++----------- LightlessSync/UI/IntroUI.cs | 2 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index 09a9674..6d6409e 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -4,9 +4,7 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; -using LightlessSync.WebAPI.SignalR; using Microsoft.AspNetCore.SignalR; -using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -16,7 +14,6 @@ public class BroadcastService : IHostedService, IMediatorSubscriber private readonly ILogger _logger; private readonly ApiController _apiController; private readonly LightlessMediator _mediator; - private readonly HubFactory _hubFactory; private readonly LightlessConfigService _config; private readonly DalamudUtilService _dalamudUtil; public LightlessMediator Mediator => _mediator; @@ -29,35 +26,37 @@ public class BroadcastService : IHostedService, IMediatorSubscriber private TimeSpan? _remainingTtl = null; private DateTime _lastTtlCheck = DateTime.MinValue; private DateTime _lastForcedDisableTime = DateTime.MinValue; - private static readonly TimeSpan DisableCooldown = TimeSpan.FromSeconds(5); + private static readonly TimeSpan _disableCooldown = TimeSpan.FromSeconds(5); public TimeSpan? RemainingTtl => _remainingTtl; public TimeSpan? RemainingCooldown { get { var elapsed = DateTime.UtcNow - _lastForcedDisableTime; - if (elapsed >= DisableCooldown) return null; - return DisableCooldown - elapsed; + if (elapsed >= _disableCooldown) return null; + return _disableCooldown - elapsed; } } - public BroadcastService(ILogger logger, LightlessMediator mediator, HubFactory hubFactory, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController) + + public BroadcastService(ILogger logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController) { _logger = logger; _mediator = mediator; - _hubFactory = hubFactory; _config = config; _dalamudUtil = dalamudUtil; _apiController = apiController; } + private async Task RequireConnectionAsync(string context, Func action) { if (!_apiController.IsConnected) { - _logger.LogDebug($"{context} skipped, not connected"); + _logger.LogDebug(context + " skipped, not connected"); return; } await action().ConfigureAwait(false); } + public async Task StartAsync(CancellationToken cancellationToken) { _mediator.Subscribe(this, OnEnableBroadcast); @@ -65,7 +64,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _mediator.Subscribe(this, OnTick); _apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken); - _ = CheckLightfinderSupportAsync(cancellationToken); + //_ = CheckLightfinderSupportAsync(cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) @@ -86,13 +85,12 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (cancellationToken.IsCancellationRequested) return; - var hub = _hubFactory.GetOrCreate(CancellationToken.None); var dummy = "0".PadLeft(64, '0'); - await hub.InvokeAsync("IsUserBroadcasting", dummy, cancellationToken); - await hub.InvokeAsync("SetBroadcastStatus", dummy, true, null, cancellationToken); - await hub.InvokeAsync("GetBroadcastTtl", dummy, cancellationToken); - await hub.InvokeAsync>("AreUsersBroadcasting", new[] { dummy }, cancellationToken); + await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false); + await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false); + await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false); + await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false); IsLightFinderAvailable = true; _logger.LogInformation("Lightfinder is available."); @@ -119,6 +117,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); } } @@ -151,13 +150,13 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _config.Save(); _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); - Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational,$"Disabled Lightfinder for Player: {msg.HashedCid}"))); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}"))); return; } _waitingForTtlFetch = true; - var ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); + TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); if (ttl is { } remaining && remaining > TimeSpan.Zero) { @@ -325,8 +324,8 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _syncedOnStartup = true; try { - var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - var ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); + string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); if (ttl is { } remaining && remaining > TimeSpan.Zero) { @@ -357,8 +356,8 @@ public class BroadcastService : IHostedService, IMediatorSubscriber return; } - var expiry = _config.Current.BroadcastTtl; - var remaining = expiry - DateTime.UtcNow; + DateTime expiry = _config.Current.BroadcastTtl; + TimeSpan remaining = expiry - DateTime.UtcNow; _remainingTtl = remaining > TimeSpan.Zero ? remaining : null; if (_remainingTtl == null) { diff --git a/LightlessSync/UI/IntroUI.cs b/LightlessSync/UI/IntroUI.cs index bc32128..470cadb 100644 --- a/LightlessSync/UI/IntroUI.cs +++ b/LightlessSync/UI/IntroUI.cs @@ -167,7 +167,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase } else { - UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " + + UiSharedService.TextWrapped("To not unnecessarily download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " + "Additionally, a local storage folder must be set where Lightless Sync will download other character files to. " + "Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service."); UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); -- 2.49.1 From 9bc2ad24cd752d3b3e060c2017450d2dbfa4fdf5 Mon Sep 17 00:00:00 2001 From: azyges Date: Fri, 26 Sep 2025 04:51:05 +0900 Subject: [PATCH 17/69] clearing syncshell from lightfinder users --- LightlessAPI | 2 +- LightlessSync/UI/SyncshellAdminUI.cs | 20 +++++++++++++++---- LightlessSync/UI/SyncshellFinderUI.cs | 2 +- .../SignalR/ApiController.Functions.Groups.cs | 5 +++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index d62adbb..3c10380 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit d62adbb5b61ee12fc62e43cd70fb679eb2bcd1e4 +Subproject commit 3c10380162b162c47c99f63ecfc627a49887fe84 diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 3dd90de..fb30067 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -336,7 +336,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } ImGui.Separator(); - if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("LightlessPurple"))) + if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("DimRed"))) { using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) { @@ -348,6 +348,18 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell." + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + ImGui.SameLine(); + + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Brush, "Clear Lightfinder Users")) + { + _ = _apiController.GroupClearFinder(new(GroupFullInfo.Group)); + } + } + UiSharedService.AttachToolTip("This will remove all users that joined through Lightfinder from the Syncshell." + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + ImGuiHelpers.ScaledDummy(2f); ImGui.Separator(); ImGuiHelpers.ScaledDummy(2f); @@ -410,12 +422,12 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed."); } } - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + _uiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); ImGui.TreePop(); } ImGui.Separator(); - if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessPurple"))) + if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessYellow"))) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) { @@ -456,7 +468,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } ImGui.EndTable(); } - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } ImGui.Separator(); diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 291cce6..6f4050e 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -189,7 +189,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); - _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions, true)); + _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); _joinDto = null; _joinInfo = null; } diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs index cd17ea8..c7581d1 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -45,6 +45,11 @@ public partial class ApiController CheckConnection(); await _lightlessHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); } + public async Task GroupClearFinder(GroupDto group) + { + CheckConnection(); + await _lightlessHub!.SendAsync(nameof(GroupClearFinder), group).ConfigureAwait(false); + } public async Task GroupCreate() { -- 2.49.1 From e3a3f16d14706f330ae807708facda1189fb45ad Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Fri, 26 Sep 2025 04:40:15 +0200 Subject: [PATCH 18/69] Hiding of syncshells that already been joined in shell finder --- LightlessSync/Plugin.cs | 2 +- LightlessSync/UI/SyncshellFinderUI.cs | 91 ++++++++++++++------------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index b9a70fe..09f218e 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -230,7 +230,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped((s) => new BroadcastUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 6f4050e..e009bb5 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -4,15 +4,15 @@ 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.LightlessConfiguration; +using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; -using LightlessSync.API.Data.Extensions; using System.Numerics; namespace LightlessSync.UI; @@ -20,12 +20,12 @@ namespace LightlessSync.UI; public class SyncshellFinderUI : WindowMediatorSubscriberBase { private readonly ApiController _apiController; - private readonly LightlessConfigService _configService; private readonly BroadcastService _broadcastService; private readonly UiSharedService _uiSharedService; private readonly BroadcastScannerService _broadcastScannerService; + private readonly PairManager _pairManager; - private readonly List _nearbySyncshells = new(); + private readonly List _nearbySyncshells = []; private int _selectedNearbyIndex = -1; private GroupJoinDto? _joinDto; @@ -37,17 +37,16 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase LightlessMediator mediator, PerformanceCollectorService performanceCollectorService, BroadcastService broadcastService, - LightlessConfigService configService, UiSharedService uiShared, ApiController apiController, - BroadcastScannerService broadcastScannerService - ) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) + BroadcastScannerService broadcastScannerService, + PairManager pairManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; - _configService = configService; _apiController = apiController; _broadcastScannerService = broadcastScannerService; + _pairManager = pairManager; IsOpen = false; SizeConstraints = new() @@ -56,14 +55,14 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase MaximumSize = new(600, 550) }; - Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync()); - Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync()); + 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(); + await RefreshSyncshellsAsync().ConfigureAwait(false); } protected override void DrawInternal() @@ -170,28 +169,31 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private void DrawConfirmation() { - 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}")) + if (_joinDto != null && _joinInfo != null) { - var finalPermissions = GroupUserPreferredPermissions.NoneSet; - finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); - finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); - finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); + ImGui.Separator(); + ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}"); + ImGuiHelpers.ScaledDummy(2f); + ImGui.TextUnformatted("Suggested Syncshell Permissions:"); - _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); - _joinDto = null; - _joinInfo = null; + 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)); + _joinDto = null; + _joinInfo = null; + } } } @@ -224,6 +226,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private async Task RefreshSyncshellsAsync() { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); + var currentSyncshells = _pairManager.GroupPairs.Select(g => g.Key).ToList(); if (syncshellBroadcasts.Count == 0) { @@ -231,11 +234,20 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } - List updatedList; + List updatedList = []; try { - var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts); - updatedList = groups?.ToList() ?? new(); + var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); + if (groups != null && currentSyncshells != null) + { + foreach (var group in groups) + { + if (!currentSyncshells.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal))) + { + updatedList = groups?.ToList(); + } + } + } } catch (Exception ex) { @@ -243,8 +255,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } - var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(); - var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(); + var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal); + var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal); if (currentGids.SetEquals(newGids)) return; @@ -256,7 +268,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase if (previousGid != null) { - var newIndex = _nearbySyncshells.FindIndex(s => s.Group.GID == previousGid); + var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); if (newIndex >= 0) { _selectedNearbyIndex = newIndex; @@ -290,9 +302,4 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return _nearbySyncshells[_selectedNearbyIndex].Group.GID; } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - } } -- 2.49.1 From c182d10a0aa2725b77a18da202453057daab5fb3 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Fri, 26 Sep 2025 04:54:12 +0200 Subject: [PATCH 19/69] Moved the warning triangle downwards --- LightlessSync/UI/CompactUI.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index b93b393..6af9498 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -561,6 +561,7 @@ public class CompactUi : WindowMediatorSubscriberBase if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds) { ImGui.SameLine(); + ImGui.SetCursorPosY(cursorY + 15f); _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")); string warningMessage = ""; -- 2.49.1 From f6784fdf2864fe2aa9f0d9470065630ae1023134 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Fri, 26 Sep 2025 05:14:39 +0200 Subject: [PATCH 20/69] Refactored drawfolders function of CompactUI --- LightlessSync/UI/CompactUI.cs | 273 +++++++++++++++++----------------- 1 file changed, 137 insertions(+), 136 deletions(-) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 6af9498..77305c1 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -141,7 +141,7 @@ public class CompactUi : WindowMediatorSubscriberBase }, }; - _drawFolders = [.. GetDrawFolders()]; + _drawFolders = [.. DrawFolders]; #if DEBUG string dev = "Dev Build"; @@ -158,7 +158,7 @@ public class CompactUi : WindowMediatorSubscriberBase Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); - Mediator.Subscribe(this, (msg) => _drawFolders = GetDrawFolders().ToList()); + Mediator.Subscribe(this, (msg) => _drawFolders = DrawFolders.ToList()); Flags |= ImGuiWindowFlags.NoDocking; @@ -612,164 +612,165 @@ public class CompactUi : WindowMediatorSubscriberBase } } - private IEnumerable GetDrawFolders() + private IEnumerable DrawFolders { - List drawFolders = []; - - var allPairs = _pairManager.PairsWithGroups - .ToDictionary(k => k.Key, k => k.Value); - var filteredPairs = allPairs - .Where(p => - { - if (_tabMenu.Filter.IsNullOrEmpty()) return true; - return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) || - (p.Key.GetNote()?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false) || - (p.Key.PlayerName?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false); - }) - .ToDictionary(k => k.Key, k => k.Value); - - string? AlphabeticalSort(KeyValuePair> u) - => (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName) - ? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName) - : (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID)); - bool FilterOnlineOrPausedSelf(KeyValuePair> u) - => (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) - || u.Key.UserPair.OwnPermissions.IsPaused()); - Dictionary> BasicSortedDictionary(IEnumerable>> u) - => u.OrderByDescending(u => u.Key.IsVisible) - .ThenByDescending(u => u.Key.IsOnline) - .ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase) - .ToDictionary(u => u.Key, u => u.Value); - ImmutableList ImmutablePairList(IEnumerable>> u) - => u.Select(k => k.Key).ToImmutableList(); - bool FilterVisibleUsers(KeyValuePair> u) - => u.Key.IsVisible - && (_configService.Current.ShowSyncshellUsersInVisible || !(!_configService.Current.ShowSyncshellUsersInVisible && !u.Key.IsDirectlyPaired)); - bool FilterTagUsers(KeyValuePair> u, string tag) - => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasPairTag(u.Key.UserData.UID, tag); - bool FilterGroupUsers(KeyValuePair> u, GroupFullInfoDto group) - => u.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal)); - bool FilterNotTaggedUsers(KeyValuePair> u) - => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyPairTag(u.Key.UserData.UID); - bool FilterNotTaggedSyncshells(GroupFullInfoDto group) - => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll; - bool FilterOfflineUsers(KeyValuePair> u) - => ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) - || !_configService.Current.ShowSyncshellOfflineUsersSeparately) - && (!u.Key.IsOneSidedPair || u.Value.Any()) && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused(); - bool FilterOfflineSyncshellUsers(KeyValuePair> u) - => (!u.Key.IsDirectlyPaired && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused()); - - - if (_configService.Current.ShowVisibleUsersSeparately) + get { - var allVisiblePairs = ImmutablePairList(allPairs - .Where(FilterVisibleUsers)); - var filteredVisiblePairs = BasicSortedDictionary(filteredPairs - .Where(FilterVisibleUsers)); + var drawFolders = new List(); + var filter = _tabMenu.Filter; - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs)); - } + var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value); + var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value); - List groupFolders = new(); - - foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) - { - GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); - if (FilterNotTaggedSyncshells(group)) + //Filter of online/visible pairs + if (_configService.Current.ShowVisibleUsersSeparately) { - groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)); + var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key))); + var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key))); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs)); } - } - if (_configService.Current.GroupUpSyncshells) - drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, "")); - else - drawFolders.AddRange(groupFolders); - - var tags = _tagHandler.GetAllPairTagsSorted(); - foreach (var tag in tags) - { - var allTagPairs = ImmutablePairList(allPairs - .Where(u => FilterTagUsers(u, tag))); - var filteredTagPairs = BasicSortedDictionary(filteredPairs - .Where(u => FilterTagUsers(u, tag) && FilterOnlineOrPausedSelf(u))); - - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs)); - } - - var syncshellTags = _tagHandler.GetAllSyncshellTagsSorted(); - foreach (var syncshelltag in syncshellTags) - { - List syncshellFolderTags = []; + //Filter of not foldered syncshells + var groupFolders = new List(); foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) { - if (_tagHandler.HasSyncshellTag(group.GID, syncshelltag)) + GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); + + if (FilterNotTaggedSyncshells(group)) { - GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); - syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs)); + groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)); } } - drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshelltag)); - } - var allOnlineNotTaggedPairs = ImmutablePairList(allPairs - .Where(FilterNotTaggedUsers)); - var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs - .Where(u => FilterNotTaggedUsers(u) && FilterOnlineOrPausedSelf(u))); + //Filter of grouped up syncshells (All Syncshells Folder) + if (_configService.Current.GroupUpSyncshells) + drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService, + _selectSyncshellForTagUi, _renameSyncshellTagUi, "")); + else + drawFolders.AddRange(groupFolders); - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), - onlineNotTaggedPairs, allOnlineNotTaggedPairs)); - - if (_configService.Current.ShowOfflineUsersSeparately) - { - var allOfflinePairs = ImmutablePairList(allPairs - .Where(FilterOfflineUsers)); - var filteredOfflinePairs = BasicSortedDictionary(filteredPairs - .Where(FilterOfflineUsers)); - - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs)); - if (_configService.Current.ShowSyncshellOfflineUsersSeparately) + //Filter of grouped/foldered pairs + foreach (var tag in _tagHandler.GetAllPairTagsSorted()) { - var allOfflineSyncshellUsers = ImmutablePairList(allPairs - .Where(FilterOfflineSyncshellUsers)); - var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs - .Where(FilterOfflineSyncshellUsers)); + var allTagPairs = ImmutablePairList(allPairs.Where(p => FilterTagUsers(p.Key, tag))); + var filteredTagPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterTagUsers(p.Key, tag) && FilterOnlineOrPausedSelf(p.Key))); - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, - filteredOfflineSyncshellUsers, - allOfflineSyncshellUsers)); + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs)); } - } - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag, - BasicSortedDictionary(filteredPairs.Where(u => u.Key.IsOneSidedPair)), - ImmutablePairList(allPairs.Where(u => u.Key.IsOneSidedPair)))); - - return drawFolders; - - void GetGroups(Dictionary> allPairs, Dictionary> filteredPairs, GroupFullInfoDto group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs) - { - allGroupPairs = ImmutablePairList(allPairs - .Where(u => FilterGroupUsers(u, group))); - filteredGroupPairs = filteredPairs - .Where(u => FilterGroupUsers(u, group) && FilterOnlineOrPausedSelf(u)) - .OrderByDescending(u => u.Key.IsOnline) - .ThenBy(u => + //Filter of grouped/foldered syncshells + foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted()) + { + var syncshellFolderTags = new List(); + foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) { - if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0; - if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info)) + if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag)) { - if (info.IsModerator()) return 1; - if (info.IsPinned()) return 2; + GetGroups(allPairs, filteredPairs, group, + out ImmutableList allGroupPairs, + out Dictionary> filteredGroupPairs); + + syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs)); } - return u.Key.IsVisible ? 3 : 4; - }) - .ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase) - .ToDictionary(k => k.Key, k => k.Value); + } + + drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag)); + } + + //Filter of offline pairs + var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key))); + var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key))); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs)); + + if (_configService.Current.ShowOfflineUsersSeparately) + { + var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value))); + var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value))); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs)); + + if (_configService.Current.ShowSyncshellOfflineUsersSeparately) + { + var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key))); + var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key))); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers)); + } + } + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag, + BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)), + ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair)))); + + return drawFolders; } } + private static bool PassesFilter(Pair pair, string filter) + { + if (string.IsNullOrEmpty(filter)) return true; + + return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false); + } + + private string AlphabeticalSortKey(Pair pair) + { + if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName)) + { + return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName; + } + + return pair.GetNote() ?? pair.UserData.AliasOrUID; + } + + private bool FilterOnlineOrPausedSelf(Pair pair) => pair.IsOnline || (!pair.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || pair.UserPair.OwnPermissions.IsPaused(); + + private bool FilterVisibleUsers(Pair pair) => pair.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || pair.IsDirectlyPaired); + + private bool FilterTagUsers(Pair pair, string tag) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && _tagHandler.HasPairTag(pair.UserData.UID, tag); + + private static bool FilterGroupUsers(List groups, GroupFullInfoDto group) => groups.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal)); + + private bool FilterNotTaggedUsers(Pair pair) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && !_tagHandler.HasAnyPairTag(pair.UserData.UID); + + private bool FilterNotTaggedSyncshells(GroupFullInfoDto group) => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll; + + private bool FilterOfflineUsers(Pair pair, List groups) => ((pair.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!pair.IsOneSidedPair || groups.Count != 0) && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused(); + + private static bool FilterOfflineSyncshellUsers(Pair pair) => !pair.IsDirectlyPaired && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused(); + + private Dictionary> BasicSortedDictionary(IEnumerable>> pairs) => pairs.OrderByDescending(u => u.Key.IsVisible).ThenByDescending(u => u.Key.IsOnline).ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase).ToDictionary(u => u.Key, u => u.Value); + + private static ImmutableList ImmutablePairList(IEnumerable>> pairs) => [.. pairs.Select(k => k.Key)]; + + private void GetGroups(Dictionary> allPairs, + Dictionary> filteredPairs, + GroupFullInfoDto group, + out ImmutableList allGroupPairs, + out Dictionary> filteredGroupPairs) + { + allGroupPairs = ImmutablePairList(allPairs + .Where(u => FilterGroupUsers(u.Value, group))); + + filteredGroupPairs = filteredPairs + .Where(u => FilterGroupUsers( u.Value, group) && FilterOnlineOrPausedSelf(u.Key)) + .OrderByDescending(u => u.Key.IsOnline) + .ThenBy(u => + { + if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0; + if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info)) + { + if (info.IsModerator()) return 1; + if (info.IsPinned()) return 2; + } + return u.Key.IsVisible ? 3 : 4; + }) + .ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase) + .ToDictionary(k => k.Key, k => k.Value); + } + private string GetServerError() { return _apiController.ServerState switch -- 2.49.1 From 3e15dd643e86106823e460a0b860aaebad824a84 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Fri, 26 Sep 2025 05:17:16 +0200 Subject: [PATCH 21/69] Added more comments --- LightlessSync/UI/CompactUI.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 77305c1..fea81d9 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -678,7 +678,7 @@ public class CompactUi : WindowMediatorSubscriberBase drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag)); } - //Filter of offline pairs + //Filter of not grouped/foldered and offline pairs var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key))); var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key))); @@ -700,6 +700,7 @@ public class CompactUi : WindowMediatorSubscriberBase } } + //Unpaired drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag, BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)), ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair)))); -- 2.49.1 From fd9bd3975b04f7e2fa56ebabd577ea11e567971b Mon Sep 17 00:00:00 2001 From: azyges Date: Sat, 27 Sep 2025 01:38:05 +0900 Subject: [PATCH 22/69] colored uid client support --- LightlessAPI | 2 +- LightlessSync/UI/BroadcastUI.cs | 2 +- LightlessSync/UI/EditProfileUi.cs | 452 ++++++++++++------ LightlessSync/UI/Handlers/IdDisplayHandler.cs | 25 +- LightlessSync/UI/JoinSyncshellUI.cs | 2 +- .../SignalR/ApIController.Functions.Users.cs | 6 + LightlessSync/WebAPI/SignalR/ApiController.cs | 4 + 7 files changed, 329 insertions(+), 164 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 3c10380..5bfd21a 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 3c10380162b162c47c99f63ecfc627a49887fe84 +Subproject commit 5bfd21aaa90817f14c9e2931e77b20f4276f16ed diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 90b19ad..4977fb6 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -131,7 +131,7 @@ namespace LightlessSync.UI ImGuiHelpers.ScaledDummy(0.25f); } - if (ImGui.BeginTabBar("##MyTabBar")) + if (ImGui.BeginTabBar("##BroadcastTabs")) { if (ImGui.BeginTabItem("Lightfinder")) { diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 753c790..6c787b3 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -4,14 +4,17 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data; using LightlessSync.API.Dto.User; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using System.Numerics; namespace LightlessSync.UI; @@ -30,6 +33,15 @@ public class EditProfileUi : WindowMediatorSubscriberBase private bool _showFileDialogError = false; private bool _wasOpen; + private bool vanityInitialized; // useless for now + private bool textEnabled; + private bool glowEnabled; + private Vector4 textColor; + private Vector4 glowColor; + + private record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); + private VanityState _savedVanity; + public EditProfileUi(ILogger logger, LightlessMediator mediator, ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService) @@ -38,8 +50,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase IsOpen = false; this.SizeConstraints = new() { - MinimumSize = new(768, 512), - MaximumSize = new(768, 2000) + MinimumSize = new(850, 640), + MaximumSize = new(850, 700) }; _apiController = apiController; _uiSharedService = uiSharedService; @@ -57,172 +69,314 @@ public class EditProfileUi : WindowMediatorSubscriberBase _pfpTextureWrap = null; } }); + Mediator.Subscribe(this, msg => + { + LoadVanity(); + }); + } + + void DrawNoteLine(string icon, Vector4 color, string text) + { + _uiSharedService.MediumText(icon, color); + ImGui.SameLine(); + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + ImGui.TextWrapped(text); + } + + private void LoadVanity() + { + textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex); + glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex); + + textColor = textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One; + glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; + + _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); + vanityInitialized = true; } protected override void DrawInternal() { - _uiSharedService.BigText("Current Profile (as saved on server)"); + _uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow")); + ImGui.Dummy(new Vector2(5)); + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, 2)); + + DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description."); + DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules."); + DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); + DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); + DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely."); + DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent."); + DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings."); + + ImGui.PopStyleVar(); + + ImGui.Dummy(new Vector2(3)); var profile = _lightlessProfileManager.GetLightlessProfile(new UserData(_apiController.UID)); - if (profile.IsFlagged) - { - UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed); - return; - } - - if (!_profileImage.SequenceEqual(profile.ImageData.Value)) - { - _profileImage = profile.ImageData.Value; - _pfpTextureWrap?.Dispose(); - _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); - } - - if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase)) - { - _profileDescription = profile.Description; - _descriptionText = _profileDescription; - } - - if (_pfpTextureWrap != null) - { - ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); - } - - var spacing = ImGui.GetStyle().ItemSpacing.X; - ImGuiHelpers.ScaledRelativeSameLine(256, spacing); - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f); - var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256); - if (descriptionTextSize.Y > childFrame.Y) + if (ImGui.BeginTabBar("##EditProfileTabs")) + { + if (ImGui.BeginTabItem("Current Profile")) { - _adjustedForScollBarsOnlineProfile = true; - } - else - { - _adjustedForScollBarsOnlineProfile = false; - } - childFrame = childFrame with - { - X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(101, childFrame)) - { - UiSharedService.TextWrapped(profile.Description); - } - ImGui.EndChildFrame(); - } + _uiSharedService.MediumText("Current Profile (as saved on server)", UIColors.Get("LightlessPurple")); + ImGui.Dummy(new Vector2(5)); - var nsfw = profile.IsNSFW; - ImGui.BeginDisabled(); - ImGui.Checkbox("Is NSFW", ref nsfw); - ImGui.EndDisabled(); - - ImGui.Separator(); - _uiSharedService.BigText("Notes and Rules for Profiles"); - - ImGui.TextWrapped($"- All users that are paired and unpaused with you will be able to see your profile picture and description.{Environment.NewLine}" + - $"- Other users have the possibility to report your profile for breaking the rules.{Environment.NewLine}" + - $"- !!! AVOID: anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.){Environment.NewLine}" + - $"- !!! AVOID: slurs of any kind in the description that can be considered highly offensive{Environment.NewLine}" + - $"- In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely.{Environment.NewLine}" + - $"- Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent.{Environment.NewLine}" + - $"- If your profile picture or profile description could be considered NSFW, enable the toggle below."); - ImGui.Separator(); - _uiSharedService.BigText("Profile Settings"); - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) - { - _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => - { - if (!success) return; - _ = Task.Run(async () => + if (profile.IsFlagged) { - var fileContent = File.ReadAllBytes(file); - using MemoryStream ms = new(fileContent); - var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed); + return; + } + + if (!_profileImage.SequenceEqual(profile.ImageData.Value)) + { + _profileImage = profile.ImageData.Value; + _pfpTextureWrap?.Dispose(); + _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); + } + + if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase)) + { + _profileDescription = profile.Description; + _descriptionText = _profileDescription; + } + + if (_pfpTextureWrap != null) + { + ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); + } + + var spacing = ImGui.GetStyle().ItemSpacing.X; + ImGuiHelpers.ScaledRelativeSameLine(256, spacing); + using (_uiSharedService.GameFont.Push()) + { + var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f); + var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256); + if (descriptionTextSize.Y > childFrame.Y) { - _showFileDialogError = true; - return; + _adjustedForScollBarsOnlineProfile = true; } - using var image = Image.Load(fileContent); - - if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024)) + else { - _showFileDialogError = true; - return; + _adjustedForScollBarsOnlineProfile = false; } + childFrame = childFrame with + { + X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0), + }; + if (ImGui.BeginChildFrame(101, childFrame)) + { + UiSharedService.TextWrapped(profile.Description); + } + ImGui.EndChildFrame(); + } - _showFileDialogError = false; - await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null)) - .ConfigureAwait(false); - }); - }); - } - UiSharedService.AttachToolTip("Select and upload a new profile picture"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null)); - } - UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); - if (_showFileDialogError) - { - UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); - } - var isNsfw = profile.IsNSFW; - if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null)); - } - _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); - var widthTextBox = 400; - var posX = ImGui.GetCursorPosX(); - ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); - ImGui.SetCursorPosX(posX); - ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); - ImGui.TextUnformatted("Preview (approximate)"); - using (_uiSharedService.GameFont.Push()) - ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); + var nsfw = profile.IsNSFW; + ImGui.BeginDisabled(); + ImGui.Checkbox("Is NSFW", ref nsfw); + ImGui.EndDisabled(); - ImGui.SameLine(); - - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f); - var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); - if (descriptionTextSizeLocal.Y > childFrameLocal.Y) - { - _adjustedForScollBarsLocalProfile = true; + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.EndTabItem(); } - else - { - _adjustedForScollBarsLocalProfile = false; - } - childFrameLocal = childFrameLocal with - { - X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(102, childFrameLocal)) - { - UiSharedService.TextWrapped(_descriptionText); - } - ImGui.EndChildFrame(); - } - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText)); + if (ImGui.BeginTabItem("Profile Settings")) + { + _uiSharedService.MediumText("Profile Settings", UIColors.Get("LightlessPurple")); + ImGui.Dummy(new Vector2(5)); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) + { + _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => + { + if (!success) return; + _ = Task.Run(async () => + { + var fileContent = File.ReadAllBytes(file); + using MemoryStream ms = new(fileContent); + var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); + if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + { + _showFileDialogError = true; + return; + } + using var image = Image.Load(fileContent); + + if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024)) + { + _showFileDialogError = true; + return; + } + + _showFileDialogError = false; + await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null)) + .ConfigureAwait(false); + }); + }); + } + UiSharedService.AttachToolTip("Select and upload a new profile picture"); + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null)); + } + UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); + if (_showFileDialogError) + { + UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); + } + var isNsfw = profile.IsNSFW; + if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null)); + } + _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); + var widthTextBox = 400; + var posX = ImGui.GetCursorPosX(); + ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); + ImGui.SetCursorPosX(posX); + ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); + ImGui.TextUnformatted("Preview (approximate)"); + using (_uiSharedService.GameFont.Push()) + ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); + + ImGui.SameLine(); + + using (_uiSharedService.GameFont.Push()) + { + var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f); + var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); + if (descriptionTextSizeLocal.Y > childFrameLocal.Y) + { + _adjustedForScollBarsLocalProfile = true; + } + else + { + _adjustedForScollBarsLocalProfile = false; + } + childFrameLocal = childFrameLocal with + { + X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), + }; + if (ImGui.BeginChildFrame(102, childFrameLocal)) + { + UiSharedService.TextWrapped(_descriptionText); + } + ImGui.EndChildFrame(); + } + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText)); + } + UiSharedService.AttachToolTip("Sets your profile description text"); + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "")); + } + UiSharedService.AttachToolTip("Clears your profile description text"); + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Vanity Settings")) + { + _uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple")); + ImGui.Dummy(new Vector2(4)); + DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings."); + + var hasVanity = _apiController.HasVanity; + + if (!hasVanity) + { + UiSharedService.ColorTextWrapped("You do not currently have vanity access. Become a supporter to unlock these features.", UIColors.Get("DimRed")); + ImGui.Dummy(new Vector2(8)); + ImGui.BeginDisabled(); + } + + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + _uiSharedService.MediumText("Colored UID", UIColors.Get("LightlessPurple")); + ImGui.Dummy(new Vector2(5)); + + var font = UiBuilder.MonoFont; + var playerUID = _apiController.UID; + var playerDisplay = _apiController.DisplayName; + + var previewTextColor = textEnabled ? textColor : Vector4.One; + var previewGlowColor = glowEnabled ? glowColor : Vector4.Zero; + + var seString = SeStringUtils.BuildFormattedPlayerName(playerDisplay, previewTextColor, previewGlowColor); + + using (ImRaii.PushFont(font)) + { + var offsetX = 10f; + var pos = ImGui.GetCursorScreenPos() + new Vector2(offsetX, 0); + var size = ImGui.CalcTextSize(seString.TextValue); + + var padding = new Vector2(6, 3); + var rectMin = pos - padding; + var rectMax = pos + size + padding; + + var bgColor = new Vector4(0.15f, 0.15f, 0.15f, 1f); + var borderColor = UIColors.Get("LightlessPurple"); + + var drawList = ImGui.GetWindowDrawList(); + drawList.AddRectFilled(rectMin, rectMax, ImGui.GetColorU32(bgColor), 6.0f); + drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 6.0f, ImDrawFlags.None, 1.5f); + + SeStringUtils.RenderSeStringWithHitbox(seString, pos, font); + } + + const float colorPickAlign = 90f; + + DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color"); + ImGui.SameLine(colorPickAlign); + ImGui.Checkbox("##toggleTextColor", ref textEnabled); + ImGui.SameLine(); + ImGui.BeginDisabled(!textEnabled); + ImGui.ColorEdit4($"##color_text", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); + ImGui.EndDisabled(); + + DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color"); + ImGui.SameLine(colorPickAlign); + ImGui.Checkbox("##toggleGlowColor", ref glowEnabled); + ImGui.SameLine(); + ImGui.BeginDisabled(!glowEnabled); + ImGui.ColorEdit4($"##color_glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); + ImGui.EndDisabled(); + + bool changed = !Equals(_savedVanity, new VanityState(textEnabled, glowEnabled, textColor, glowColor)); + + if (!changed) + ImGui.BeginDisabled(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Changes")) + { + string? newText = textEnabled ? UIColors.RgbaToHex(textColor) : string.Empty; + string? newGlow = glowEnabled ? UIColors.RgbaToHex(glowColor) : string.Empty; + + _ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow)); + + _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); + } + + if (!changed) + ImGui.EndDisabled(); + + ImGui.Dummy(new Vector2(5)); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + + if (!hasVanity) + ImGui.EndDisabled(); + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); } - UiSharedService.AttachToolTip("Sets your profile description text"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "")); - } - UiSharedService.AttachToolTip("Clears your profile description text"); } protected override void Dispose(bool disposing) diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 81ed337..048efe9 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -98,20 +98,21 @@ public class IdDisplayHandler var font = UiBuilder.MonoFont; - var isAdmin = pair.UserData.IsAdmin; - var isModerator = pair.UserData.IsModerator; + Vector4? textColor = null; + Vector4? glowColor = null; - Vector4? textColor = isAdmin - ? UIColors.Get("LightlessAdminText") - : isModerator - ? UIColors.Get("LightlessModeratorText") - : null; + if (pair.UserData.HasVanity) + { + if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex)) + { + textColor = UIColors.HexToRgba(pair.UserData.TextColorHex); + } - Vector4? glowColor = isAdmin - ? UIColors.Get("LightlessAdminGlow") - : isModerator - ? UIColors.Get("LightlessModeratorGlow") - : null; + if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex)) + { + glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex); + } + } var seString = (textColor != null || glowColor != null) ? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor) diff --git a/LightlessSync/UI/JoinSyncshellUI.cs b/LightlessSync/UI/JoinSyncshellUI.cs index 3de9026..b02a84e 100644 --- a/LightlessSync/UI/JoinSyncshellUI.cs +++ b/LightlessSync/UI/JoinSyncshellUI.cs @@ -63,7 +63,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase "Joining a Syncshell will pair you implicitly with all existing users in the Syncshell." + Environment.NewLine + "All permissions to all users in the Syncshell will be set to the preferred Syncshell permissions on joining, excluding prior set preferred permissions."); ImGui.Separator(); - ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. MSS- is part of Syncshell IDs, unless using Vanity IDs."); + ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. LLS- is part of Syncshell IDs, unless using Vanity IDs."); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Syncshell ID"); diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index 95f1de8..d268dd8 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -134,6 +134,12 @@ public partial class ApiController await _lightlessHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false); } + public async Task UserUpdateVanityColors(UserVanityColorsDto dto) + { + if (!IsConnected) return; + await _lightlessHub!.InvokeAsync(nameof(UserUpdateVanityColors), dto).ConfigureAwait(false); + } + public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto) { CheckConnection(); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 4a4071b..efa6e6f 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -77,6 +77,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null; public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty; + public bool HasVanity => _connectionDto?.HasVanity ?? false; + public string TextColorHex => _connectionDto?.TextColorHex ?? string.Empty; + public string TextGlowColorHex => _connectionDto?.TextGlowColorHex ?? string.Empty; + public bool IsConnected => ServerState == ServerState.Connected; public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0)); -- 2.49.1 From c6f8d6843e77fd6e39c14cc1302e1c948f960a9f Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Fri, 26 Sep 2025 18:39:38 +0200 Subject: [PATCH 23/69] Disabled button and added tooltip for already joined/owned syncshells in finder --- LightlessSync/UI/SyncshellFinderUI.cs | 86 ++++++++++++++------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index e009bb5..81f0b0e 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -26,6 +26,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private readonly PairManager _pairManager; private readonly List _nearbySyncshells = []; + private List _currentSyncshells = []; private int _selectedNearbyIndex = -1; private GroupJoinDto? _joinDto; @@ -106,10 +107,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); ImGui.TableHeadersRow(); - for (int i = 0; i < _nearbySyncshells.Count; i++) + foreach (var shell in _nearbySyncshells) { - var shell = _nearbySyncshells[i]; - ImGui.TableNextRow(); ImGui.TableNextColumn(); ImGui.TextUnformatted(shell.Group.Alias ?? "(No Alias)"); @@ -122,41 +121,53 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); - if (ImGui.Button(label)) + if (!_currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal))) { - _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); - _ = Task.Run(async () => + + if (ImGui.Button(label)) { - try - { - var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( - shell.Group, - shell.Password, - shell.GroupUserPreferredPermissions - )).ConfigureAwait(false); + _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); - 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) + _ = Task.Run(async () => { - _logger.LogError(ex, $"Join failed for {shell.Group.GID}"); - } - }); + 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); } @@ -226,7 +237,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private async Task RefreshSyncshellsAsync() { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); - var currentSyncshells = _pairManager.GroupPairs.Select(g => g.Key).ToList(); + _currentSyncshells = _pairManager.GroupPairs.Select(g => g.Key).ToList(); if (syncshellBroadcasts.Count == 0) { @@ -238,16 +249,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase try { var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); - if (groups != null && currentSyncshells != null) - { - foreach (var group in groups) - { - if (!currentSyncshells.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal))) - { - updatedList = groups?.ToList(); - } - } - } + updatedList = groups?.ToList(); } catch (Exception ex) { -- 2.49.1 From 7c4269b0117e2654718af49b0f14b9c4d406ad78 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 27 Sep 2025 21:58:24 -0500 Subject: [PATCH 24/69] Testing dev release workflow --- .../workflows/lightless-tag-and-release.yml | 101 +++++++++++++++++- 1 file changed, 96 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/lightless-tag-and-release.yml b/.gitea/workflows/lightless-tag-and-release.yml index 63b4fb0..adf40b3 100644 --- a/.gitea/workflows/lightless-tag-and-release.yml +++ b/.gitea/workflows/lightless-tag-and-release.yml @@ -2,7 +2,7 @@ name: Tag and Release Lightless on: push: - branches: [ master ] + branches: [ master, dev ] env: PLUGIN_NAME: LightlessSync @@ -33,6 +33,11 @@ jobs: curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip unzip latest.zip -d /root/.xlcore/dalamud/Hooks/dev + - name: Checkout Dev branch (only for dev) + if: github.ref == 'refs/heads/dev' + run: | + git checkout dev + - name: Lets Build Lightless! run: | dotnet restore @@ -62,7 +67,8 @@ jobs: mkdir -p output (cd /workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/ && zip -r $OLDPWD/output/LightlessClient.zip *) - - name: Create Git tag if not exists + - name: Create Git tag if not exists (master) + if: github.ref == 'refs/heads/master' run: | tag="${{ steps.package_version.outputs.version }}" git fetch --tags @@ -76,7 +82,23 @@ jobs: echo "Tag $tag already exists. Skipping tag creation." fi - - name: Create Release + - name: Create Git tag if not exists (dev) + if: github.ref == 'refs/heads/dev' + run: | + tag="${{ steps.package_version.outputs.version }}-Dev" + git fetch --tags + if ! git tag -l "$tag" | grep -q "$tag"; then + echo "Tag $tag does not exist. Creating and pushing..." + git config user.name "GitHub Action" + git config user.email "action@github.com" + git tag "$tag" + git push origin "$tag" + else + echo "Tag $tag already exists. Skipping tag creation." + fi + + - name: Create Release (master) + if: github.ref == 'refs/heads/master' id: create_release run: | echo "=== Searching for existing release ${{ steps.package_version.outputs.version }}===" @@ -107,6 +129,35 @@ jobs: release_id=$(echo "$response" | jq -r .id) echo "release_id=$release_id" >> "$GITHUB_OUTPUT" + - name: Create Release (dev) + if: github.ref == 'refs/heads/dev' + id: create_release + run: | + version="${{ steps.package_version.outputs.version }}-Dev" + echo "=== Searching for existing release $version===" + release_id=$(curl -s -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + "https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/$version" | jq -r .id) + if [ "$release_id" != "null" ]; then + echo "=== Deleting existing release $version===" + curl -X DELETE -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + "https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$release_id" + fi + echo "=== Creating new release $version===" + response=$( + curl --fail-with-body -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + -d '{ + "tag_name": "'"$version"'", + "name": "'"$version"'", + "draft": false, + "prerelease": false + }' \ + "https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases" + ) + release_id=$(echo "$response" | jq -r .id) + echo "release_id=$release_id" >> "$GITHUB_OUTPUT" + - name: Upload Assets to release run: | curl --fail-with-body -s -X POST \ @@ -122,7 +173,8 @@ jobs: env: GIT_TERMINAL_PROMPT: 0 - - name: Update plogonmaster.json with version + - name: Update plogonmaster.json with version (master) + if: github.ref == 'refs/heads/master' env: VERSION: ${{ steps.package_version.outputs.version }} run: | @@ -159,7 +211,6 @@ jobs: .DalamudApiLevel = $dalamudApiLevel | .AssemblyVersion = $version | .DownloadLinkInstall = $downloadUrl - | .DownloadLinkTesting = $downloadUrl | .DownloadLinkUpdate = $downloadUrl else . @@ -172,6 +223,46 @@ jobs: # Output the content of the file cat "$repoJsonPath" + - name: Update plogonmaster.json with version (dev) + if: github.ref == 'refs/heads/dev' + env: + VERSION: ${{ steps.package_version.outputs.version }} + run: | + set -e + pluginJsonPath="${PLUGIN_NAME}/bin/x64/Release/${PLUGIN_NAME}.json" + repoJsonPath="LightlessSyncRepo/LightlessSync/plogonmaster.json" + assemblyVersion="${VERSION}" + version="${VERSION}-Dev" + downloadUrl="https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip" + pluginJson=$(cat "$pluginJsonPath") + internalName=$(jq -r '.InternalName' <<< "$pluginJson") + dalamudApiLevel=$(jq -r '.DalamudApiLevel' <<< "$pluginJson") + repoJsonRaw=$(cat "$repoJsonPath") + if echo "$repoJsonRaw" | jq 'type' | grep -q '"array"'; then + repoJson="$repoJsonRaw" + else + repoJson="[$repoJsonRaw]" + fi + updatedRepoJson=$(jq \ + --arg internalName "$internalName" \ + --arg dalamudApiLevel "$dalamudApiLevel" \ + --arg version "$version" \ + --arg downloadUrl "$downloadUrl" \ + ' + map( + if .InternalName == $internalName + then + .DalamudApiLevel = $dalamudApiLevel + | .TestingAssemblyVersion = $assemblyVersion + | .DownloadLinkTesting = $downloadUrl + else + . + end + ) + ' <<< "$repoJson") + echo "$updatedRepoJson" > "$repoJsonPath" + cat "$repoJsonPath" + - name: Commit and push to LightlessSync run: | cd LightlessSyncRepo/LightlessSync -- 2.49.1 From 73f130a95a3f6ea95b74d231257edf0c57a7e08b Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 27 Sep 2025 22:05:33 -0500 Subject: [PATCH 25/69] remove redundant checkout --- .gitea/workflows/lightless-tag-and-release.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitea/workflows/lightless-tag-and-release.yml b/.gitea/workflows/lightless-tag-and-release.yml index adf40b3..d92764f 100644 --- a/.gitea/workflows/lightless-tag-and-release.yml +++ b/.gitea/workflows/lightless-tag-and-release.yml @@ -33,11 +33,6 @@ jobs: curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip unzip latest.zip -d /root/.xlcore/dalamud/Hooks/dev - - name: Checkout Dev branch (only for dev) - if: github.ref == 'refs/heads/dev' - run: | - git checkout dev - - name: Lets Build Lightless! run: | dotnet restore -- 2.49.1 From 3831dd24f1be5bda865c65ea669ea14e7db0f55c Mon Sep 17 00:00:00 2001 From: choco Date: Sun, 28 Sep 2025 15:16:45 +0200 Subject: [PATCH 26/69] settings cleanup readded title settings --- LightlessSync/UI/CompactUI.cs | 15 +++++++++++++++ LightlessSync/UI/TopTabMenu.cs | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index a14be42..247e440 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -109,6 +109,21 @@ public class CompactUi : WindowMediatorSubscriberBase AllowClickthrough = false; TitleBarButtons = new() { + new TitleBarButton() + { + Icon = FontAwesomeIcon.Cog, + Click = (msg) => + { + Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + }, + IconOffset = new(2,1), + ShowTooltip = () => + { + ImGui.BeginTooltip(); + ImGui.Text("Open Lightless Settings"); + ImGui.EndTooltip(); + } + }, new TitleBarButton() { Icon = FontAwesomeIcon.Book, diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 9aa112c..339b22c 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -154,7 +154,7 @@ public class TopTabMenu } } - UiSharedService.AttachToolTip("Lightless Sync Settings"); + UiSharedService.AttachToolTip("Open Lightless Settings"); ImGui.NewLine(); btncolor.Dispose(); -- 2.49.1 From 08e3c8678f6b252480f22b4f337b72818f4dd0a7 Mon Sep 17 00:00:00 2001 From: choco Date: Sun, 28 Sep 2025 15:49:15 +0200 Subject: [PATCH 27/69] added broadcaster name display in Syncshell finder table --- LightlessSync/Plugin.cs | 4 ++-- LightlessSync/UI/SyncshellFinderUI.cs | 29 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 09f218e..3529735 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; @@ -230,7 +230,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped((s) => new BroadcastUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 81f0b0e..32be263 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -24,6 +24,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private readonly UiSharedService _uiSharedService; private readonly BroadcastScannerService _broadcastScannerService; private readonly PairManager _pairManager; + private readonly DalamudUtilService _dalamudUtilService; private readonly List _nearbySyncshells = []; private List _currentSyncshells = []; @@ -41,13 +42,15 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase UiSharedService uiShared, ApiController apiController, BroadcastScannerService broadcastScannerService, - PairManager pairManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) + 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() @@ -102,8 +105,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) { - ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("GID", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); ImGui.TableHeadersRow(); @@ -111,9 +114,21 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase { ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.TextUnformatted(shell.Group.Alias ?? "(No Alias)"); + + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; + ImGui.TextUnformatted(displayName); + ImGui.TableNextColumn(); - ImGui.TextUnformatted(shell.Group.GID); + var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); + var broadcast = broadcasts.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); + var broadcasterName = "Unknown"; + if (broadcast != null) + { + var playerInfo = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); + broadcasterName = !string.IsNullOrEmpty(playerInfo.Name) ? playerInfo.Name : "Unknown Player"; + } + ImGui.TextUnformatted(broadcasterName); + ImGui.TableNextColumn(); var label = $"Join##{shell.Group.GID}"; @@ -123,8 +138,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase if (!_currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal))) { - - if (ImGui.Button(label)) { _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); -- 2.49.1 From 914553d5ab3d3208ee4a55f7d35060a6da46f972 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sun, 28 Sep 2025 10:11:13 -0500 Subject: [PATCH 28/69] plogon missing var --- .gitea/workflows/lightless-tag-and-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/lightless-tag-and-release.yml b/.gitea/workflows/lightless-tag-and-release.yml index d92764f..3401e21 100644 --- a/.gitea/workflows/lightless-tag-and-release.yml +++ b/.gitea/workflows/lightless-tag-and-release.yml @@ -241,6 +241,7 @@ jobs: updatedRepoJson=$(jq \ --arg internalName "$internalName" \ --arg dalamudApiLevel "$dalamudApiLevel" \ + --arg assemblyVersion "$assemblyVersion" \ --arg version "$version" \ --arg downloadUrl "$downloadUrl" \ ' -- 2.49.1 From 0cc7181e98552f5e577f1d1288c8bd24a1bc2f43 Mon Sep 17 00:00:00 2001 From: choco Date: Sun, 28 Sep 2025 19:41:19 +0200 Subject: [PATCH 29/69] added world name to syncshell broadcaster info --- LightlessSync/Services/DalamudUtilService.cs | 15 +++++++++++++-- LightlessSync/UI/SyncshellFinderUI.cs | 12 +++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index ed66506..e5fd735 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; @@ -541,7 +541,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber curWaitTime += tick; Thread.Sleep(tick); } - Thread.Sleep(tick * 2); } @@ -557,6 +556,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber return result; } + public string? GetWorldNameFromPlayerAddress(nint address) + { + if (address == nint.Zero) return null; + + EnsureIsOnFramework(); + var playerCharacter = _objectTable.OfType().FirstOrDefault(p => p.Address == address); + if (playerCharacter == null) return null; + + var worldId = (ushort)playerCharacter.HomeWorld.RowId; + return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null; + } + private unsafe void CheckCharacterForDrawing(nint address, string characterName) { var gameObj = (GameObject*)address; diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 32be263..4af72d5 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -119,13 +119,18 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.TextUnformatted(displayName); ImGui.TableNextColumn(); - var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); - var broadcast = broadcasts.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); var broadcasterName = "Unknown"; + var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts() + .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); + if (broadcast != null) { var playerInfo = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); - broadcasterName = !string.IsNullOrEmpty(playerInfo.Name) ? playerInfo.Name : "Unknown Player"; + if (!string.IsNullOrEmpty(playerInfo.Name)) + { + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(playerInfo.Address); + broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{playerInfo.Name} ({worldName})" : playerInfo.Name; + } } ImGui.TextUnformatted(broadcasterName); @@ -317,4 +322,5 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return _nearbySyncshells[_selectedNearbyIndex].Group.GID; } + } -- 2.49.1 From 7b415b4e478b56d38b3a7685969980b74e1dcbe6 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Mon, 29 Sep 2025 18:37:02 +0200 Subject: [PATCH 30/69] Changed it to so debug shows while in debug mode. --- LightlessSync/UI/BroadcastUI.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 4977fb6..2076f6c 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -309,7 +309,7 @@ namespace LightlessSync.UI ImGui.EndTabItem(); } - +#if DEBUG if (ImGui.BeginTabItem("Debug")) { ImGui.Text("Broadcast Cache"); @@ -365,7 +365,7 @@ namespace LightlessSync.UI ImGui.EndTable(); } - +#endif ImGui.EndTabItem(); } -- 2.49.1 From f74cd01a3290ff5de3821d6af4345276ad527fc5 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Mon, 29 Sep 2025 19:44:36 +0200 Subject: [PATCH 31/69] Pair request button only shows when lightfinder is on --- LightlessSync/Plugin.cs | 2 +- LightlessSync/UI/ContextMenu.cs | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 3529735..24f1745 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -147,7 +147,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle); - collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable)); + collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, p.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService>(), pluginInterface, diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index 1e1cfc3..4a4ed62 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -3,6 +3,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; @@ -19,6 +20,7 @@ internal class ContextMenu : IHostedService private readonly IDataManager _gameData; private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; + private readonly LightlessConfigService _configService; private readonly ApiController _apiController; private readonly IObjectTable _objectTable; @@ -37,7 +39,8 @@ internal class ContextMenu : IHostedService ILogger logger, DalamudUtilService dalamudUtil, ApiController apiController, - IObjectTable objectTable) + IObjectTable objectTable, + LightlessConfigService configService) { _contextMenu = contextMenu; _pluginInterface = pluginInterface; @@ -46,6 +49,7 @@ internal class ContextMenu : IHostedService _dalamudUtil = dalamudUtil; _apiController = apiController; _objectTable = objectTable; + _configService = configService; } public Task StartAsync(CancellationToken cancellationToken) @@ -90,14 +94,17 @@ internal class ContextMenu : IHostedService if (!IsWorldValid(world)) return; - args.AddMenuItem(new MenuItem + if (_configService.Current.BroadcastEnabled) { - Name = "Send Pair Request", - PrefixChar = 'L', - UseDefaultPrefix = false, - PrefixColor = 708, - OnClicked = async _ => await HandleSelection(args) - }); + args.AddMenuItem(new MenuItem + { + Name = "Send Pair Request", + PrefixChar = 'L', + UseDefaultPrefix = false, + PrefixColor = 708, + OnClicked = async _ => await HandleSelection(args) + }); + } } private async Task HandleSelection(IMenuArgs args) -- 2.49.1 From dc1fdd4a3e0816818f98fd894a18a633d80b618e Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Mon, 29 Sep 2025 19:52:19 +0200 Subject: [PATCH 32/69] Removed the lightfinder needs to be turned on. --- LightlessSync/UI/ContextMenu.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index 4a4ed62..e34a694 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -94,17 +94,14 @@ internal class ContextMenu : IHostedService if (!IsWorldValid(world)) return; - if (_configService.Current.BroadcastEnabled) + args.AddMenuItem(new MenuItem { - args.AddMenuItem(new MenuItem - { - Name = "Send Pair Request", - PrefixChar = 'L', - UseDefaultPrefix = false, - PrefixColor = 708, - OnClicked = async _ => await HandleSelection(args) - }); - } + Name = "Send Pair Request", + PrefixChar = 'L', + UseDefaultPrefix = false, + PrefixColor = 708, + OnClicked = async _ => await HandleSelection(args) + }); } private async Task HandleSelection(IMenuArgs args) -- 2.49.1 From 7d6d500e6a7436837980918dc3d553557ac8e54d Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Mon, 29 Sep 2025 22:38:38 +0200 Subject: [PATCH 33/69] Changes from Bites to Bytes. --- LightlessSync/UI/ContextMenu.cs | 5 ++--- LightlessSync/UI/UISharedService.cs | 10 ++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index e34a694..5a8b291 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -1,5 +1,4 @@ using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin; using Dalamud.Plugin.Services; @@ -81,7 +80,7 @@ internal class ContextMenu : IHostedService if (!_pluginInterface.UiBuilder.ShouldModifyUi) return; - if (!ValidAddons.Contains(args.AddonName)) + if (!ValidAddons.Contains(args.AddonName, StringComparer.Ordinal)) return; if (args.Target is not MenuTargetDefault target) @@ -127,7 +126,7 @@ internal class ContextMenu : IHostedService return; } - var senderCid = (await _dalamudUtil.GetCIDAsync()).ToString().GetHash256(); + var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index e4753dc..7fdf97f 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -173,12 +173,14 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public static string ByteToString(long bytes, bool addSuffix = true) { - string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"]; - int i; + string[] suffix = { "B", "KB", "MB", "GB", "TB" }; + int i = 0; double dblSByte = bytes; - for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) + + while (dblSByte >= 1000 && i < suffix.Length - 1) { - dblSByte = bytes / 1024.0; + dblSByte /= 1000.0; + i++; } return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; -- 2.49.1 From 8a9d4b8daa81f6c7a432662273ace589e0550978 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Mon, 29 Sep 2025 23:19:02 +0200 Subject: [PATCH 34/69] Added more byte options --- LightlessSync/UI/UISharedService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 7fdf97f..9a935ba 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -173,7 +173,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public static string ByteToString(long bytes, bool addSuffix = true) { - string[] suffix = { "B", "KB", "MB", "GB", "TB" }; + string[] suffix = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; int i = 0; double dblSByte = bytes; -- 2.49.1 From 0b7b543dd7e07439a7935b7c2c90a28e59930693 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Mon, 29 Sep 2025 23:56:11 +0200 Subject: [PATCH 35/69] Add Group check, reworked world check for instances. --- LightlessSync/UI/BroadcastUI.cs | 21 ++++++++------------- LightlessSync/UI/ContextMenu.cs | 24 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 2076f6c..6d50184 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -22,7 +22,7 @@ namespace LightlessSync.UI private IReadOnlyList _allSyncshells; private string _userUid = string.Empty; - private List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); + private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); public BroadcastUI( ILogger logger, @@ -48,7 +48,7 @@ namespace LightlessSync.UI MaximumSize = new(750, 400) }; - mediator.Subscribe(this, async _ => await RefreshSyncshells()); + mediator.Subscribe(this, async _ => await RefreshSyncshells().ConfigureAwait(false)); } private void RebuildSyncshellDropdownOptions() @@ -62,7 +62,7 @@ namespace LightlessSync.UI _syncshellOptions.Clear(); _syncshellOptions.Add(("None", null, true)); - var addedGids = new HashSet(); + var addedGids = new HashSet(StringComparer.Ordinal); foreach (var shell in ownedSyncshells) { @@ -73,7 +73,7 @@ namespace LightlessSync.UI if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid)) { - var matching = allSyncshells.FirstOrDefault(g => g.GID == selectedGid); + var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal)); if (matching != null) { var label = matching.GroupAliasOrGID ?? matching.GID; @@ -97,7 +97,7 @@ namespace LightlessSync.UI { if (!_apiController.IsConnected) { - _allSyncshells = Array.Empty(); + _allSyncshells = []; RebuildSyncshellDropdownOptions(); return; } @@ -109,7 +109,7 @@ namespace LightlessSync.UI catch (Exception ex) { _logger.LogError(ex, "Failed to fetch Syncshells."); - _allSyncshells = Array.Empty(); + _allSyncshells = []; } RebuildSyncshellDropdownOptions(); @@ -260,14 +260,14 @@ namespace LightlessSync.UI } var selectedGid = _configService.Current.SelectedFinderSyncshell; - var currentOption = _syncshellOptions.FirstOrDefault(o => o.GID == selectedGid); + var currentOption = _syncshellOptions.FirstOrDefault(o => string.Equals(o.GID, selectedGid, StringComparison.Ordinal)); var preview = currentOption.Label ?? "Select a Syncshell..."; if (ImGui.BeginCombo("##SyncshellDropdown", preview)) { foreach (var (label, gid, available) in _syncshellOptions) { - bool isSelected = gid == selectedGid; + bool isSelected = string.Equals(gid, selectedGid, StringComparison.Ordinal); if (!available) ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); @@ -373,10 +373,5 @@ namespace LightlessSync.UI ImGui.EndTabBar(); } } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - } } } diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index 5a8b291..80bbb0c 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -9,6 +9,7 @@ using LightlessSync.WebAPI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using System.Linq; namespace LightlessSync.UI; @@ -141,8 +142,29 @@ internal class ContextMenu : IHostedService private World GetWorld(uint worldId) { var sheet = _gameData.GetExcelSheet()!; - return sheet.TryGetRow(worldId, out var world) ? world : sheet.First(); + var luminaWorlds = sheet.Where(x => + { + var dc = x.DataCenter.ValueNullable; + var name = x.Name.ExtractText(); + var internalName = x.InternalName.ExtractText(); + + if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText())) + return false; + + if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName)) + return false; + + if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal)) + return false; + + return x.RowId > 3001 && IsChineseJapaneseKoreanString(name); + }); + + return luminaWorlds.FirstOrDefault(x => x.RowId == worldId); } + private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); + + private static bool IsChineseJapaneseKoreanCharacter(char c) => (c >= 0x4E00 && c <= 0x9FFF); public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId)); -- 2.49.1 From 9696ac60f1fe8ac65863263a48ab1cf90d3be25b Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Tue, 30 Sep 2025 00:00:11 +0200 Subject: [PATCH 36/69] Added region bound. --- LightlessSync/UI/ContextMenu.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index 80bbb0c..19b5955 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -157,9 +157,15 @@ internal class ContextMenu : IHostedService if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal)) return false; - return x.RowId > 3001 && IsChineseJapaneseKoreanString(name); + return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name); }); + foreach (var world in luminaWorlds) + { + _logger.LogInformation(message: $"ID: {world.RowId} - World: {world.Name.ExtractText()}"); + } + _logger.LogInformation(message: $"Character is in World: {worldId}"); + return luminaWorlds.FirstOrDefault(x => x.RowId == worldId); } private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); -- 2.49.1 From e85b9007546dbbf4c715daa28b97cfcd71b0f875 Mon Sep 17 00:00:00 2001 From: azyges Date: Tue, 30 Sep 2025 11:17:55 +0900 Subject: [PATCH 37/69] some ui updates --- LightlessSync/UI/BroadcastUI.cs | 5 +- LightlessSync/UI/DataAnalysisUi.cs | 207 +++++++++++++++++----------- LightlessSync/UI/SettingsUi.cs | 17 +++ LightlessSync/UI/UISharedService.cs | 15 ++ 4 files changed, 158 insertions(+), 86 deletions(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 6d50184..eda5a53 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -309,7 +309,8 @@ namespace LightlessSync.UI ImGui.EndTabItem(); } -#if DEBUG + + #if DEBUG if (ImGui.BeginTabItem("Debug")) { ImGui.Text("Broadcast Cache"); @@ -365,10 +366,10 @@ namespace LightlessSync.UI ImGui.EndTable(); } -#endif ImGui.EndTabItem(); } + #endif ImGui.EndTabBar(); } diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index e1ef15c..5b750f3 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -547,73 +547,147 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString()); if (tab.Success) { - var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal) - .OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); + var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); - ImGui.TextUnformatted("Files for " + kvp.Key); - ImGui.SameLine(); - ImGui.TextUnformatted(kvp.Value.Count.ToString()); - ImGui.SameLine(); + ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(1f, 1f)); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f)); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + if (ImGui.BeginTable($"##fileStats_{kvp.Key}", 3, + ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit)) { - ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); - } - if (ImGui.IsItemHovered()) - { - string text = ""; - text = string.Join(Environment.NewLine, groupedfiles - .Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)) - + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); - ImGui.SetTooltip(text); - } - ImGui.TextUnformatted($"{kvp.Key} size (actual):"); - ImGui.SameLine(); - ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); - ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):"); - ImGui.SameLine(); - ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); - ImGui.Separator(); - - var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); - if (vramUsage != null) - { - var actualVramUsage = vramUsage.Sum(f => f.OriginalSize); - ImGui.TextUnformatted($"{kvp.Key} VRAM usage:"); + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"Files for {kvp.Key}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(kvp.Value.Count.ToString()); ImGui.SameLine(); - ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage)); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); + if (ImGui.IsItemHovered()) + { + string text = string.Join(Environment.NewLine, groupedfiles.Select(f => + $"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}")); + ImGui.SetTooltip(text); + } + ImGui.TableNextColumn(); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{kvp.Key} size (actual):"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); + ImGui.TableNextColumn(); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):"); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + ImGui.TableNextColumn(); + + var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); + if (vramUsage != null) + { + var actualVramUsage = vramUsage.Sum(f => f.OriginalSize); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{kvp.Key} VRAM usage:"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage)); + ImGui.TableNextColumn(); + + if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds + || _playerPerformanceConfig.Current.ShowPerformanceIndicator) + { + var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB; + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Configured VRAM threshold:"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{currentVramWarning} MiB."); + ImGui.TableNextColumn(); + if (currentVramWarning * 1024 * 1024 < actualVramUsage) + { + UiSharedService.ColorText( + $"You exceed your own threshold by {UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}", + UIColors.Get("LightlessYellow")); + } + } + } + + var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles); + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{kvp.Key} modded model triangles:"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualTriCount.ToString()); + ImGui.TableNextColumn(); + if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds || _playerPerformanceConfig.Current.ShowPerformanceIndicator) { - using var _ = ImRaii.PushIndent(10f); - var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB; - ImGui.TextUnformatted($"Configured VRAM warning threshold: {currentVramWarning} MiB."); - if (currentVramWarning * 1024 * 1024 < actualVramUsage) + var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands; + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Configured triangle threshold:"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{currentTriWarning * 1000} triangles."); + ImGui.TableNextColumn(); + if (currentTriWarning * 1000 < actualTriCount) { - UiSharedService.ColorText($"You exceed your own threshold by " + - $"{UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}.", + UiSharedService.ColorText( + $"You exceed your own threshold by {actualTriCount - (currentTriWarning * 1000)}", UIColors.Get("LightlessYellow")); } } + + ImGui.EndTable(); } - var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles); - ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {actualTriCount}"); - if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds - || _playerPerformanceConfig.Current.ShowPerformanceIndicator) + ImGui.PopStyleVar(2); + + _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + + _uiSharedService.MediumText("Selected file:", UIColors.Get("LightlessBlue")); + ImGui.SameLine(); + _uiSharedService.MediumText(_selectedHash, UIColors.Get("LightlessYellow")); + + if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) { - using var _ = ImRaii.PushIndent(10f); - var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands; - ImGui.TextUnformatted($"Configured triangle warning threshold: {currentTriWarning * 1000} triangles."); - if (currentTriWarning * 1000 < actualTriCount) + var filePaths = item.FilePaths; + UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue")); + ImGui.SameLine(); + UiSharedService.TextWrapped(filePaths[0]); + if (filePaths.Count > 1) { - UiSharedService.ColorText($"You exceed your own threshold by " + - $"{actualTriCount - (currentTriWarning * 1000)} triangles.", - UIColors.Get("LightlessYellow")); + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1))); + } + + var gamepaths = item.GamePaths; + UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue")); + ImGui.SameLine(); + UiSharedService.TextWrapped(gamepaths[0]); + if (gamepaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1))); } } ImGui.Separator(); + if (_selectedObjectTab != kvp.Key) { _selectedHash = string.Empty; @@ -692,41 +766,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } } - - ImGui.Separator(); - - ImGui.TextUnformatted("Selected file:"); - ImGui.SameLine(); - UiSharedService.ColorText(_selectedHash, UIColors.Get("LightlessYellow")); - - if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) - { - var filePaths = item.FilePaths; - ImGui.TextUnformatted("Local file path:"); - ImGui.SameLine(); - UiSharedService.TextWrapped(filePaths[0]); - if (filePaths.Count > 1) - { - ImGui.SameLine(); - ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)"); - ImGui.SameLine(); - _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); - UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1))); - } - - var gamepaths = item.GamePaths; - ImGui.TextUnformatted("Used by game path:"); - ImGui.SameLine(); - UiSharedService.TextWrapped(gamepaths[0]); - if (gamepaths.Count > 1) - { - ImGui.SameLine(); - ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)"); - ImGui.SameLine(); - _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); - UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1))); - } - } } public override void OnOpen() @@ -855,7 +894,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } var filePath = item.FilePaths[0]; bool toConvert = _texturesToConvert.ContainsKey(filePath); - if (ImGui.Checkbox("###convert" + item.Hash, ref toConvert)) + if (UiSharedService.CheckboxWithBorder("###convert" + item.Hash, ref toConvert, UIColors.Get("LightlessPurple"), 1.5f)) { if (toConvert && !_texturesToConvert.ContainsKey(filePath)) { diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index fdbe821..95bb2af 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -979,9 +979,17 @@ public class SettingsUi : WindowMediatorSubscriberBase var colorNames = new[] { ("LightlessPurple", "Lightless Purple", "Primary colors"), + ("LightlessPurpleActive", "Lightless Purple Active", "Primary colors"), + ("LightlessPurpleDefault", "Lightless Purple Inactive", "Primary colors"), ("LightlessBlue", "Lightless Blue", "Secondary colors"), + + ("LightlessGreen", "Lightless Green", "Active elements"), + ("LightlessYellow", "Lightless Yellow", "Warning colors"), + ("LightlessYellow2", "Lightless Yellow 2", "Warning colors"), + ("PairBlue", "Pair Blue", "Pair UI elements"), + ("DimRed", "Dim Red", "Error and offline") }; @@ -1020,6 +1028,8 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + ImGui.TextUnformatted("Server Info Bar Colors"); if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) @@ -1054,6 +1064,9 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.Spacing(); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + ImGui.TextUnformatted("Nameplate Colors"); var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled; @@ -1092,6 +1105,10 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + ImGui.Spacing(); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + if (ImGui.Checkbox("Use the complete redesign of the UI for Lightless client.", ref useLightlessRedesign)) { _configService.Current.UseLightlessRedesign = useLightlessRedesign; diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 9a935ba..24899ee 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -512,6 +512,21 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Dummy(new Vector2(0, thickness * scale)); } + public static bool CheckboxWithBorder(string label, ref bool value, Vector4? borderColor = null, float borderThickness = 1.0f, float rounding = 3.0f) + { + var pos = ImGui.GetCursorScreenPos(); + + bool changed = ImGui.Checkbox(label, ref value); + + var min = pos; + var max = ImGui.GetItemRectMax(); + + var col = ImGui.GetColorU32(borderColor ?? ImGuiColors.DalamudGrey); + ImGui.GetWindowDrawList().AddRect(min, max, col, rounding, ImDrawFlags.None, borderThickness); + + return changed; + } + public void MediumText(string text, Vector4? color = null) { FontText(text, MediumFont, color); -- 2.49.1 From 1fae47b1718a72ebe386dd2b3cd2a2be15930c1d Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Tue, 30 Sep 2025 05:44:49 +0200 Subject: [PATCH 38/69] Removal of Logging of worlds for testing on what region I should needed --- LightlessSync/UI/ContextMenu.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index 19b5955..6cdc90c 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -131,7 +131,7 @@ internal class ContextMenu : IHostedService var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); - await _apiController.TryPairWithContentId(receiverCid, senderCid); + await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false); } catch (Exception ex) { @@ -160,12 +160,6 @@ internal class ContextMenu : IHostedService return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name); }); - foreach (var world in luminaWorlds) - { - _logger.LogInformation(message: $"ID: {world.RowId} - World: {world.Name.ExtractText()}"); - } - _logger.LogInformation(message: $"Character is in World: {worldId}"); - return luminaWorlds.FirstOrDefault(x => x.RowId == worldId); } private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); -- 2.49.1 From 597a0beb91aeceb8e14511e27980e8d74f8a06a1 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Tue, 30 Sep 2025 05:50:39 +0200 Subject: [PATCH 39/69] Added check if context user is in range or in object table. --- LightlessSync/UI/ContextMenu.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index 6cdc90c..b828cdd 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -90,6 +90,10 @@ internal class ContextMenu : IHostedService if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0) return; + IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); + if (targetData == null || targetData.Address == IntPtr.Zero) + return; + var world = GetWorld(target.TargetHomeWorld.RowId); if (!IsWorldValid(world)) return; @@ -100,7 +104,7 @@ internal class ContextMenu : IHostedService PrefixChar = 'L', UseDefaultPrefix = false, PrefixColor = 708, - OnClicked = async _ => await HandleSelection(args) + OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false) }); } @@ -115,11 +119,7 @@ internal class ContextMenu : IHostedService try { - var targetData = _objectTable - .OfType() - .FirstOrDefault(p => - string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) && - p.HomeWorld.RowId == target.TargetHomeWorld.RowId); + IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); if (targetData == null || targetData.Address == IntPtr.Zero) { @@ -139,6 +139,15 @@ internal class ContextMenu : IHostedService } } + private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target) + { + return _objectTable + .OfType() + .FirstOrDefault(p => + string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) && + p.HomeWorld.RowId == target.TargetHomeWorld.RowId); + } + private World GetWorld(uint worldId) { var sheet = _gameData.GetExcelSheet()!; @@ -162,6 +171,7 @@ internal class ContextMenu : IHostedService return luminaWorlds.FirstOrDefault(x => x.RowId == worldId); } + private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); private static bool IsChineseJapaneseKoreanCharacter(char c) => (c >= 0x4E00 && c <= 0x9FFF); -- 2.49.1 From 36c1611486099fa3f293e4b2118a252fede661d6 Mon Sep 17 00:00:00 2001 From: choco Date: Tue, 30 Sep 2025 16:09:57 +0200 Subject: [PATCH 40/69] compactui settings button peepoo --- LightlessSync/UI/CompactUI.cs | 15 --------------- LightlessSync/UI/TopTabMenu.cs | 6 ++++-- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index d56f035..5390c2f 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -109,21 +109,6 @@ public class CompactUi : WindowMediatorSubscriberBase AllowClickthrough = false; TitleBarButtons = new() { - new TitleBarButton() - { - Icon = FontAwesomeIcon.Cog, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); - }, - IconOffset = new(2,1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("Open Lightless Settings"); - ImGui.EndTooltip(); - } - }, new TitleBarButton() { Icon = FontAwesomeIcon.Book, diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 339b22c..fd7f72c 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -148,11 +148,13 @@ public class TopTabMenu ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) { + var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) { _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); } - + ImGui.SameLine(); + var xAfter = ImGui.GetCursorScreenPos(); } UiSharedService.AttachToolTip("Open Lightless Settings"); -- 2.49.1 From 571decd33b50614df0871aafc9650957080175f6 Mon Sep 17 00:00:00 2001 From: choco Date: Tue, 30 Sep 2025 16:14:53 +0200 Subject: [PATCH 41/69] removed unused cursor position --- LightlessSync/UI/TopTabMenu.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index fd7f72c..52fcc13 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -154,7 +154,6 @@ public class TopTabMenu _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); } ImGui.SameLine(); - var xAfter = ImGui.GetCursorScreenPos(); } UiSharedService.AttachToolTip("Open Lightless Settings"); -- 2.49.1 From 068c8cb1807d5c26d6c1cdb9ad3da779b65e66a3 Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 30 Sep 2025 09:52:12 -0500 Subject: [PATCH 42/69] Merge + Version Bump --- LightlessAPI | 2 +- LightlessSync/LightlessSync.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 5bfd21a..69f0e31 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 5bfd21aaa90817f14c9e2931e77b20f4276f16ed +Subproject commit 69f0e310bd78e0c56eab298199e6e2ca15bf56bd diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 344e968..a9b387d 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 1.12.0 + 1.12.1 https://github.com/Light-Public-Syncshells/LightlessClient -- 2.49.1 From 76520878bfbac8d112058053e5786eadfb0f5f3e Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 1 Oct 2025 03:29:56 +0900 Subject: [PATCH 43/69] bump submodule --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 5bfd21a..69f0e31 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 5bfd21aaa90817f14c9e2931e77b20f4276f16ed +Subproject commit 69f0e310bd78e0c56eab298199e6e2ca15bf56bd -- 2.49.1 From dea4ef4832d8d591c8b4cedb27e7ea5fe76400e6 Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 1 Oct 2025 08:59:08 +0900 Subject: [PATCH 44/69] rich text, updated lightfinder description and bug fixes --- LightlessSync/UI/BroadcastUI.cs | 68 ++++++++++++++++++----- LightlessSync/UI/CompactUI.cs | 23 +++++--- LightlessSync/UI/EditProfileUi.cs | 31 ++++------- LightlessSync/UI/UISharedService.cs | 49 ++++++++++++++++- LightlessSync/Utils/SeStringUtils.cs | 82 +++++++++++++++++++++++++--- 5 files changed, 205 insertions(+), 48 deletions(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index eda5a53..dc0d740 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -1,10 +1,12 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Utility; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using System.Numerics; @@ -44,8 +46,8 @@ namespace LightlessSync.UI IsOpen = false; this.SizeConstraints = new() { - MinimumSize = new(600, 340), - MaximumSize = new(750, 400) + MinimumSize = new(600, 450), + MaximumSize = new(750, 510) }; mediator.Subscribe(this, async _ => await RefreshSyncshells().ConfigureAwait(false)); @@ -137,19 +139,59 @@ namespace LightlessSync.UI { _uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue")); - ImGui.PushTextWrapPos(); - ImGui.Text("This lets other Lightless users know you use Lightless."); - ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); - ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); - ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); - ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); - ImGui.PopTextWrapPos(); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, -2)); + + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "This lets other Lightless users know you use Lightless."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "While enabled, you and other people using Lightfinder can see each other identified as Lightless users."); + ImGui.Indent(5f); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); + ImGui.PopStyleColor(); + ImGui.Unindent(5f); + + ImGuiHelpers.ScaledDummy(3f); + + _uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue")); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Pairing may be initiated via the right-click context menu on another player." + + " The process requires mutual confirmation: the sender initiates the request, and the recipient completes it by responding with a request in return."); + + _uiSharedService.DrawNoteLine( + "! ", + UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("If Lightfinder is "), + new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true), + new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will get notified about it.")); + + _uiSharedService.DrawNoteLine( + "! ", + UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("If Lightfinder is "), + new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will "), + new SeStringUtils.RichTextEntry("NOT", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" get a notification, and the request will not be visible to them in any way.")); + + ImGuiHelpers.ScaledDummy(3f); + + _uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue")); + + _uiSharedService.DrawNoteLine( + "! ", + UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("Lightfinder is entirely "), + new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(" and does not share any data with other users. All identifying information remains private to the server.")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("DimRed"), "Pairing is intended as a mutual agreement between both parties. A pair request will not be visible to the recipient unless Lightfinder is enabled."); + ImGuiHelpers.ScaledDummy(3f); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("Use it only when you want to be visible."); + ImGui.Text("Use Lightfinder only when you want to be visible."); ImGui.PopStyleColor(); - ImGuiHelpers.ScaledDummy(0.2f); + ImGui.PopStyleVar(); + + ImGuiHelpers.ScaledDummy(2.2f); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (_configService.Current.BroadcastEnabled) @@ -168,7 +210,7 @@ namespace LightlessSync.UI else { ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. + ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. ImGui.PopStyleColor(); } } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 5390c2f..170af20 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -109,6 +109,21 @@ public class CompactUi : WindowMediatorSubscriberBase AllowClickthrough = false; TitleBarButtons = new() { + new TitleBarButton() + { + Icon = FontAwesomeIcon.Cog, + Click = (msg) => + { + Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + }, + IconOffset = new(2,1), + ShowTooltip = () => + { + ImGui.BeginTooltip(); + ImGui.Text("Open Lightless Settings"); + ImGui.EndTooltip(); + } + }, new TitleBarButton() { Icon = FontAwesomeIcon.Book, @@ -431,7 +446,7 @@ public class CompactUi : WindowMediatorSubscriberBase float uidStartX = (contentWidth - uidTextSize.X) / 2f; float cursorY = ImGui.GetCursorPosY(); - if (_configService.Current.BroadcastEnabled) + if (_configService.Current.BroadcastEnabled && _apiController.IsConnected) { float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f; var buttonSize = new Vector2(iconSize.X, uidTextSize.Y); @@ -452,12 +467,6 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.Text("Lightfinder"); ImGui.PopStyleColor(); - ImGui.Text("This lets other Lightless users know you use Lightless."); - ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless."); - ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete."); - ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself."); - ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'."); - ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); ImGui.Text("Use it only when you want to be visible."); ImGui.PopStyleColor(); diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 6c787b3..50c4dbe 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -75,15 +75,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase }); } - void DrawNoteLine(string icon, Vector4 color, string text) - { - _uiSharedService.MediumText(icon, color); - ImGui.SameLine(); - - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); - ImGui.TextWrapped(text); - } - private void LoadVanity() { textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex); @@ -101,15 +92,15 @@ public class EditProfileUi : WindowMediatorSubscriberBase _uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow")); ImGui.Dummy(new Vector2(5)); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, 2)); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1)); - DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description."); - DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules."); - DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); - DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); - DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely."); - DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent."); - DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules."); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings."); ImGui.PopStyleVar(); @@ -286,7 +277,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase { _uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(4)); - DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings."); var hasVanity = _apiController.HasVanity; @@ -332,7 +323,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase const float colorPickAlign = 90f; - DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color"); + _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color"); ImGui.SameLine(colorPickAlign); ImGui.Checkbox("##toggleTextColor", ref textEnabled); ImGui.SameLine(); @@ -340,7 +331,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.ColorEdit4($"##color_text", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); ImGui.EndDisabled(); - DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color"); + _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color"); ImGui.SameLine(colorPickAlign); ImGui.Checkbox("##toggleGlowColor", ref glowEnabled); ImGui.SameLine(); diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 24899ee..eb3acce 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.GameFonts; @@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using System; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -531,6 +532,52 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase { FontText(text, MediumFont, color); } + public void DrawNoteLine(string icon, Vector4 color, string text) + { + MediumText(icon, color); + var iconHeight = ImGui.GetItemRectSize().Y; + + ImGui.SameLine(); + + float textHeight = ImGui.GetTextLineHeight(); + float offset = (iconHeight - textHeight) * 0.5f; + if (offset > 0) + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset); + + ImGui.BeginGroup(); + ImGui.TextWrapped(text); + ImGui.EndGroup(); + } + + public void DrawNoteLine(string icon, Vector4 color, ReadOnlySpan fragments) + { + if (fragments.Length == 0) + { + DrawNoteLine(icon, color, string.Empty); + return; + } + + MediumText(icon, color); + var iconHeight = ImGui.GetItemRectSize().Y; + + ImGui.SameLine(); + + float textHeight = ImGui.GetTextLineHeight(); + float offset = (iconHeight - textHeight) * 0.5f; + if (offset > 0) + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset); + + var wrapWidth = ImGui.GetContentRegionAvail().X; + ImGui.BeginGroup(); + var richText = SeStringUtils.BuildRichText(fragments); + SeStringUtils.RenderSeStringWrapped(richText, wrapWidth); + ImGui.EndGroup(); + } + + public void DrawNoteLine(string icon, Vector4 color, params SeStringUtils.RichTextEntry[] fragments) + { + DrawNoteLine(icon, color, fragments.AsSpan()); + } public bool MediumTreeNode(string label, Vector4? textColor = null, float lineWidth = 2f, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.SpanAvailWidth) { diff --git a/LightlessSync/Utils/SeStringUtils.cs b/LightlessSync/Utils/SeStringUtils.cs index 837d13d..a19a343 100644 --- a/LightlessSync/Utils/SeStringUtils.cs +++ b/LightlessSync/Utils/SeStringUtils.cs @@ -1,17 +1,23 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Utility; +using Lumina.Text; +using System; using System.Numerics; +using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString; +using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; +using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder; namespace LightlessSync.Utils; public static class SeStringUtils { - public static SeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) + public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) { - var b = new SeStringBuilder(); + var b = new DalamudSeStringBuilder(); if (glowColor is Vector4 glow) b.Add(new GlowPayload(glow)); @@ -30,14 +36,47 @@ public static class SeStringUtils return b.Build(); } - public static SeString BuildPlain(string text) + public static DalamudSeString BuildPlain(string text) { - var b = new SeStringBuilder(); + var b = new DalamudSeStringBuilder(); b.AddText(text ?? string.Empty); return b.Build(); } - public static void RenderSeString(SeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null) + public static DalamudSeString BuildRichText(ReadOnlySpan fragments) + { + var builder = new LuminaSeStringBuilder(); + + foreach (var fragment in fragments) + { + if (string.IsNullOrEmpty(fragment.Text)) + continue; + + var hasColor = fragment.Color.HasValue; + Vector4 color = default; + if (hasColor) + { + color = fragment.Color!.Value; + builder.PushColorRgba(color); + } + + if (fragment.Bold) + builder.AppendSetBold(true); + + builder.Append(fragment.Text.AsSpan()); + + if (fragment.Bold) + builder.AppendSetBold(false); + + if (hasColor) + builder.PopColor(); + } + + return DalamudSeString.Parse(builder.ToArray()); + } + + public static DalamudSeString BuildRichText(params RichTextEntry[] fragments) => BuildRichText(fragments.AsSpan()); + public static void RenderSeString(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null) { drawList ??= ImGui.GetWindowDrawList(); @@ -51,9 +90,36 @@ public static class SeStringUtils ImGui.SetCursorScreenPos(position); ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + + var textSize = ImGui.CalcTextSize(seString.TextValue); + if (textSize.Y <= 0f) + textSize.Y = ImGui.GetTextLineHeight(); + + ImGui.Dummy(new Vector2(0f, textSize.Y)); } - public static Vector2 RenderSeStringWithHitbox(SeString seString, Vector2 position, ImFontPtr? font = null) + public static void RenderSeStringWrapped(DalamudSeString seString, float wrapWidth, ImFontPtr? font = null, ImDrawListPtr? drawList = null) + { + drawList ??= ImGui.GetWindowDrawList(); + + var drawParams = new SeStringDrawParams + { + Font = font ?? ImGui.GetFont(), + Color = ImGui.GetColorU32(ImGuiCol.Text), + WrapWidth = wrapWidth, + TargetDrawList = drawList + }; + + ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + + var calcWrapWidth = wrapWidth > 0f ? wrapWidth : -1f; + var textSize = ImGui.CalcTextSize(seString.TextValue, wrapWidth: calcWrapWidth); + if (textSize.Y <= 0f) + textSize.Y = ImGui.GetTextLineHeight(); + + ImGui.Dummy(new Vector2(0f, textSize.Y)); + } + public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null) { var drawList = ImGui.GetWindowDrawList(); @@ -99,6 +165,8 @@ public static class SeStringUtils #region Internal Payloads + public readonly record struct RichTextEntry(string Text, Vector4? Color = null, bool Bold = false); + private abstract class AbstractColorPayload : Payload { protected byte Red { get; init; } -- 2.49.1 From e91d163763c9bf44b40192b3091b095bb89e3b65 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 1 Oct 2025 02:47:11 +0200 Subject: [PATCH 45/69] Disabled the pair request on already paired users --- LightlessSync/Plugin.cs | 2 +- LightlessSync/Services/NameplateService.cs | 2 -- LightlessSync/UI/ContextMenu.cs | 22 ++++++++++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 24f1745..93e666a 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -147,7 +147,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle); - collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, p.GetRequiredService())); + collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, p.GetRequiredService(), p.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService>(), pluginInterface, diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 9de944e..6a94a53 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -40,7 +40,6 @@ public class NameplateService : DisposableMediatorSubscriberBase private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) { - if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) return; @@ -78,7 +77,6 @@ public class NameplateService : DisposableMediatorSubscriberBase public void RequestRedraw() { - _namePlateGui.RequestRedraw(); } diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index b828cdd..e300f16 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -3,13 +3,13 @@ using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; +using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Linq; namespace LightlessSync.UI; @@ -21,16 +21,17 @@ internal class ContextMenu : IHostedService private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly LightlessConfigService _configService; + private readonly PairManager _pairManager; private readonly ApiController _apiController; private readonly IObjectTable _objectTable; - private static readonly string[] ValidAddons = new[] - { + private static readonly string[] _validAddons = + [ null, "PartyMemberList", "FriendList", "FreeCompany", "LinkShell", "CrossWorldLinkshell", "_PartyList", "ChatLog", "LookingForGroup", "BlackList", "ContentMemberList", "SocialList", "ContactList", "BeginnerChatList", "MuteList" - }; + ]; public ContextMenu( IContextMenu contextMenu, @@ -40,7 +41,8 @@ internal class ContextMenu : IHostedService DalamudUtilService dalamudUtil, ApiController apiController, IObjectTable objectTable, - LightlessConfigService configService) + LightlessConfigService configService, + PairManager pairManager) { _contextMenu = contextMenu; _pluginInterface = pluginInterface; @@ -50,6 +52,7 @@ internal class ContextMenu : IHostedService _apiController = apiController; _objectTable = objectTable; _configService = configService; + _pairManager = pairManager; } public Task StartAsync(CancellationToken cancellationToken) @@ -81,7 +84,7 @@ internal class ContextMenu : IHostedService if (!_pluginInterface.UiBuilder.ShouldModifyUi) return; - if (!ValidAddons.Contains(args.AddonName, StringComparer.Ordinal)) + if (!_validAddons.Contains(args.AddonName, StringComparer.Ordinal)) return; if (args.Target is not MenuTargetDefault target) @@ -94,6 +97,9 @@ internal class ContextMenu : IHostedService if (targetData == null || targetData.Address == IntPtr.Zero) return; + if (VisibleUserIds.Any(u => u == target.TargetObjectId)) + return; + var world = GetWorld(target.TargetHomeWorld.RowId); if (!IsWorldValid(world)) return; @@ -138,6 +144,10 @@ internal class ContextMenu : IHostedService _logger.LogError(ex, "Error sending pair request."); } } + private HashSet VisibleUserIds => _pairManager.GetOnlineUserPairs() + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId) + .ToHashSet(); private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target) { -- 2.49.1 From 49d138049e0b6ffcb3a03090d1a9241c68d540a5 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 1 Oct 2025 03:15:11 +0200 Subject: [PATCH 46/69] Added check if in PVP/Gpose and check if you are targetting your own. --- LightlessSync/Plugin.cs | 4 +++- LightlessSync/UI/ContextMenu.cs | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 93e666a..7490ebe 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -147,7 +147,9 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle); - collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, p.GetRequiredService(), p.GetRequiredService())); + collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, + p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, + p.GetRequiredService(), p.GetRequiredService(), clientState)); collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService>(), pluginInterface, diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/UI/ContextMenu.cs index e300f16..efa9997 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/UI/ContextMenu.cs @@ -21,6 +21,7 @@ internal class ContextMenu : IHostedService private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly LightlessConfigService _configService; + private readonly IClientState _clientState; private readonly PairManager _pairManager; private readonly ApiController _apiController; private readonly IObjectTable _objectTable; @@ -42,7 +43,8 @@ internal class ContextMenu : IHostedService ApiController apiController, IObjectTable objectTable, LightlessConfigService configService, - PairManager pairManager) + PairManager pairManager, + IClientState clientState) { _contextMenu = contextMenu; _pluginInterface = pluginInterface; @@ -53,6 +55,7 @@ internal class ContextMenu : IHostedService _objectTable = objectTable; _configService = configService; _pairManager = pairManager; + _clientState = clientState; } public Task StartAsync(CancellationToken cancellationToken) @@ -86,20 +89,29 @@ internal class ContextMenu : IHostedService if (!_validAddons.Contains(args.AddonName, StringComparer.Ordinal)) return; - + + //Check if target is not menutargetdefault. if (args.Target is not MenuTargetDefault target) return; + //Check if name or target id isnt null/zero if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0) return; + //Check if it is a real target. IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); if (targetData == null || targetData.Address == IntPtr.Zero) return; - if (VisibleUserIds.Any(u => u == target.TargetObjectId)) + //Check if user is paired or is own. + if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId) return; + //Check if in PVP or GPose + if (_clientState.IsPvPExcludingDen || _clientState.IsGPosing) + return; + + //Check for valid world. var world = GetWorld(target.TargetHomeWorld.RowId); if (!IsWorldValid(world)) return; -- 2.49.1 From bf3770025b8db0b5fe25897af04d4cccaf2e7501 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 1 Oct 2025 03:20:31 +0200 Subject: [PATCH 47/69] Changed logger calls to debug/trace. --- LightlessSync/Services/BroadcastService.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index 6d6409e..bf9eb05 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -141,7 +141,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false); - _logger.LogInformation("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); + _logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); if (!msg.Enabled) { @@ -164,7 +164,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _config.Current.BroadcastEnabled = true; _config.Save(); - _logger.LogInformation("Fetched TTL from server: {TTL}", remaining); + _logger.LogDebug("Fetched TTL from server: {TTL}", remaining); _mediator.Publish(new BroadcastStatusChangedMessage(true, remaining)); Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); } @@ -201,13 +201,13 @@ public class BroadcastService : IHostedService, IMediatorSubscriber { try { - _logger.LogInformation("[BroadcastCheck] Checking CID: {cid}", targetCid); + _logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid); var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false); result = info?.TTL > TimeSpan.Zero; - _logger.LogInformation("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID); + _logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID); } catch (Exception ex) { @@ -251,7 +251,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber result[kv.Key] = kv.Value; } - _logger.LogInformation("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count); + _logger.LogTrace("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count); } catch (Exception ex) { @@ -291,10 +291,10 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (!newStatus) { _lastForcedDisableTime = DateTime.UtcNow; - _logger.LogInformation("Manual disable: cooldown timer started."); + _logger.LogDebug("Manual disable: cooldown timer started."); } - _logger.LogInformation("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus); + _logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus); _mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus)); } @@ -332,7 +332,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; _config.Current.BroadcastEnabled = true; _config.Save(); - _logger.LogInformation("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining); + _logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining); } else { @@ -361,7 +361,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _remainingTtl = remaining > TimeSpan.Zero ? remaining : null; if (_remainingTtl == null) { - _logger.LogInformation("Broadcast TTL expired. Disabling broadcast locally."); + _logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally."); _config.Current.BroadcastEnabled = false; _config.Current.BroadcastTtl = DateTime.MinValue; _config.Save(); -- 2.49.1 From 3a627f2d3edb790a0319ff2438170a0f849dd973 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 1 Oct 2025 04:11:01 +0200 Subject: [PATCH 48/69] Added refetch of syncshells after comfirmation. --- LightlessSync/UI/SyncshellFinderUI.cs | 61 +++++++++++++++------------ 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 4af72d5..c5dc2d2 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -103,6 +103,14 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase 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); @@ -122,18 +130,18 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase var broadcasterName = "Unknown"; var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts() .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); - + if (broadcast != null) { - var playerInfo = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); - if (!string.IsNullOrEmpty(playerInfo.Name)) + var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); + if (!string.IsNullOrEmpty(Name)) { - var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(playerInfo.Address); - broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{playerInfo.Name} ({worldName})" : playerInfo.Name; + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address); + broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name; } } ImGui.TextUnformatted(broadcasterName); - + ImGui.TableNextColumn(); var label = $"Join##{shell.Group.GID}"; @@ -179,7 +187,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase } else { - using (ImRaii.Disabled()) { ImGui.Button(label); @@ -191,9 +198,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.EndTable(); } - - if (_joinDto != null && _joinInfo != null && _joinInfo.Success) - DrawConfirmation(); } private void DrawConfirmation() @@ -222,6 +226,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); _joinDto = null; _joinInfo = null; + _ = RefreshSyncshellsAsync(); } } } @@ -255,7 +260,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private async Task RefreshSyncshellsAsync() { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); - _currentSyncshells = _pairManager.GroupPairs.Select(g => g.Key).ToList(); + _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; if (syncshellBroadcasts.Count == 0) { @@ -263,7 +268,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } - List updatedList = []; + List? updatedList = []; try { var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); @@ -276,23 +281,27 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase } var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal); - var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal); - if (currentGids.SetEquals(newGids)) - return; - - var previousGid = GetSelectedGid(); - - _nearbySyncshells.Clear(); - _nearbySyncshells.AddRange(updatedList); - - if (previousGid != null) + if (updatedList != null) { - var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - if (newIndex >= 0) - { - _selectedNearbyIndex = newIndex; + var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal); + + if (currentGids.SetEquals(newGids)) return; + + 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; + } } } -- 2.49.1 From 714aeef468dbff14448c54fc2f704272359db115 Mon Sep 17 00:00:00 2001 From: CakeAndBanana Date: Wed, 1 Oct 2025 04:16:19 +0200 Subject: [PATCH 49/69] Moved ContextMenu from UI to Service. --- LightlessSync/Plugin.cs | 6 ++--- .../ContextMenuService.cs} | 22 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) rename LightlessSync/{UI/ContextMenu.cs => Services/ContextMenuService.cs} (92%) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 7490ebe..bb66c5a 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -147,8 +147,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle); - collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, - p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, + collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData, + p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, p.GetRequiredService(), p.GetRequiredService(), clientState)); collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); @@ -263,7 +263,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); }) .Build(); diff --git a/LightlessSync/UI/ContextMenu.cs b/LightlessSync/Services/ContextMenuService.cs similarity index 92% rename from LightlessSync/UI/ContextMenu.cs rename to LightlessSync/Services/ContextMenuService.cs index efa9997..e941311 100644 --- a/LightlessSync/UI/ContextMenu.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -4,21 +4,20 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Pairs; -using LightlessSync.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace LightlessSync.UI; +namespace LightlessSync.Services; -internal class ContextMenu : IHostedService +internal class ContextMenuService : IHostedService { private readonly IContextMenu _contextMenu; private readonly IDalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly LightlessConfigService _configService; private readonly IClientState _clientState; @@ -34,11 +33,11 @@ internal class ContextMenu : IHostedService "SocialList", "ContactList", "BeginnerChatList", "MuteList" ]; - public ContextMenu( + public ContextMenuService( IContextMenu contextMenu, IDalamudPluginInterface pluginInterface, IDataManager gameData, - ILogger logger, + ILogger logger, DalamudUtilService dalamudUtil, ApiController apiController, IObjectTable objectTable, @@ -100,7 +99,7 @@ internal class ContextMenu : IHostedService //Check if it is a real target. IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); - if (targetData == null || targetData.Address == IntPtr.Zero) + if (targetData == null || targetData.Address == nint.Zero) return; //Check if user is paired or is own. @@ -139,7 +138,7 @@ internal class ContextMenu : IHostedService { IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); - if (targetData == null || targetData.Address == IntPtr.Zero) + if (targetData == null || targetData.Address == nint.Zero) { _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name); return; @@ -156,10 +155,9 @@ internal class ContextMenu : IHostedService _logger.LogError(ex, "Error sending pair request."); } } - private HashSet VisibleUserIds => _pairManager.GetOnlineUserPairs() + private HashSet VisibleUserIds => [.. _pairManager.GetOnlineUserPairs() .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId) - .ToHashSet(); + .Select(u => (ulong)u.PlayerCharacterId)]; private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target) { @@ -196,7 +194,7 @@ internal class ContextMenu : IHostedService private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); - private static bool IsChineseJapaneseKoreanCharacter(char c) => (c >= 0x4E00 && c <= 0x9FFF); + private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF; public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId)); -- 2.49.1 From afc42d97d16e4eb2012645fae21ec6bff8636b4b Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 1 Oct 2025 23:25:34 +0900 Subject: [PATCH 50/69] highlight dark uid's --- .../Configurations/LightlessConfig.cs | 1 + LightlessSync/UI/Handlers/IdDisplayHandler.cs | 70 +++++++++++++++++-- LightlessSync/UI/SettingsUi.cs | 13 +++- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 235f0c5..7194c60 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -67,6 +67,7 @@ public class LightlessConfig : ILightlessConfiguration public bool UseFocusTarget { get; set; } = false; public bool overrideFriendColor { get; set; } = false; public bool overridePartyColor { get; set; } = false; + public bool useColoredUIDs { get; set; } = true; public bool BroadcastEnabled { get; set; } = false; public DateTime BroadcastTtl { get; set; } = DateTime.MinValue; public bool SyncshellFinderEnabled { get; set; } = false; diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 048efe9..1e3fee2 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -1,5 +1,6 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; +using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; @@ -7,6 +8,7 @@ using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.Utils; +using System; using System.Numerics; namespace LightlessSync.UI.Handlers; @@ -114,14 +116,74 @@ public class IdDisplayHandler } } - var seString = (textColor != null || glowColor != null) + var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null); + var seString = useVanityColors ? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor) : SeStringUtils.BuildPlain(playerText); + var rowStart = ImGui.GetCursorScreenPos(); + var drawList = ImGui.GetWindowDrawList(); + bool useHighlight = false; + float highlightPadX = 0f; + float highlightPadY = 0f; + + if (useVanityColors && textColor is Vector4 contrastColor) + { + var brightness = (0.299f * contrastColor.X) + (0.587f * contrastColor.Y) + (0.114f * contrastColor.Z); + if (brightness < 0.35f) + { + var style = ImGui.GetStyle(); + useHighlight = true; + highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale); + highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale); + drawList.ChannelsSplit(2); + drawList.ChannelsSetCurrent(1); + } + } + + Vector2 itemMin; + Vector2 itemMax; + Vector2 textSize; using (ImRaii.PushFont(font, textIsUid)) { - var pos = ImGui.GetCursorScreenPos(); - SeStringUtils.RenderSeStringWithHitbox(seString, pos, font); + SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font); + itemMin = ImGui.GetItemRectMin(); + itemMax = ImGui.GetItemRectMax(); + textSize = itemMax - itemMin; + } + + if (useHighlight) + { + var style = ImGui.GetStyle(); + var frameHeight = ImGui.GetFrameHeight(); + var rowTop = rowStart.Y - style.FramePadding.Y; + var rowBottom = rowTop + frameHeight; + + var highlightMin = new Vector2(itemMin.X - highlightPadX, rowTop - highlightPadY); + var highlightMax = new Vector2(itemMax.X + highlightPadX, rowBottom + highlightPadY); + + var windowPos = ImGui.GetWindowPos(); + var contentMin = windowPos + ImGui.GetWindowContentRegionMin(); + var contentMax = windowPos + ImGui.GetWindowContentRegionMax(); + highlightMin.X = MathF.Max(highlightMin.X, contentMin.X); + highlightMax.X = MathF.Min(highlightMax.X, contentMax.X); + highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y); + highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y); + + var highlightColor = style.Colors[(int)ImGuiCol.TableRowBgAlt]; + highlightColor.X = 0.25f; + highlightColor.Y = 0.25f; + highlightColor.Z = 0.25f; + highlightColor.W = 1f; + + float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale; + drawList.ChannelsSetCurrent(0); + drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding); + + var borderColor = style.Colors[(int)ImGuiCol.Border]; + borderColor.W *= 0.25f; + drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding); + drawList.ChannelsMerge(); } if (ImGui.IsItemHovered()) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 95bb2af..2047fa0 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1109,12 +1109,23 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - if (ImGui.Checkbox("Use the complete redesign of the UI for Lightless client.", ref useLightlessRedesign)) + ImGui.TextUnformatted("UI Theme"); + + if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign)) { _configService.Current.UseLightlessRedesign = useLightlessRedesign; _configService.Save(); } + var usePairColoredUIDs = _configService.Current.useColoredUIDs; + + if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs)) + { + _configService.Current.useColoredUIDs = usePairColoredUIDs; + _configService.Save(); + } + _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } -- 2.49.1 From f6e1832d62503cfa22613881af0b2e2f3745b9f5 Mon Sep 17 00:00:00 2001 From: defnotken Date: Wed, 1 Oct 2025 19:26:03 -0500 Subject: [PATCH 51/69] Bugfixes for lightfinder --- LightlessSync/LightlessSync.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index a9b387d..4bab148 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 1.12.1 + 1.12.2 https://github.com/Light-Public-Syncshells/LightlessClient -- 2.49.1 From 54ea1e9d4c290dd0bbd564377aafeec9f1a8bdc1 Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 2 Oct 2025 15:38:04 -0500 Subject: [PATCH 52/69] cleanup workflows --- .../workflows/lightless-tag-and-release.yml | 97 +++++++++++- .../workflows/lightless-tag-and-release.yml | 140 ------------------ 2 files changed, 92 insertions(+), 145 deletions(-) delete mode 100644 .github/workflows/lightless-tag-and-release.yml diff --git a/.gitea/workflows/lightless-tag-and-release.yml b/.gitea/workflows/lightless-tag-and-release.yml index 63b4fb0..3401e21 100644 --- a/.gitea/workflows/lightless-tag-and-release.yml +++ b/.gitea/workflows/lightless-tag-and-release.yml @@ -2,7 +2,7 @@ name: Tag and Release Lightless on: push: - branches: [ master ] + branches: [ master, dev ] env: PLUGIN_NAME: LightlessSync @@ -62,7 +62,8 @@ jobs: mkdir -p output (cd /workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/ && zip -r $OLDPWD/output/LightlessClient.zip *) - - name: Create Git tag if not exists + - name: Create Git tag if not exists (master) + if: github.ref == 'refs/heads/master' run: | tag="${{ steps.package_version.outputs.version }}" git fetch --tags @@ -76,7 +77,23 @@ jobs: echo "Tag $tag already exists. Skipping tag creation." fi - - name: Create Release + - name: Create Git tag if not exists (dev) + if: github.ref == 'refs/heads/dev' + run: | + tag="${{ steps.package_version.outputs.version }}-Dev" + git fetch --tags + if ! git tag -l "$tag" | grep -q "$tag"; then + echo "Tag $tag does not exist. Creating and pushing..." + git config user.name "GitHub Action" + git config user.email "action@github.com" + git tag "$tag" + git push origin "$tag" + else + echo "Tag $tag already exists. Skipping tag creation." + fi + + - name: Create Release (master) + if: github.ref == 'refs/heads/master' id: create_release run: | echo "=== Searching for existing release ${{ steps.package_version.outputs.version }}===" @@ -107,6 +124,35 @@ jobs: release_id=$(echo "$response" | jq -r .id) echo "release_id=$release_id" >> "$GITHUB_OUTPUT" + - name: Create Release (dev) + if: github.ref == 'refs/heads/dev' + id: create_release + run: | + version="${{ steps.package_version.outputs.version }}-Dev" + echo "=== Searching for existing release $version===" + release_id=$(curl -s -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + "https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/$version" | jq -r .id) + if [ "$release_id" != "null" ]; then + echo "=== Deleting existing release $version===" + curl -X DELETE -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + "https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$release_id" + fi + echo "=== Creating new release $version===" + response=$( + curl --fail-with-body -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + -d '{ + "tag_name": "'"$version"'", + "name": "'"$version"'", + "draft": false, + "prerelease": false + }' \ + "https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases" + ) + release_id=$(echo "$response" | jq -r .id) + echo "release_id=$release_id" >> "$GITHUB_OUTPUT" + - name: Upload Assets to release run: | curl --fail-with-body -s -X POST \ @@ -122,7 +168,8 @@ jobs: env: GIT_TERMINAL_PROMPT: 0 - - name: Update plogonmaster.json with version + - name: Update plogonmaster.json with version (master) + if: github.ref == 'refs/heads/master' env: VERSION: ${{ steps.package_version.outputs.version }} run: | @@ -159,7 +206,6 @@ jobs: .DalamudApiLevel = $dalamudApiLevel | .AssemblyVersion = $version | .DownloadLinkInstall = $downloadUrl - | .DownloadLinkTesting = $downloadUrl | .DownloadLinkUpdate = $downloadUrl else . @@ -172,6 +218,47 @@ jobs: # Output the content of the file cat "$repoJsonPath" + - name: Update plogonmaster.json with version (dev) + if: github.ref == 'refs/heads/dev' + env: + VERSION: ${{ steps.package_version.outputs.version }} + run: | + set -e + pluginJsonPath="${PLUGIN_NAME}/bin/x64/Release/${PLUGIN_NAME}.json" + repoJsonPath="LightlessSyncRepo/LightlessSync/plogonmaster.json" + assemblyVersion="${VERSION}" + version="${VERSION}-Dev" + downloadUrl="https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip" + pluginJson=$(cat "$pluginJsonPath") + internalName=$(jq -r '.InternalName' <<< "$pluginJson") + dalamudApiLevel=$(jq -r '.DalamudApiLevel' <<< "$pluginJson") + repoJsonRaw=$(cat "$repoJsonPath") + if echo "$repoJsonRaw" | jq 'type' | grep -q '"array"'; then + repoJson="$repoJsonRaw" + else + repoJson="[$repoJsonRaw]" + fi + updatedRepoJson=$(jq \ + --arg internalName "$internalName" \ + --arg dalamudApiLevel "$dalamudApiLevel" \ + --arg assemblyVersion "$assemblyVersion" \ + --arg version "$version" \ + --arg downloadUrl "$downloadUrl" \ + ' + map( + if .InternalName == $internalName + then + .DalamudApiLevel = $dalamudApiLevel + | .TestingAssemblyVersion = $assemblyVersion + | .DownloadLinkTesting = $downloadUrl + else + . + end + ) + ' <<< "$repoJson") + echo "$updatedRepoJson" > "$repoJsonPath" + cat "$repoJsonPath" + - name: Commit and push to LightlessSync run: | cd LightlessSyncRepo/LightlessSync diff --git a/.github/workflows/lightless-tag-and-release.yml b/.github/workflows/lightless-tag-and-release.yml deleted file mode 100644 index 9391829..0000000 --- a/.github/workflows/lightless-tag-and-release.yml +++ /dev/null @@ -1,140 +0,0 @@ -name: Tag and Release Lightless - -on: - push: - branches: [ master ] - -env: - PLUGIN_NAME: LightlessSync - DOTNET_VERSION: 9.x - -jobs: - tag-and-release: - runs-on: windows-2022 - permissions: - contents: write - - steps: - - name: Checkout Lightless - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: true - - - name: Setup .NET 9 SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.x - - - name: Download Dalamud - run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip - Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - - - name: Lets Build Lightless! - run: | - dotnet restore - dotnet build --configuration Release --no-restore - dotnet publish --configuration Release --no-build - - - name: Get version - id: package_version - uses: KageKirin/get-csproj-version@v0 - with: - file: LightlessSync/LightlessSync.csproj - - - name: Display version - run: | - echo "Version: ${{ steps.package_version.outputs.version }}" - - - name: Prepare Lightless Client - run: | - $publishPath = "${{ env.PLUGIN_NAME }}/bin/x64/Release/publish" - if (Test-Path $publishPath) { - Remove-Item -Recurse -Force $publishPath - Write-Host "Removed $publishPath" - } else { - Write-Host "$publishPath does not exist, nothing to remove." - } - mkdir output - Compress-Archive -Path ${{ env.PLUGIN_NAME }}/bin/x64/Release/* -DestinationPath output/LightlessClient.zip - - - name: Create Git tag if not exists - shell: pwsh - run: | - $tag = "${{ steps.package_version.outputs.version }}" - git fetch --tags - if (-not (git tag -l $tag)) { - Write-Host "Tag $tag does not exist. Creating and pushing..." - git config user.name "GitHub Action" - git config user.email "action@github.com" - git tag $tag - git push origin $tag - } else { - Write-Host "Tag $tag already exists. Skipping tag creation." - } - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.package_version.outputs.version }} - name: ${{ steps.package_version.outputs.version }} - draft: false - prerelease: false - files: output/LightlessClient.zip - - - name: Clone plugin hosting repo - run: | - mkdir LightlessSyncRepo - cd LightlessSyncRepo - git clone https://github.com/${{ github.repository_owner }}/LightlessSync.git - env: - GIT_TERMINAL_PROMPT: 0 - - - name: Update plogonmaster.json with version - shell: pwsh - env: - VERSION: ${{ steps.package_version.outputs.version }} - run: | - $pluginJsonPath = "${{ env.PLUGIN_NAME }}/bin/x64/Release/${{ env.PLUGIN_NAME }}.json" - $pluginJson = Get-Content $pluginJsonPath | ConvertFrom-Json - $repoJsonPath = "LightlessSyncRepo/LightlessSync/plogonmaster.json" - $repoJsonRaw = Get-Content $repoJsonPath -Raw - $repoJson = $repoJsonRaw | ConvertFrom-Json - $version = $env:VERSION - $downloadUrl = "https://github.com/${{ github.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip" - - if (-not ($repoJson -is [System.Collections.IEnumerable])) { - $repoJson = @($repoJson) - } - - foreach ($plugin in $repoJson) { - if ($plugin.InternalName -eq $pluginJson.InternalName) { - $plugin.DalamudApiLevel = $pluginJson.DalamudApiLevel - $plugin.AssemblyVersion = $version - $plugin.DownloadLinkInstall = $downloadUrl - $plugin.DownloadLinkTesting = $downloadUrl - $plugin.DownloadLinkUpdate = $downloadUrl - } - } - - $repoJson | ConvertTo-Json -Depth 100 | Set-Content $repoJsonPath - - # Convert to JSON and force array brackets if necessary - $repoJsonString = $repoJson | ConvertTo-Json -Depth 100 - - # If the output is not an array, wrap it manually - if ($repoJsonString.Trim().StartsWith('{')) { - $repoJsonString = "[$repoJsonString]" - } - - $repoJsonString | Set-Content $repoJsonPath - - - name: Commit and push to LightlessSync - run: | - cd LightlessSyncRepo/LightlessSync - git config user.name "github-actions" - git config user.email "github-actions@github.com" - git add . - git commit -m "Update ${{ env.PLUGIN_NAME }} to ${{ steps.package_version.outputs.version }}" - git push https://x-access-token:${{ secrets.LIGHTLESS_TOKEN }}@github.com/${{ github.repository_owner }}/LightlessSync.git HEAD:main -- 2.49.1 From 05872c285cdda5512f649abdd5f36d1eb5d5152d Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 2 Oct 2025 15:39:58 -0500 Subject: [PATCH 53/69] more fixes --- LightlessSync/LightlessSync.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 4bab148..5b31c88 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 1.12.2 + 1.12.3 https://github.com/Light-Public-Syncshells/LightlessClient -- 2.49.1 From f2b7b0c4e3c9709dec266a0935c8ebc3e6c9ca5b Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 2 Oct 2025 17:29:15 -0500 Subject: [PATCH 54/69] Send Pair Request will only show on Target Player rather than menus. --- LightlessSync/Services/ContextMenuService.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index e941311..632a047 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -19,20 +19,11 @@ internal class ContextMenuService : IHostedService private readonly IDataManager _gameData; private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; - private readonly LightlessConfigService _configService; private readonly IClientState _clientState; private readonly PairManager _pairManager; private readonly ApiController _apiController; private readonly IObjectTable _objectTable; - private static readonly string[] _validAddons = - [ - null, - "PartyMemberList", "FriendList", "FreeCompany", "LinkShell", "CrossWorldLinkshell", - "_PartyList", "ChatLog", "LookingForGroup", "BlackList", "ContentMemberList", - "SocialList", "ContactList", "BeginnerChatList", "MuteList" - ]; - public ContextMenuService( IContextMenu contextMenu, IDalamudPluginInterface pluginInterface, @@ -52,7 +43,6 @@ internal class ContextMenuService : IHostedService _dalamudUtil = dalamudUtil; _apiController = apiController; _objectTable = objectTable; - _configService = configService; _pairManager = pairManager; _clientState = clientState; } @@ -83,10 +73,11 @@ internal class ContextMenuService : IHostedService private void OnMenuOpened(IMenuOpenedArgs args) { + if (!_pluginInterface.UiBuilder.ShouldModifyUi) return; - if (!_validAddons.Contains(args.AddonName, StringComparer.Ordinal)) + if (args.AddonName != null) return; //Check if target is not menutargetdefault. @@ -114,7 +105,7 @@ internal class ContextMenuService : IHostedService var world = GetWorld(target.TargetHomeWorld.RowId); if (!IsWorldValid(world)) return; - + args.AddMenuItem(new MenuItem { Name = "Send Pair Request", -- 2.49.1 From 4862921b039f0acb754b4097bafae96569d4567f Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 3 Oct 2025 09:50:37 +0200 Subject: [PATCH 55/69] table layout for color settings --- LightlessSync/UI/SettingsUi.cs | 75 ++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 2047fa0..95292de 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -978,45 +978,66 @@ public class SettingsUi : WindowMediatorSubscriberBase var colorNames = new[] { - ("LightlessPurple", "Lightless Purple", "Primary colors"), - ("LightlessPurpleActive", "Lightless Purple Active", "Primary colors"), - ("LightlessPurpleDefault", "Lightless Purple Inactive", "Primary colors"), - ("LightlessBlue", "Lightless Blue", "Secondary colors"), + ("LightlessPurple", "Lightless Purple", "Section titles and dividers"), + ("LightlessPurpleActive", "Lightless Purple Active", "Active tabs and hover highlights"), + ("LightlessPurpleDefault", "Lightless Purple Inactive", "Inactive tabs and default dividers"), + ("LightlessBlue", "Lightless Blue", "On/true toggles and 'Upload complete' status"), - ("LightlessGreen", "Lightless Green", "Active elements"), + ("LightlessGreen", "Lightless Green", "Join buttons and success messages"), ("LightlessYellow", "Lightless Yellow", "Warning colors"), ("LightlessYellow2", "Lightless Yellow 2", "Warning colors"), - ("PairBlue", "Pair Blue", "Pair UI elements"), + ("PairBlue", "Pair Blue", "Syncshell headers, toggle highlights, and moderator actions"), - ("DimRed", "Dim Red", "Error and offline") + ("DimRed", "Dim Red", "Error and offline colors") }; - - foreach (var (colorKey, displayName, description) in colorNames) + if (ImGui.BeginTable("##ColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { - var currentColor = UIColors.Get(colorKey); - var colorToEdit = currentColor; + ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); + ImGui.TableHeadersRow(); - ImGui.AlignTextToFramePadding(); - - if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + foreach (var (colorKey, displayName, description) in colorNames) { - UIColors.Set(colorKey, colorToEdit); - } - - ImGui.SameLine(); - ImGui.TextUnformatted($"{displayName} - {description}"); - - if (UIColors.IsCustom(colorKey)) - { - ImGui.SameLine(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, $"Reset {colorKey}")) + ImGui.TableNextRow(); + + // olor column + ImGui.TableSetColumnIndex(0); + var currentColor = UIColors.Get(colorKey); + var colorToEdit = currentColor; + if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) { - UIColors.Reset(colorKey); + UIColors.Set(colorKey, colorToEdit); + } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(displayName); + + // description column + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(description); + + // actions column + ImGui.TableSetColumnIndex(2); + if (UIColors.IsCustom(colorKey)) + { + using var resetId = ImRaii.PushId($"Reset_{colorKey}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + { + UIColors.Reset(colorKey); + } + } + UiSharedService.AttachToolTip("Reset this color to default"); } - UiSharedService.AttachToolTip("Reset this color to default"); } + + ImGui.EndTable(); } ImGui.Spacing(); @@ -2263,4 +2284,4 @@ public class SettingsUi : WindowMediatorSubscriberBase _wasOpen = IsOpen; IsOpen = false; } -} \ No newline at end of file +} -- 2.49.1 From e8a3d87ff05122ae79ccf9abc9e5cc34e9641dbb Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 3 Oct 2025 09:53:46 +0200 Subject: [PATCH 56/69] renamed color variables --- LightlessSync/UI/SettingsUi.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 95292de..e2a218f 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -978,19 +978,19 @@ public class SettingsUi : WindowMediatorSubscriberBase var colorNames = new[] { - ("LightlessPurple", "Lightless Purple", "Section titles and dividers"), - ("LightlessPurpleActive", "Lightless Purple Active", "Active tabs and hover highlights"), - ("LightlessPurpleDefault", "Lightless Purple Inactive", "Inactive tabs and default dividers"), - ("LightlessBlue", "Lightless Blue", "On/true toggles and 'Upload complete' status"), + ("LightlessPurple", "Accent Purple", "Section titles and dividers"), + ("LightlessPurpleActive", "Accent Purple (Active)", "Active tabs and hover highlights"), + ("LightlessPurpleDefault", "Accent Purple (Inactive)", "Inactive tabs and default dividers"), + ("LightlessBlue", "Status Blue", "On/true toggles and 'Upload complete' status"), - ("LightlessGreen", "Lightless Green", "Join buttons and success messages"), + ("LightlessGreen", "Success Green", "Join buttons and success messages"), - ("LightlessYellow", "Lightless Yellow", "Warning colors"), - ("LightlessYellow2", "Lightless Yellow 2", "Warning colors"), + ("LightlessYellow", "Warning Yellow", "Warning colors"), + ("LightlessYellow2", "Warning Yellow (Alt)", "Warning colors"), - ("PairBlue", "Pair Blue", "Syncshell headers, toggle highlights, and moderator actions"), + ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), - ("DimRed", "Dim Red", "Error and offline colors") + ("DimRed", "Error Red", "Error and offline colors") }; if (ImGui.BeginTable("##ColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { @@ -1003,7 +1003,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { ImGui.TableNextRow(); - // olor column + // color column ImGui.TableSetColumnIndex(0); var currentColor = UIColors.Get(colorKey); var colorToEdit = currentColor; -- 2.49.1 From 9242f23787db8e290b18de05de8c51c3e6692d5a Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 3 Oct 2025 09:55:54 +0200 Subject: [PATCH 57/69] color reset button always shown, but disabled if no custom value --- LightlessSync/UI/SettingsUi.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index e2a218f..ce2b230 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1022,10 +1022,12 @@ public class SettingsUi : WindowMediatorSubscriberBase // actions column ImGui.TableSetColumnIndex(2); - if (UIColors.IsCustom(colorKey)) + using var resetId = ImRaii.PushId($"Reset_{colorKey}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + var isCustom = UIColors.IsCustom(colorKey); + + using (ImRaii.Disabled(!isCustom)) { - using var resetId = ImRaii.PushId($"Reset_{colorKey}"); - var availableWidth = ImGui.GetContentRegionAvail().X; using (ImRaii.PushFont(UiBuilder.IconFont)) { if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) @@ -1033,8 +1035,8 @@ public class SettingsUi : WindowMediatorSubscriberBase UIColors.Reset(colorKey); } } - UiSharedService.AttachToolTip("Reset this color to default"); } + UiSharedService.AttachToolTip(isCustom ? "Reset this color to default" : "Color is already at default value"); } ImGui.EndTable(); -- 2.49.1 From 012547056a6f2ffb11f8a7e2d65037de9b244dd1 Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 3 Oct 2025 09:58:58 +0200 Subject: [PATCH 58/69] better column alignment --- LightlessSync/UI/SettingsUi.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index ce2b230..a4e450a 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -994,7 +994,7 @@ public class SettingsUi : WindowMediatorSubscriberBase }; if (ImGui.BeginTable("##ColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { - ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); ImGui.TableHeadersRow(); -- 2.49.1 From 36bf17aee29af70756bff1fdbabe9c7af73c22b9 Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 3 Oct 2025 10:03:09 +0200 Subject: [PATCH 59/69] rename accent purple labels to primary purple --- LightlessSync/UI/SettingsUi.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index a4e450a..9cc1114 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -978,9 +978,9 @@ public class SettingsUi : WindowMediatorSubscriberBase var colorNames = new[] { - ("LightlessPurple", "Accent Purple", "Section titles and dividers"), - ("LightlessPurpleActive", "Accent Purple (Active)", "Active tabs and hover highlights"), - ("LightlessPurpleDefault", "Accent Purple (Inactive)", "Inactive tabs and default dividers"), + ("LightlessPurple", "Primary Purple", "Section titles and dividers"), + ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), + ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), ("LightlessBlue", "Status Blue", "On/true toggles and 'Upload complete' status"), ("LightlessGreen", "Success Green", "Join buttons and success messages"), -- 2.49.1 From 931009607b0ee4e36f8cb3e886958ff6b87f3c06 Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 3 Oct 2025 10:33:36 +0200 Subject: [PATCH 60/69] color key name change --- LightlessSync/UI/SettingsUi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 9cc1114..231294e 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -981,7 +981,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ("LightlessPurple", "Primary Purple", "Section titles and dividers"), ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), - ("LightlessBlue", "Status Blue", "On/true toggles and 'Upload complete' status"), + ("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"), ("LightlessGreen", "Success Green", "Join buttons and success messages"), -- 2.49.1 From 3d2650cc5fba0c24d2def34bbb735285546dd221 Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 3 Oct 2025 16:11:09 +0200 Subject: [PATCH 61/69] added FC tag color override option for player nameplates --- .../Configurations/LightlessConfig.cs | 1 + LightlessSync/Services/NameplateService.cs | 10 +++++++--- LightlessSync/UI/SettingsUi.cs | 9 ++++++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 7194c60..6703f1c 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -67,6 +67,7 @@ public class LightlessConfig : ILightlessConfiguration public bool UseFocusTarget { get; set; } = false; public bool overrideFriendColor { get; set; } = false; public bool overridePartyColor { get; set; } = false; + public bool overrideFcTagColor { get; set; } = false; public bool useColoredUIDs { get; set; } = true; public bool BroadcastEnabled { get; set; } = false; public DateTime BroadcastTtl { get; set; } = DateTime.MinValue; diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 6a94a53..27fa06f 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Gui.NamePlate; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; @@ -68,10 +68,14 @@ public class NameplateService : DisposableMediatorSubscriberBase (isFriend && !friendColorAllowed) )) { - //_logger.LogInformation("added nameplate color to {Name}", playerCharacter.Name.TextValue); handler.NameParts.TextWrap = CreateTextWrap(colors); - } + if (_configService.Current.overrideFcTagColor) + { + handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors); + handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors); + } + } } } diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 231294e..cb34877 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -1096,6 +1096,7 @@ public class SettingsUi : WindowMediatorSubscriberBase var nameColors = _configService.Current.NameplateColors; var isFriendOverride = _configService.Current.overrideFriendColor; var isPartyOverride = _configService.Current.overridePartyColor; + var isFcTagOverride = _configService.Current.overrideFcTagColor; if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) { @@ -1126,6 +1127,12 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); _nameplateService.RequestRedraw(); } + if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride)) + { + _configService.Current.overrideFcTagColor = isFcTagOverride; + _configService.Save(); + _nameplateService.RequestRedraw(); + } } ImGui.Spacing(); -- 2.49.1 From 458aa5f933915a844c2a1c59f2f06a9563769cc9 Mon Sep 17 00:00:00 2001 From: azyges Date: Sat, 4 Oct 2025 01:57:50 +0900 Subject: [PATCH 62/69] bunch of changes - incoming pair requests - auto fill notes when paired - vanity colored uid at the top - notifications now resolve player names - hide lightfinder icon when not connected - fixed download snapshot crashing the ui, supposedly --- LightlessAPI | 2 +- LightlessSync/Plugin.cs | 3 +- LightlessSync/Services/Mediator/Messages.cs | 1 + LightlessSync/Services/PairRequestService.cs | 189 ++++++++++++++ LightlessSync/UI/BroadcastUI.cs | 8 +- LightlessSync/UI/CompactUI.cs | 95 ++++++- LightlessSync/UI/TopTabMenu.cs | 236 +++++++++++++++++- .../ApiController.Functions.Callbacks.cs | 22 ++ LightlessSync/WebAPI/SignalR/ApiController.cs | 5 +- 9 files changed, 542 insertions(+), 19 deletions(-) create mode 100644 LightlessSync/Services/PairRequestService.cs diff --git a/LightlessAPI b/LightlessAPI index 69f0e31..6c542c0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 69f0e310bd78e0c56eab298199e6e2ca15bf56bd +Subproject commit 6c542c0ccca0327896ef895f9de02a76869ea311 diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index bb66c5a..c3b1216 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; @@ -113,6 +113,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 2f7d8d2..b753341 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -100,6 +100,7 @@ public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase; +public record PairRequestsUpdatedMessage : MessageBase; public record VisibilityChange : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/LightlessSync/Services/PairRequestService.cs b/LightlessSync/Services/PairRequestService.cs new file mode 100644 index 0000000..998ea42 --- /dev/null +++ b/LightlessSync/Services/PairRequestService.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services; + +public sealed class PairRequestService : DisposableMediatorSubscriberBase +{ + private readonly DalamudUtilService _dalamudUtil; + private readonly PairManager _pairManager; + private readonly object _syncRoot = new(); + private readonly List _requests = []; + + private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); + + public PairRequestService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager) + : base(logger, mediator) + { + _dalamudUtil = dalamudUtil; + _pairManager = pairManager; + + Mediator.Subscribe(this, _ => + { + bool removed; + lock (_syncRoot) + { + removed = CleanupExpiredUnsafe(); + } + + if (removed) + { + Mediator.Publish(new PairRequestsUpdatedMessage()); + } + }); + } + + public PairRequestDisplay RegisterIncomingRequest(string hashedCid, string messageTemplate) + { + if (string.IsNullOrWhiteSpace(hashedCid)) + { + hashedCid = string.Empty; + } + + messageTemplate ??= string.Empty; + + PairRequestEntry entry = new(hashedCid, messageTemplate, DateTime.UtcNow); + lock (_syncRoot) + { + CleanupExpiredUnsafe(); + var index = _requests.FindIndex(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)); + if (index >= 0) + { + _requests[index] = entry; + } + else + { + _requests.Add(entry); + } + } + + var display = _dalamudUtil.IsOnFrameworkThread + ? ToDisplay(entry) + : _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult(); + + Mediator.Publish(new PairRequestsUpdatedMessage()); + return display; + } + + public IReadOnlyList GetActiveRequests() + { + List entries; + lock (_syncRoot) + { + CleanupExpiredUnsafe(); + entries = _requests + .OrderByDescending(r => r.ReceivedAt) + .ToList(); + } + + return _dalamudUtil.IsOnFrameworkThread + ? entries.Select(ToDisplay).ToList() + : _dalamudUtil.RunOnFrameworkThread(() => entries.Select(ToDisplay).ToList()).GetAwaiter().GetResult(); + } + + public bool RemoveRequest(string hashedCid) + { + bool removed; + lock (_syncRoot) + { + removed = _requests.RemoveAll(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)) > 0; + } + + if (removed) + { + Mediator.Publish(new PairRequestsUpdatedMessage()); + } + + return removed; + } + + public bool HasPendingRequests() + { + lock (_syncRoot) + { + CleanupExpiredUnsafe(); + return _requests.Count > 0; + } + } + + private PairRequestDisplay ToDisplay(PairRequestEntry entry) + { + var displayName = ResolveDisplayName(entry.HashedCid); + var message = FormatMessage(entry.MessageTemplate, displayName); + return new PairRequestDisplay(entry.HashedCid, displayName, message, entry.ReceivedAt); + } + + private string ResolveDisplayName(string hashedCid) + { + if (string.IsNullOrWhiteSpace(hashedCid)) + { + return string.Empty; + } + + var (name, address) = _dalamudUtil.FindPlayerByNameHash(hashedCid); + if (!string.IsNullOrWhiteSpace(name)) + { + var worldName = _dalamudUtil.GetWorldNameFromPlayerAddress(address); + return !string.IsNullOrWhiteSpace(worldName) + ? $"{name} @ {worldName}" + : name; + } + + var pair = _pairManager + .GetOnlineUserPairs() + .FirstOrDefault(p => string.Equals(p.Ident, hashedCid, StringComparison.Ordinal)); + + if (pair != null) + { + if (!string.IsNullOrWhiteSpace(pair.PlayerName)) + { + return pair.PlayerName; + } + + if (!string.IsNullOrWhiteSpace(pair.UserData.AliasOrUID)) + { + return pair.UserData.AliasOrUID; + } + } + + return string.Empty; + } + + private static string FormatMessage(string template, string displayName) + { + var safeName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName; + template ??= string.Empty; + const string placeholder = "{DisplayName}"; + + if (!string.IsNullOrEmpty(template) && template.Contains(placeholder, StringComparison.Ordinal)) + { + return template.Replace(placeholder, safeName, StringComparison.Ordinal); + } + + if (!string.IsNullOrWhiteSpace(template)) + { + return $"{safeName}: {template}"; + } + + return $"{safeName} sent you a pair request."; + } + + private bool CleanupExpiredUnsafe() + { + if (_requests.Count == 0) + { + return false; + } + + var now = DateTime.UtcNow; + return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0; + } + + private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt); + + public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt); +} diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index dc0d740..ae3d17a 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -46,8 +46,8 @@ namespace LightlessSync.UI IsOpen = false; this.SizeConstraints = new() { - MinimumSize = new(600, 450), - MaximumSize = new(750, 510) + MinimumSize = new(600, 465), + MaximumSize = new(750, 525) }; mediator.Subscribe(this, async _ => await RefreshSyncshells().ConfigureAwait(false)); @@ -143,11 +143,11 @@ namespace LightlessSync.UI _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "This lets other Lightless users know you use Lightless."); _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "While enabled, you and other people using Lightfinder can see each other identified as Lightless users."); - ImGui.Indent(5f); + ImGui.Indent(15f); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); ImGui.PopStyleColor(); - ImGui.Unindent(5f); + ImGui.Unindent(15f); ImGuiHelpers.ScaledDummy(3f); diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 170af20..7edbefc 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Globalization; +using System.Linq; using System.Numerics; using System.Reflection; @@ -85,7 +86,7 @@ public class CompactUi : WindowMediatorSubscriberBase IpcManager ipcManager, BroadcastService broadcastService, CharacterAnalyzer characterAnalyzer, - PlayerPerformanceConfigService playerPerformanceConfig) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; @@ -103,7 +104,7 @@ public class CompactUi : WindowMediatorSubscriberBase _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; _broadcastService = broadcastService; - _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService); + _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService); AllowPinning = true; AllowClickthrough = false; @@ -401,7 +402,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.TextUnformatted("No uploads in progress"); } - var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList(); + var currentDownloads = BuildCurrentDownloadSnapshot(); ImGui.AlignTextToFramePadding(); _uiSharedService.IconText(FontAwesomeIcon.Download); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); @@ -428,10 +429,53 @@ public class CompactUi : WindowMediatorSubscriberBase } } + + private List BuildCurrentDownloadSnapshot() + { + List snapshot = new(); + + foreach (var kvp in _currentDownloads.ToArray()) + { + var value = kvp.Value; + if (value == null || value.Count == 0) + continue; + + try + { + snapshot.AddRange(value.Values.ToArray()); + } + catch (System.ArgumentException) + { + // skibidi + } + } + + return snapshot; + } + private void DrawUIDHeader() { var uidText = GetUidText(); + Vector4? vanityTextColor = null; + Vector4? vanityGlowColor = null; + bool useVanityColors = false; + + if (_configService.Current.useColoredUIDs && _apiController.HasVanity) + { + if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex)) + { + vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex); + } + + if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex)) + { + vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex); + } + + useVanityColors = vanityTextColor is not null || vanityGlowColor is not null; + } + //Getting information of character and triangles threshold to show overlimit status in UID bar. _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); @@ -518,12 +562,30 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.SetCursorPosY(cursorY); ImGui.SetCursorPosX(uidStartX); + + bool headerItemClicked; using (_uiSharedService.UidFont.Push()) { - ImGui.TextColored(GetUidColor(), uidText); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(uidText); + if (useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor); + var cursorPos = ImGui.GetCursorScreenPos(); + var fontPtr = ImGui.GetFont(); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr); + } + else + { + ImGui.TextColored(GetUidColor(), uidText); + } } + + headerItemClicked = ImGui.IsItemClicked(); + + if (headerItemClicked) + { + ImGui.SetClipboardText(uidText); + } + UiSharedService.AttachToolTip("Click to copy"); if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected) @@ -583,7 +645,7 @@ public class CompactUi : WindowMediatorSubscriberBase if (_apiController.ServerState is ServerState.Connected) { - if (ImGui.IsItemClicked()) + if (headerItemClicked) { ImGui.SetClipboardText(_apiController.DisplayName); } @@ -592,9 +654,22 @@ public class CompactUi : WindowMediatorSubscriberBase { var origTextSize = ImGui.CalcTextSize(_apiController.UID); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2)); - ImGui.TextColored(GetUidColor(), _apiController.UID); + + if (useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor); + var cursorPos = ImGui.GetCursorScreenPos(); + var fontPtr = ImGui.GetFont(); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr); + } + else + { + ImGui.TextColored(GetUidColor(), _apiController.UID); + } + + bool uidFooterClicked = ImGui.IsItemClicked(); UiSharedService.AttachToolTip("Click to copy"); - if (ImGui.IsItemClicked()) + if (uidFooterClicked) { _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 52fcc13..8a9e6c9 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -5,10 +5,18 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; +using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using LightlessSync.WebAPI; +using Serilog; +using System; +using System.Collections.Generic; using System.Numerics; +using System.Reflection.Emit; +using System.Threading.Tasks; namespace LightlessSync.UI; @@ -19,18 +27,25 @@ public class TopTabMenu private readonly LightlessMediator _lightlessMediator; private readonly PairManager _pairManager; + private readonly PairRequestService _pairRequestService; + private readonly DalamudUtilService _dalamudUtilService; + private readonly HashSet _pendingPairRequestActions = new(StringComparer.Ordinal); + private bool _pairRequestsExpanded; // useless for now + private int _lastRequestCount; private readonly UiSharedService _uiSharedService; private string _filter = string.Empty; private int _globalControlCountdown = 0; - + private float _pairRequestsHeight = 150f; private string _pairToAdd = string.Empty; private SelectedTab _selectedTab = SelectedTab.None; - public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService) + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) { _lightlessMediator = lightlessMediator; _apiController = apiController; _pairManager = pairManager; + _pairRequestService = pairRequestService; + _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; } @@ -182,6 +197,22 @@ public class TopTabMenu } if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); + + #if DEBUG + if (ImGui.Button("Add Test Pair Request")) + { + var fakeCid = Guid.NewGuid().ToString("N"); + var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request"); + _lightlessMediator.Publish(new NotificationMessage( + "Pair request received (debug)", + display.Message, + NotificationType.Info, + TimeSpan.FromSeconds(5))); + } + #endif + + DrawIncomingPairRequests(availableWidth); + ImGui.Separator(); DrawFilter(availableWidth, spacing.X); @@ -205,6 +236,207 @@ public class TopTabMenu UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); } + private void DrawIncomingPairRequests(float availableWidth) + { + var requests = _pairRequestService.GetActiveRequests(); + var count = requests.Count; + if (count == 0) + { + _pairRequestsExpanded = false; + _lastRequestCount = 0; + return; + } + + if (count > _lastRequestCount) + { + _pairRequestsExpanded = true; + } + _lastRequestCount = count; + + var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader"; + + using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple"))) + using (ImRaii.PushColor(ImGuiCol.HeaderHovered, UIColors.Get("LightlessPurpleActive"))) + using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple"))) + { + bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen); + _pairRequestsExpanded = open; + + if (ImGui.IsItemHovered()) + UiSharedService.AttachToolTip("Expand to view incoming pair requests."); + + if (open) + { + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + //var desiredHeight = Math.Clamp(count * lineHeight * 2f, 130f * ImGuiHelpers.GlobalScale, 185f * ImGuiHelpers.GlobalScale); we use resize bar instead + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f); + + using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple"))) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f)) + { + if (ImGui.BeginChild("##IncomingPairRequestsOuter", new Vector2(availableWidth + 5f, _pairRequestsHeight), true)) + { + var defaultChildBg = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg]; + using (ImRaii.PushColor(ImGuiCol.ChildBg, defaultChildBg)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f)) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(6, 6))) + { + if (ImGui.BeginChild("##IncomingPairRequestsInner", new Vector2(0, 0), true)) + { + using (ImRaii.PushColor(ImGuiCol.TableBorderStrong, ImGui.GetStyle().Colors[(int)ImGuiCol.Border])) + using (ImRaii.PushColor(ImGuiCol.TableBorderLight, ImGui.GetStyle().Colors[(int)ImGuiCol.Border])) + { + DrawPairRequestList(requests); + } + } + ImGui.EndChild(); + } + } + ImGui.EndChild(); + + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple")); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive")); + ImGui.Button("##resizeHandle", new Vector2(availableWidth, 4f)); + ImGui.PopStyleColor(3); + + if (ImGui.IsItemActive()) + { + _pairRequestsHeight += ImGui.GetIO().MouseDelta.Y; + _pairRequestsHeight = Math.Clamp(_pairRequestsHeight, 100f, 300f); + } + } + } + } + } + + private void DrawPairRequestList(IReadOnlyList requests) + { + float playerColWidth = 207f * ImGuiHelpers.GlobalScale; + float receivedColWidth = 73f * ImGuiHelpers.GlobalScale; + float actionsColWidth = 50f * ImGuiHelpers.GlobalScale; + + ImGui.Separator(); + ImGui.TextUnformatted("Player"); + ImGui.SameLine(playerColWidth + 2f); + ImGui.TextUnformatted("Received"); + ImGui.SameLine(playerColWidth + receivedColWidth + 12f); + ImGui.TextUnformatted("Actions"); + ImGui.Separator(); + + foreach (var request in requests) + { + ImGui.BeginGroup(); + + var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; + + ImGui.TextUnformatted(label.Truncate(26)); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted(label); + ImGui.EndTooltip(); + } + + ImGui.SameLine(playerColWidth); + + ImGui.TextUnformatted(GetRelativeTime(request.ReceivedAt)); + ImGui.SameLine(playerColWidth + receivedColWidth); + + DrawPairRequestActions(request); + + ImGui.EndGroup(); + } + } + + private void DrawPairRequestActions(PairRequestService.PairRequestDisplay request) + { + using var id = ImRaii.PushId(request.HashedCid); + var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; + var inFlight = _pendingPairRequestActions.Contains(request.HashedCid); + using (ImRaii.Disabled(inFlight)) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Check)) + { + _ = AcceptPairRequestAsync(request); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Accept request"); + + ImGui.SameLine(); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Times)) + { + RejectPairRequest(request.HashedCid, label); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Decline request"); + } + } + + private static string GetRelativeTime(DateTime receivedAt) + { + var delta = DateTime.UtcNow - receivedAt; + if (delta <= TimeSpan.FromSeconds(10)) + { + return "Just now"; + } + + if (delta.TotalMinutes >= 1) + { + return $"{Math.Floor(delta.TotalMinutes)}m {delta.Seconds:D2}s ago"; + } + + return $"{delta.Seconds}s ago"; + } + + private async Task AcceptPairRequestAsync(PairRequestService.PairRequestDisplay request) + { + if (!_pendingPairRequestActions.Add(request.HashedCid)) + { + return; + } + + try + { + var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); + _pairRequestService.RemoveRequest(request.HashedCid); + + var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; + _lightlessMediator.Publish(new NotificationMessage( + "Pair request accepted", + $"Sent a pair request back to {display}.", + NotificationType.Info, + TimeSpan.FromSeconds(3))); + } + catch (Exception ex) + { + _lightlessMediator.Publish(new NotificationMessage( + "Failed to accept pair request", + ex.Message, + NotificationType.Error, + TimeSpan.FromSeconds(5))); + } + finally + { + _pendingPairRequestActions.Remove(request.HashedCid); + } + } + + private void RejectPairRequest(string hashedCid, string playerName) + { + if (!_pairRequestService.RemoveRequest(hashedCid)) + { + return; + } + + _lightlessMediator.Publish(new NotificationMessage("Pair request declined", "Declined " + playerName + "'s pending pair request.", + NotificationType.Info, + TimeSpan.FromSeconds(3))); + } + private void DrawFilter(float availableWidth, float spacingX) { var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Ban, "Clear"); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 457361a..263c87a 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -105,6 +105,21 @@ public partial class ApiController return Task.CompletedTask; } + public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) + { + if (dto == null) + return Task.CompletedTask; + + var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); + + Mediator.Publish(new NotificationMessage( + "Pair request received", + request.Message, + NotificationType.Info, + TimeSpan.FromSeconds(5))); + + return Task.CompletedTask; + } public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) { SystemInfoDto = systemInfo; @@ -277,6 +292,7 @@ public partial class ApiController _lightlessHub!.On(nameof(Client_GroupSendInfo), act); } + public void OnGroupUpdateProfile(Action act) { if (_initialized) return; @@ -289,6 +305,12 @@ public partial class ApiController _lightlessHub!.On(nameof(Client_ReceiveServerMessage), act); } + public void OnReceiveBroadcastPairRequest(Action act) + { + if (_initialized) return; + _lightlessHub!.On(nameof(Client_ReceiveBroadcastPairRequest), act); + } + public void OnUpdateSystemInfo(Action act) { if (_initialized) return; diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index efa6e6f..90be67f 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -28,6 +28,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private readonly DalamudUtilService _dalamudUtil; private readonly HubFactory _hubFactory; private readonly PairManager _pairManager; + private readonly PairRequestService _pairRequestService; private readonly ServerConfigurationManager _serverManager; private readonly TokenProvider _tokenProvider; private readonly LightlessConfigService _lightlessConfigService; @@ -42,12 +43,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private CensusUpdateMessage? _lastCensus; public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, - PairManager pairManager, ServerConfigurationManager serverManager, LightlessMediator mediator, + PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil; _pairManager = pairManager; + _pairRequestService = pairRequestService; _serverManager = serverManager; _tokenProvider = tokenProvider; _lightlessConfigService = lightlessConfigService; @@ -428,6 +430,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogDebug("Initializing data"); OnDownloadReady((guid) => _ = Client_DownloadReady(guid)); OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg)); + OnReceiveBroadcastPairRequest(dto => _ = Client_ReceiveBroadcastPairRequest(dto)); OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto)); OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto)); -- 2.49.1 From 87e6c0b3f6f35d4e02e85daad45b95cafa9e898a Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 4 Oct 2025 14:37:44 +0200 Subject: [PATCH 63/69] prevent null reference when checking FC tag color override for players without a FC --- LightlessSync/Services/NameplateService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 27fa06f..a30c21d 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -70,7 +70,8 @@ public class NameplateService : DisposableMediatorSubscriberBase { handler.NameParts.TextWrap = CreateTextWrap(colors); - if (_configService.Current.overrideFcTagColor) + if (_configService.Current.overrideFcTagColor && + playerCharacter.CompanyTag.TextValue.Length > 0) { handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors); handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors); -- 2.49.1 From 8dd13479fc1b2987a8bfab1e8e30e162b0bcf9ce Mon Sep 17 00:00:00 2001 From: azyges Date: Sun, 5 Oct 2025 03:28:02 +0900 Subject: [PATCH 64/69] few changes: - sending a successful pair request through context menu while a pending one exists in client clears it - adjustments to higlight coloring, preventing any text colors to blend with the highlight - some text adjustments - editing uid color in profile editor also previews the highlight --- LightlessSync/Plugin.cs | 6 +- LightlessSync/Services/ContextMenuService.cs | 9 ++- LightlessSync/UI/BroadcastUI.cs | 11 ++-- LightlessSync/UI/CompactUI.cs | 2 +- LightlessSync/UI/EditProfileUi.cs | 35 +++++++--- LightlessSync/UI/Handlers/IdDisplayHandler.cs | 34 +++++++--- LightlessSync/UI/Style/Luminance.cs | 64 +++++++++++++++++++ 7 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 LightlessSync/UI/Style/Luminance.cs diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index c3b1216..f6bdcb6 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; @@ -150,7 +150,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(addonLifecycle); collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData, p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, - p.GetRequiredService(), p.GetRequiredService(), clientState)); + p.GetRequiredService(), p.GetRequiredService(), p.GetRequiredService(), clientState)); collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService>(), pluginInterface, @@ -277,4 +277,4 @@ public sealed class Plugin : IDalamudPlugin _host.StopAsync().GetAwaiter().GetResult(); _host.Dispose(); } -} \ No newline at end of file +} diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 632a047..b03ef28 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin; using Dalamud.Plugin.Services; @@ -21,6 +21,7 @@ internal class ContextMenuService : IHostedService private readonly DalamudUtilService _dalamudUtil; private readonly IClientState _clientState; private readonly PairManager _pairManager; + private readonly PairRequestService _pairRequestService; private readonly ApiController _apiController; private readonly IObjectTable _objectTable; @@ -33,6 +34,7 @@ internal class ContextMenuService : IHostedService ApiController apiController, IObjectTable objectTable, LightlessConfigService configService, + PairRequestService pairRequestService, PairManager pairManager, IClientState clientState) { @@ -44,6 +46,7 @@ internal class ContextMenuService : IHostedService _apiController = apiController; _objectTable = objectTable; _pairManager = pairManager; + _pairRequestService = pairRequestService; _clientState = clientState; } @@ -140,6 +143,10 @@ internal class ContextMenuService : IHostedService _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(receiverCid)) + { + _pairRequestService.RemoveRequest(receiverCid); + } } catch (Exception ex) { diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index ae3d17a..b83e657 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -141,8 +141,8 @@ namespace LightlessSync.UI ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, -2)); - _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "This lets other Lightless users know you use Lightless."); - _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "While enabled, you and other people using Lightfinder can see each other identified as Lightless users."); + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"),"This lets other Lightless users know you use Lightless. While enabled, you and others using Lightfinder can see each other identified as Lightless users."); + ImGui.Indent(15f); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); @@ -183,15 +183,16 @@ namespace LightlessSync.UI new SeStringUtils.RichTextEntry(" and does not share any data with other users. All identifying information remains private to the server.")); _uiSharedService.DrawNoteLine("! ", UIColors.Get("DimRed"), "Pairing is intended as a mutual agreement between both parties. A pair request will not be visible to the recipient unless Lightfinder is enabled."); - ImGuiHelpers.ScaledDummy(3f); + + ImGuiHelpers.ScaledDummy(5f); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("Use Lightfinder only when you want to be visible."); + ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience."); ImGui.PopStyleColor(); ImGui.PopStyleVar(); - ImGuiHelpers.ScaledDummy(2.2f); + ImGuiHelpers.ScaledDummy(3f); _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (_configService.Current.BroadcastEnabled) diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 7edbefc..42311c6 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -512,7 +512,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.PopStyleColor(); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("Use it only when you want to be visible."); + ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience."); ImGui.PopStyleColor(); ImGuiHelpers.ScaledDummy(0.2f); diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 50c4dbe..72e8d33 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -9,6 +9,7 @@ using LightlessSync.API.Data; using LightlessSync.API.Dto.User; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; @@ -33,6 +34,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase private bool _showFileDialogError = false; private bool _wasOpen; + private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); private bool vanityInitialized; // useless for now private bool textEnabled; private bool glowEnabled; @@ -303,22 +305,37 @@ public class EditProfileUi : WindowMediatorSubscriberBase using (ImRaii.PushFont(font)) { - var offsetX = 10f; - var pos = ImGui.GetCursorScreenPos() + new Vector2(offsetX, 0); - var size = ImGui.CalcTextSize(seString.TextValue); + var drawList = ImGui.GetWindowDrawList(); + var textSize = ImGui.CalcTextSize(seString.TextValue); - var padding = new Vector2(6, 3); - var rectMin = pos - padding; - var rectMax = pos + size + padding; + float minWidth = 150f * ImGuiHelpers.GlobalScale; + float bgWidth = Math.Max(textSize.X + 20f, minWidth); + + float paddingY = 5f * ImGuiHelpers.GlobalScale; + + var cursor = ImGui.GetCursorScreenPos(); + + var rectMin = cursor; + var rectMax = rectMin + new Vector2(bgWidth, textSize.Y + (paddingY * 2f)); + + float boost = Luminance.ComputeHighlight(previewTextColor, previewGlowColor); + + var baseBg = new Vector4(0.15f + boost, 0.15f + boost, 0.15f + boost, 1f); + var bgColor = Luminance.BackgroundContrast(previewTextColor, previewGlowColor, baseBg, ref _currentBg); - var bgColor = new Vector4(0.15f, 0.15f, 0.15f, 1f); var borderColor = UIColors.Get("LightlessPurple"); - var drawList = ImGui.GetWindowDrawList(); drawList.AddRectFilled(rectMin, rectMax, ImGui.GetColorU32(bgColor), 6.0f); drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 6.0f, ImDrawFlags.None, 1.5f); - SeStringUtils.RenderSeStringWithHitbox(seString, pos, font); + var textPos = new Vector2( + rectMin.X + (bgWidth - textSize.X) * 0.5f, + rectMin.Y + paddingY + ); + + SeStringUtils.RenderSeStringWithHitbox(seString, textPos, font); + + ImGui.Dummy(new Vector2(5)); } const float colorPickAlign = 90f; diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 1e3fee2..01f0df6 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -7,6 +7,7 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Style; using LightlessSync.Utils; using System; using System.Numerics; @@ -26,6 +27,9 @@ public class IdDisplayHandler private bool _popupShown = false; private DateTime? _popupTime; + private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); + private float _highlightBoost; + public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService) { _mediator = mediator; @@ -98,7 +102,7 @@ public class IdDisplayHandler { ImGui.AlignTextToFramePadding(); - var font = UiBuilder.MonoFont; + var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont(); Vector4? textColor = null; Vector4? glowColor = null; @@ -127,10 +131,11 @@ public class IdDisplayHandler float highlightPadX = 0f; float highlightPadY = 0f; - if (useVanityColors && textColor is Vector4 contrastColor) + if (useVanityColors) { - var brightness = (0.299f * contrastColor.X) + (0.587f * contrastColor.Y) + (0.114f * contrastColor.Z); - if (brightness < 0.35f) + float boost = Luminance.ComputeHighlight(textColor, glowColor); + + if (boost > 0f) { var style = ImGui.GetStyle(); useHighlight = true; @@ -138,6 +143,12 @@ public class IdDisplayHandler highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale); drawList.ChannelsSplit(2); drawList.ChannelsSetCurrent(1); + + _highlightBoost = boost; + } + else + { + _highlightBoost = 0f; } } @@ -149,7 +160,7 @@ public class IdDisplayHandler SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font); itemMin = ImGui.GetItemRectMin(); itemMax = ImGui.GetItemRectMax(); - textSize = itemMax - itemMin; + //textSize = itemMax - itemMin; } if (useHighlight) @@ -170,11 +181,14 @@ public class IdDisplayHandler highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y); highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y); - var highlightColor = style.Colors[(int)ImGuiCol.TableRowBgAlt]; - highlightColor.X = 0.25f; - highlightColor.Y = 0.25f; - highlightColor.Z = 0.25f; - highlightColor.W = 1f; + var highlightColor = new Vector4( + 0.25f + _highlightBoost, + 0.25f + _highlightBoost, + 0.25f + _highlightBoost, + 1f + ); + + highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg); float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale; drawList.ChannelsSetCurrent(0); diff --git a/LightlessSync/UI/Style/Luminance.cs b/LightlessSync/UI/Style/Luminance.cs new file mode 100644 index 0000000..62609af --- /dev/null +++ b/LightlessSync/UI/Style/Luminance.cs @@ -0,0 +1,64 @@ +using System; +using System.Numerics; + +namespace LightlessSync.UI.Style +{ + internal static class Luminance + { + public static float BrightnessThreshold { get; set; } = 0.4f; + public static float HighlightBoostMax { get; set; } = 0.1f; + public static float SmoothFactor { get; set; } = 0.15f; + + private static float Brightness(Vector4 color) + => Math.Max(color.X, Math.Max(color.Y, color.Z)); + + public static float ComputeHighlight(Vector4? textColor, Vector4? glowColor) + { + float brightnessText = textColor.HasValue ? Brightness(textColor.Value) : 1f; + float brightnessGlow = glowColor.HasValue ? Brightness(glowColor.Value) : 1f; + + if (brightnessText >= BrightnessThreshold || brightnessGlow >= BrightnessThreshold) + return 0f; + + float deficit = Math.Min(BrightnessThreshold - brightnessText, + BrightnessThreshold - brightnessGlow); + + float factor = Math.Clamp(deficit / BrightnessThreshold, 0f, 1f); + factor = MathF.Pow(factor, 2.0f); + + return factor * HighlightBoostMax; + } + + public static Vector4 BackgroundContrast(Vector4? textColor, Vector4? glowColor, Vector4 backgroundColor, ref Vector4 currentBg) + { + if (!textColor.HasValue && !glowColor.HasValue) + return backgroundColor; + + float brightnessText = textColor.HasValue ? Brightness(textColor.Value) : 0f; + float brightnessGlow = glowColor.HasValue ? Brightness(glowColor.Value) : 0f; + + float fgBrightness = Math.Max(brightnessText, brightnessGlow); + float bgBrightness = Brightness(backgroundColor); + float diff = Math.Abs(bgBrightness - fgBrightness); + + bool shouldBeDark = fgBrightness > 0.5f; + Vector4 targetBg; + + if (diff >= BrightnessThreshold) + { + targetBg = backgroundColor; + } + else + { + targetBg = shouldBeDark + ? new Vector4(0.05f, 0.05f, 0.05f, backgroundColor.W) + : new Vector4(0.95f, 0.95f, 0.95f, backgroundColor.W); + } + + float t = Math.Clamp(SmoothFactor, 0f, 1f); + currentBg = t <= 0f ? targetBg : Vector4.Lerp(currentBg, targetBg, t); + + return currentBg; + } + } +} -- 2.49.1 From 78455f7523508820e950ad3b665ff6815fb8bc93 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 4 Oct 2025 14:16:47 -0500 Subject: [PATCH 65/69] Whitelist Kdb --- LightlessSync/FileCache/CacheMonitor.cs | 2 +- LightlessSync/FileCache/TransientResourceManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index b03f311..3b41d85 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -20,7 +20,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private long _currentFileProgress = 0; private CancellationTokenSource _scanCancellationTokenSource = new(); private readonly CancellationTokenSource _periodicCalculationTokenSource = new(); - public static readonly IImmutableList AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"]; + public static readonly IImmutableList AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"]; public CacheMonitor(ILogger logger, IpcManager ipcManager, LightlessConfigService configService, FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil, diff --git a/LightlessSync/FileCache/TransientResourceManager.cs b/LightlessSync/FileCache/TransientResourceManager.cs index 4359bdc..6a9575a 100644 --- a/LightlessSync/FileCache/TransientResourceManager.cs +++ b/LightlessSync/FileCache/TransientResourceManager.cs @@ -17,7 +17,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private readonly HashSet _cachedHandledPaths = new(StringComparer.Ordinal); private readonly TransientConfigService _configurationService; private readonly DalamudUtilService _dalamudUtil; - private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"]; + private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"]; private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"]; private readonly HashSet _playerRelatedPointers = []; private ConcurrentDictionary _cachedFrameAddresses = []; -- 2.49.1 From f037e7587d1b9b8e6ec0c50f101e3f7e7b12d32e Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 4 Oct 2025 14:19:18 -0500 Subject: [PATCH 66/69] update penumbra pointer --- PenumbraAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PenumbraAPI b/PenumbraAPI index dd14131..648b6fc 160000 --- a/PenumbraAPI +++ b/PenumbraAPI @@ -1 +1 @@ -Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa +Subproject commit 648b6fc2ce600a95ab2b2ced27e1639af2b04502 -- 2.49.1 From dfb2e948c539ba61c58632e694ebbc33f63241a9 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 4 Oct 2025 14:29:56 -0500 Subject: [PATCH 67/69] Update LightlessAPI pointer --- LightlessAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessAPI b/LightlessAPI index 6c542c0..167508d 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 6c542c0ccca0327896ef895f9de02a76869ea311 +Subproject commit 167508d27b754347554797fa769c5feb3f91552e -- 2.49.1 From 0e274f04f183cbd0672472b12d870d87587b0c8a Mon Sep 17 00:00:00 2001 From: choco Date: Sat, 4 Oct 2025 21:55:42 +0200 Subject: [PATCH 68/69] apply nameplate colors to cross-world players without FC tags --- LightlessSync/Services/NameplateService.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index a30c21d..f441a60 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -70,11 +70,17 @@ public class NameplateService : DisposableMediatorSubscriberBase { handler.NameParts.TextWrap = CreateTextWrap(colors); - if (_configService.Current.overrideFcTagColor && - playerCharacter.CompanyTag.TextValue.Length > 0) + if (_configService.Current.overrideFcTagColor) { - handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors); - handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors); + bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0; + bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId; + bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm); + + if (shouldColorFcArea) + { + handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors); + handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors); + } } } } -- 2.49.1 From e4931ded9f13b85a33af67c11c0f0c25ccae90f3 Mon Sep 17 00:00:00 2001 From: defnotken Date: Sat, 4 Oct 2025 22:04:19 -0500 Subject: [PATCH 69/69] 1.12.0 not 3 --- LightlessSync/LightlessSync.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 5b31c88..344e968 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 1.12.3 + 1.12.0 https://github.com/Light-Public-Syncshells/LightlessClient -- 2.49.1