From deb99628f6a95a698821d270a87213afaf4888ea Mon Sep 17 00:00:00 2001 From: cake Date: Sun, 28 Dec 2025 03:01:02 +0100 Subject: [PATCH 1/3] Added debug mode for lightfinder IMGUI, added caching of file cache entries to reduce load of loading all entries again. --- LightlessSync/FileCache/FileCacheEntity.cs | 16 +- LightlessSync/FileCache/FileCacheManager.cs | 151 ++++- LightlessSync/Plugin.cs | 3 +- LightlessSync/Services/CharacterAnalyzer.cs | 157 +++-- .../LightFinder/LightFinderPlateHandler.cs | 570 ++++++++++++------ LightlessSync/UI/DataAnalysisUi.cs | 19 +- LightlessSync/UI/LightFinderUI.cs | 45 +- LightlessSync/UI/SettingsUi.cs | 6 +- 8 files changed, 723 insertions(+), 244 deletions(-) diff --git a/LightlessSync/FileCache/FileCacheEntity.cs b/LightlessSync/FileCache/FileCacheEntity.cs index 418e3d4..9d0515d 100644 --- a/LightlessSync/FileCache/FileCacheEntity.cs +++ b/LightlessSync/FileCache/FileCacheEntity.cs @@ -1,15 +1,23 @@ #nullable disable +using System.Text.Json.Serialization; + namespace LightlessSync.FileCache; public class FileCacheEntity { - public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null) + [JsonConstructor] + public FileCacheEntity( + string hash, + string prefixedFilePath, + string lastModifiedDateTicks, + long? size = null, + long? compressedSize = null) { Size = size; CompressedSize = compressedSize; Hash = hash; - PrefixedFilePath = path; + PrefixedFilePath = prefixedFilePath; LastModifiedDateTicks = lastModifiedDateTicks; } @@ -23,7 +31,5 @@ public class FileCacheEntity public long? Size { get; set; } public void SetResolvedFilePath(string filePath) - { - ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal); - } + => ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal); } \ No newline at end of file diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index e2cdc72..b0becf3 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; using System.Text; namespace LightlessSync.FileCache; @@ -31,6 +33,14 @@ public sealed class FileCacheManager : IHostedService private bool _csvHeaderEnsured; public string CacheFolder => _configService.Current.CacheFolder; + private const string _compressedCacheExtension = ".llz4"; + private readonly ConcurrentDictionary _compressLocks = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _sizeCache = + new(StringComparer.OrdinalIgnoreCase); + + [StructLayout(LayoutKind.Auto)] + public readonly record struct SizeInfo(long Original, long Compressed); + public FileCacheManager(ILogger logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator) { _logger = logger; @@ -45,6 +55,18 @@ public sealed class FileCacheManager : IHostedService private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal) .Replace("\\\\", "\\", StringComparison.Ordinal); + private SemaphoreSlim GetCompressLock(string hash) + => _compressLocks.GetOrAdd(hash, _ => new SemaphoreSlim(1, 1)); + + public void SetSizeInfo(string hash, long original, long compressed) + => _sizeCache[hash] = new SizeInfo(original, compressed); + + public bool TryGetSizeInfo(string hash, out SizeInfo info) + => _sizeCache.TryGetValue(hash, out info); + + private string GetCompressedCachePath(string hash) + => Path.Combine(CacheFolder, hash + _compressedCacheExtension); + private static string NormalizePrefixedPathKey(string prefixedPath) { if (string.IsNullOrEmpty(prefixedPath)) @@ -111,6 +133,114 @@ public sealed class FileCacheManager : IHostedService return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version); } + public void UpdateSizeInfo(string hash, long? original = null, long? compressed = null) + { + _sizeCache.AddOrUpdate( + hash, + _ => new SizeInfo(original ?? 0, compressed ?? 0), + (_, old) => new SizeInfo(original ?? old.Original, compressed ?? old.Compressed)); + } + + private void UpdateEntitiesSizes(string hash, long original, long compressed) + { + if (_fileCaches.TryGetValue(hash, out var dict)) + { + foreach (var e in dict.Values) + { + e.Size = original; + e.CompressedSize = compressed; + } + } + } + + public static void ApplySizesToEntries(IEnumerable entries, long original, long compressed) + { + foreach (var e in entries) + { + if (e == null) continue; + e.Size = original; + e.CompressedSize = compressed > 0 ? compressed : null; + } + } + + public async Task GetCompressedSizeAsync(string hash, CancellationToken token) + { + if (_sizeCache.TryGetValue(hash, out var info) && info.Compressed > 0) + return info.Compressed; + + if (_fileCaches.TryGetValue(hash, out var dict)) + { + var any = dict.Values.FirstOrDefault(); + if (any != null && any.CompressedSize > 0) + { + UpdateSizeInfo(hash, original: any.Size > 0 ? any.Size : null, compressed: any.CompressedSize); + return (long)any.CompressedSize; + } + } + + if (!string.IsNullOrWhiteSpace(CacheFolder)) + { + var path = GetCompressedCachePath(hash); + if (File.Exists(path)) + { + var len = new FileInfo(path).Length; + UpdateSizeInfo(hash, compressed: len); + return len; + } + + var bytes = await EnsureCompressedCacheBytesAsync(hash, token).ConfigureAwait(false); + return bytes.LongLength; + } + + var fallback = await GetCompressedFileData(hash, token).ConfigureAwait(false); + return fallback.Item2.LongLength; + } + + private async Task EnsureCompressedCacheBytesAsync(string hash, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(CacheFolder)) + throw new InvalidOperationException("CacheFolder is not set; cannot persist compressed cache."); + + Directory.CreateDirectory(CacheFolder); + + var compressedPath = GetCompressedCachePath(hash); + + if (File.Exists(compressedPath)) + return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false); + + var sem = GetCompressLock(hash); + await sem.WaitAsync(token).ConfigureAwait(false); + try + { + if (File.Exists(compressedPath)) + return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false); + + var entity = GetFileCacheByHash(hash); + if (entity == null || string.IsNullOrWhiteSpace(entity.ResolvedFilepath)) + throw new InvalidOperationException($"No local file cache found for hash {hash}."); + + var sourcePath = entity.ResolvedFilepath; + var originalSize = new FileInfo(sourcePath).Length; + + var raw = await File.ReadAllBytesAsync(sourcePath, token).ConfigureAwait(false); + var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length); + + var tmpPath = compressedPath + ".tmp"; + await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false); + File.Move(tmpPath, compressedPath, overwrite: true); + + var compressedSize = compressed.LongLength; + SetSizeInfo(hash, originalSize, compressedSize); + UpdateEntitiesSizes(hash, originalSize, compressedSize); + + return compressed; + } + finally + { + sem.Release(); + } + } + private string NormalizeToPrefixedPath(string path) { if (string.IsNullOrEmpty(path)) return string.Empty; @@ -318,9 +448,18 @@ public sealed class FileCacheManager : IHostedService public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) { + if (!string.IsNullOrWhiteSpace(CacheFolder)) + { + var bytes = await EnsureCompressedCacheBytesAsync(fileHash, uploadToken).ConfigureAwait(false); + UpdateSizeInfo(fileHash, compressed: bytes.LongLength); + return (fileHash, bytes); + } + var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath; - return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, - (int)new FileInfo(fileCache).Length)); + var raw = await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false); + var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length); + UpdateSizeInfo(fileHash, original: raw.LongLength, compressed: compressed.LongLength); + return (fileHash, compressed); } public FileCacheEntity? GetFileCacheByHash(string hash) @@ -891,6 +1030,14 @@ public sealed class FileCacheManager : IHostedService compressed = resultCompressed; } } + + if (size > 0 || compressed > 0) + { + UpdateSizeInfo(hash, + original: size > 0 ? size : null, + compressed: compressed > 0 ? compressed : null); + } + AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed))); } catch (Exception ex) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index ded2e8e..a38b924 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -478,7 +478,8 @@ public sealed class Plugin : IDalamudPlugin sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService())); + sp.GetRequiredService(), + sp.GetRequiredService())); services.AddScoped(sp => new SyncshellFinderUI( sp.GetRequiredService>(), diff --git a/LightlessSync/Services/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index 3eebced..959ece3 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.FileCache; @@ -98,11 +99,13 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _analysisCts = null; if (print) PrintAnalysis(); } + public void Dispose() { _analysisCts.CancelDispose(); _baseAnalysisCts.Dispose(); } + public async Task UpdateFileEntriesAsync(IEnumerable filePaths, CancellationToken token) { var normalized = new HashSet( @@ -125,6 +128,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable } } } + private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) { if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return; @@ -136,29 +140,47 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { token.ThrowIfCancellationRequested(); - var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList(); - if (fileCacheEntries.Count == 0) continue; - var filePath = fileCacheEntries[0].ResolvedFilepath; - FileInfo fi = new(filePath); - string ext = "unk?"; - try - { - ext = fi.Extension[1..]; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Could not identify extension for {path}", filePath); - } + var fileCacheEntries = (await _fileCacheManager + .GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token) + .ConfigureAwait(false)) + .ToList(); + + if (fileCacheEntries.Count == 0) + continue; + + var resolved = fileCacheEntries[0].ResolvedFilepath; + + var extWithDot = Path.GetExtension(resolved); + var ext = string.IsNullOrEmpty(extWithDot) ? "unk?" : extWithDot.TrimStart('.'); + var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false); - foreach (var entry in fileCacheEntries) + + var distinctFilePaths = fileCacheEntries + .Select(c => c.ResolvedFilepath) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + long orig = 0, comp = 0; + var first = fileCacheEntries[0]; + if (first.Size > 0) orig = first.Size.Value; + if (first.CompressedSize > 0) comp = first.CompressedSize.Value; + + if (_fileCacheManager.TryGetSizeInfo(fileEntry.Hash, out var cached)) { - data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, - [.. fileEntry.GamePaths], - [.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)], - entry.Size > 0 ? entry.Size.Value : 0, - entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0, - tris); + if (orig <= 0 && cached.Original > 0) orig = cached.Original; + if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed; } + + data[fileEntry.Hash] = new FileDataEntry( + fileEntry.Hash, + ext, + [.. fileEntry.GamePaths], + distinctFilePaths, + orig, + comp, + tris, + fileCacheEntries); } LastAnalysis[obj.Key] = data; } @@ -167,6 +189,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable Mediator.Publish(new CharacterDataAnalyzedMessage()); _lastDataHash = charaData.DataHash.Value; } + private void RecalculateSummary() { var builder = ImmutableDictionary.CreateBuilder(); @@ -192,6 +215,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _latestSummary = new CharacterAnalysisSummary(builder.ToImmutable()); } + private void PrintAnalysis() { if (LastAnalysis.Count == 0) return; @@ -235,42 +259,79 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize)))); Logger.LogInformation("IMPORTANT NOTES:\n\r- For Lightless up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly."); } - internal sealed record FileDataEntry(string Hash, string FileType, List GamePaths, List FilePaths, long OriginalSize, long CompressedSize, long Triangles) - { - public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; - public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token) - { - var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); - var normalSize = new FileInfo(FilePaths[0]).Length; - var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false); - foreach (var entry in entries) - { - entry.Size = normalSize; - entry.CompressedSize = compressedsize.Item2.LongLength; - } - OriginalSize = normalSize; - CompressedSize = compressedsize.Item2.LongLength; - RefreshFormat(); - } - public long OriginalSize { get; private set; } = OriginalSize; - public long CompressedSize { get; private set; } = CompressedSize; - public long Triangles { get; private set; } = Triangles; - public Lazy Format => _format ??= CreateFormatValue(); + internal sealed class FileDataEntry + { + public string Hash { get; } + public string FileType { get; } + public List GamePaths { get; } + public List FilePaths { get; } + + public long OriginalSize { get; private set; } + public long CompressedSize { get; private set; } + public long Triangles { get; private set; } + + public IReadOnlyList CacheEntries { get; } + + public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; + + public FileDataEntry( + string hash, + string fileType, + List gamePaths, + List filePaths, + long originalSize, + long compressedSize, + long triangles, + IReadOnlyList cacheEntries) + { + Hash = hash; + FileType = fileType; + GamePaths = gamePaths; + FilePaths = filePaths; + OriginalSize = originalSize; + CompressedSize = compressedSize; + Triangles = triangles; + CacheEntries = cacheEntries; + } + + public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false) + { + if (!force && IsComputed) + return; + + if (FilePaths.Count == 0 || string.IsNullOrWhiteSpace(FilePaths[0])) + return; + + var path = FilePaths[0]; + + if (!File.Exists(path)) + return; + + var original = new FileInfo(path).Length; + + var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false); + + fileCacheManager.SetSizeInfo(Hash, original, compressedLen); + FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen); + + OriginalSize = original; + CompressedSize = compressedLen; + + if (string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase)) + RefreshFormat(); + } + + public Lazy Format => _format ??= CreateFormatValue(); private Lazy? _format; - public void RefreshFormat() - { - _format = CreateFormatValue(); - } + public void RefreshFormat() => _format = CreateFormatValue(); private Lazy CreateFormatValue() => new(() => { - if (!string.Equals(FileType, "tex", StringComparison.Ordinal)) - { + if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase)) return string.Empty; - } try { diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs index 7449d74..e11fd5c 100644 --- a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -23,6 +23,7 @@ using Pictomancy; using System.Collections.Immutable; using System.Globalization; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Task = System.Threading.Tasks.Task; @@ -41,6 +42,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe private readonly LightlessConfigService _configService; private readonly PairUiService _pairUiService; private readonly LightlessMediator _mediator; + public LightlessMediator Mediator => _mediator; private readonly IUiBuilder _uiBuilder; @@ -61,16 +63,30 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe // / Overlay window flags private const ImGuiWindowFlags _overlayFlags = - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoBackground | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoSavedSettings | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoInputs; + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoBackground | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoInputs; private readonly List _uiRects = new(128); private ImmutableHashSet _activeBroadcastingCids = []; +#if DEBUG + // Debug controls + private bool _debugEnabled; + private bool _debugDisableOcclusion; + private bool _debugDrawUiRects; + private bool _debugDrawLabelRects = true; + + // Debug counters (read-only from UI) + private int _debugLabelCountLastFrame; + private int _debugUiRectCountLastFrame; + private int _debugOccludedCountLastFrame; + private uint _debugLastNameplateFrame; +#endif + private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy; public LightFinderPlateHandler( @@ -96,7 +112,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe _uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface)); _ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService)); _lastRenderer = _configService.Current.LightfinderLabelRenderer; - } private void RefreshRendererState() @@ -187,8 +202,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Draw detour for nameplate addon. /// - /// - /// private void NameplateDrawDetour(AddonEvent type, AddonArgs args) { RefreshRendererState(); @@ -199,6 +212,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return; } + // Hide our overlay when the user hides the entire game UI (ScrollLock). + if (_gameGui.GameUiHidden) + { + ClearLabelBuffer(); + Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); + _lastNamePlateDrawFrame = 0; + return; + } + + // gpose: do not draw. if (_clientState.IsGPosing) { ClearLabelBuffer(); @@ -218,6 +241,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (fw != null) _lastNamePlateDrawFrame = fw->FrameCounter; +#if DEBUG + DebugLastNameplateFrame = _lastNamePlateDrawFrame; +#endif + var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; if (_mpNameplateAddon != pNameplateAddon) @@ -234,6 +261,13 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// private void UpdateNameplateNodes() { + // If the user has hidden the UI, don't compute any labels. + if (_gameGui.GameUiHidden) + { + ClearLabelBuffer(); + return; + } + var currentHandle = _gameGui.GetAddonByName("NamePlate"); if (currentHandle.Address == nint.Zero) { @@ -297,7 +331,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe for (int i = 0; i < safeCount; ++i) { - var objectInfoPtr = vec[i]; if (objectInfoPtr == null) continue; @@ -314,7 +347,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) continue; - // CID gating + // CID gating - only show for active broadcasters var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); if (cid == null || !_activeBroadcastingCids.Contains(cid)) continue; @@ -350,12 +383,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible) continue; - // Prepare label content and scaling - var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f); + // Prepare label content and scaling factors + var scaleMultiplier = Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f); var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f; var effectiveScale = baseScale * scaleMultiplier; var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f; - var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); + var targetFontSize = (int)Math.Round(baseFontSize * scaleMultiplier); var labelContent = currentConfig.LightfinderLabelUseIcon ? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph) : _defaultLabelText; @@ -363,8 +396,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) labelContent = _defaultLabelText; - var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); + var nodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); + var nodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); AlignmentType alignment; var textScaleY = nameText->AtkResNode.ScaleY; @@ -374,7 +407,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe var blockHeight = ResolveCache( _buffers.TextHeights, nameplateIndex, - System.Math.Abs((int)nameplateObject.TextH), + Math.Abs((int)nameplateObject.TextH), () => GetScaledTextHeight(nameText), nodeHeight); @@ -384,7 +417,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe (int)nameContainer->Height, () => { - var computed = blockHeight + (int)System.Math.Round(8 * textScaleY); + var computed = blockHeight + (int)Math.Round(8 * textScaleY); return computed <= blockHeight ? blockHeight + 1 : computed; }, blockHeight + 1); @@ -392,7 +425,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe var blockTop = containerHeight - blockHeight; if (blockTop < 0) blockTop = 0; - var verticalPadding = (int)System.Math.Round(4 * effectiveScale); + var verticalPadding = (int)Math.Round(4 * effectiveScale); var positionY = blockTop - verticalPadding; @@ -400,21 +433,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe var textWidth = ResolveCache( _buffers.TextWidths, nameplateIndex, - System.Math.Abs(rawTextWidth), + Math.Abs(rawTextWidth), () => GetScaledTextWidth(nameText), nodeWidth); // Text offset caching - var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); + var textOffset = (int)Math.Round(nameText->AtkResNode.X); var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset); - if (nameContainer == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex); - continue; - } - var res = nameContainer; // X scale @@ -450,7 +476,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX; - // alignment based on config + // alignment based on config setting switch (currentConfig.LabelAlignment) { case LabelAlignment.Left: @@ -469,7 +495,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe } else { - // manual X positioning + // manual X positioning with optional cached offset var cachedTextOffset = _buffers.TextOffsets[nameplateIndex]; var hasCachedOffset = cachedTextOffset != int.MinValue; var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) @@ -489,16 +515,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe // final position before smoothing var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen); - var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y + var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; var fw = Framework.Instance(); float dt = fw->RealFrameDeltaTime; - //smoothing.. + //smoothing.. snap.. smooth.. snap finalPosition = SnapToPixels(finalPosition, dpiScale); finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt); finalPosition = SnapToPixels(finalPosition, dpiScale); - // prepare label info + // prepare label info for rendering var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon) ? AlignmentToPivot(alignment) : _defaultPivot; @@ -545,7 +571,23 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (fw == null) return; - // Frame skip check + // If UI is hidden, do not render. + if (_gameGui.GameUiHidden) + { + ClearLabelBuffer(); + Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); + _lastNamePlateDrawFrame = 0; + +#if DEBUG + DebugLabelCountLastFrame = 0; + DebugUiRectCountLastFrame = 0; + DebugOccludedCountLastFrame = 0; + DebugLastNameplateFrame = 0; +#endif + return; + } + + // Frame skip check - skip if more than 1 frame has passed since last nameplate draw. var frame = fw->FrameCounter; if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1) @@ -553,34 +595,62 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe ClearLabelBuffer(); Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); +#if DEBUG + DebugLabelCountLastFrame = 0; + DebugUiRectCountLastFrame = 0; + DebugOccludedCountLastFrame = 0; + DebugLastNameplateFrame = _lastNamePlateDrawFrame; +#endif return; } - //Gpose Check + // Gpose Check - do not render. if (_clientState.IsGPosing) { ClearLabelBuffer(); Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); _lastNamePlateDrawFrame = 0; + +#if DEBUG + DebugLabelCountLastFrame = 0; + DebugUiRectCountLastFrame = 0; + DebugOccludedCountLastFrame = 0; + DebugLastNameplateFrame = 0; +#endif return; } - // If nameplate addon is not visible, skip rendering + // If nameplate addon is not visible, skip rendering entirely. if (!IsNamePlateAddonVisible()) + { +#if DEBUG + DebugLabelCountLastFrame = 0; + DebugUiRectCountLastFrame = 0; + DebugOccludedCountLastFrame = 0; + DebugLastNameplateFrame = _lastNamePlateDrawFrame; +#endif return; + } int copyCount; lock (_labelLock) { copyCount = _labelRenderCount; if (copyCount == 0) + { +#if DEBUG + DebugLabelCountLastFrame = 0; + DebugUiRectCountLastFrame = 0; + DebugOccludedCountLastFrame = 0; + DebugLastNameplateFrame = _lastNamePlateDrawFrame; +#endif return; + } Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount); } - var uiModule = fw != null ? fw->GetUIModule() : null; - + var uiModule = fw->GetUIModule(); if (uiModule != null) { var rapture = uiModule->GetRaptureAtkModule(); @@ -599,7 +669,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe var vpPos = vp.Pos; ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos(vp.Pos); ImGui.SetNextWindowSize(vp.Size); @@ -610,54 +679,121 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe ImGui.PopStyleVar(2); - using var drawList = PictoService.Draw(); - if (drawList == null) + // --- Debug settings (wired via handler fields; no hotkey / no extra debug window here) --- + bool dbgEnabled = false; + bool dbgDisableOcc = false; + bool dbgDrawUiRects = false; + bool dbgDrawLabelRects = false; +#if DEBUG + dbgEnabled = DebugEnabled; + dbgDisableOcc = DebugDisableOcclusion; + dbgDrawUiRects = DebugDrawUiRects; + dbgDrawLabelRects = DebugDrawLabelRects; +#endif + + int occludedThisFrame = 0; + + try + { + using var drawList = PictoService.Draw(); + if (drawList == null) + return; + + // Debug drawing uses the window drawlist (so it always draws in the correct viewport). + var dbgDl = ImGui.GetWindowDrawList(); + var useViewportOffset = ImGui.GetIO().ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable); + + for (int i = 0; i < copyCount; ++i) + { + ref var info = ref _buffers.LabelCopy[i]; + + // final draw position with viewport offset (only when viewports are enabled) + var drawPos = info.ScreenPosition; + if (useViewportOffset) + drawPos += vpPos; + + var font = default(ImFontPtr); + if (info.UseIcon) + { + var ioFonts = ImGui.GetIO().Fonts; + font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont(); + } + else + { + font = ImGui.GetFont(); + } + + if (!font.IsNull) + ImGui.PushFont(font); + + // calculate size for occlusion checking + var baseSize = ImGui.CalcTextSize(info.Text); + var baseFontSize = ImGui.GetFontSize(); + + if (!font.IsNull) + ImGui.PopFont(); + + // scale size based on font size + var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f; + var size = baseSize * scale; + + // label rect for occlusion checking (in game screen coords, NOT viewport-pos-adjusted) + var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y); + var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y); + + // "Would this be occluded?" (we track this even if we force-draw) + bool wouldOcclude = IsOccludedByAnyUi(labelRect); + if (wouldOcclude) + occludedThisFrame++; + + // Debug: draw label rects + if (dbgEnabled && dbgDrawLabelRects) + { + var tl = new Vector2(labelRect.L, labelRect.T); + var br = new Vector2(labelRect.R, labelRect.B); + + if (useViewportOffset) { tl += vpPos; br += vpPos; } + + // green = visible, red = would be occluded (even if forced) + var col = wouldOcclude + ? ImGui.GetColorU32(new Vector4(1f, 0f, 0f, 0.6f)) + : ImGui.GetColorU32(new Vector4(0f, 1f, 0f, 0.6f)); + + dbgDl.AddRect(tl, br, col); + } + + // occlusion check (allow debug to disable) + if (!dbgDisableOcc && wouldOcclude) + continue; + + drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font); + } + + // Debug: draw UI rects (occluders) + if (dbgEnabled && dbgDrawUiRects && _uiRects.Count > 0) + { + var useOff = useViewportOffset ? vpPos : Vector2.Zero; + var col = ImGui.GetColorU32(new Vector4(1f, 1f, 1f, 0.35f)); + + for (int i = 0; i < _uiRects.Count; i++) + { + var r = _uiRects[i]; + dbgDl.AddRect(new Vector2(r.L, r.T) + useOff, new Vector2(r.R, r.B) + useOff, col); + } + } + } + finally { ImGui.End(); - return; } - for (int i = 0; i < copyCount; ++i) - { - ref var info = ref _buffers.LabelCopy[i]; - - // final draw position with viewport offset - var drawPos = info.ScreenPosition + vpPos; - var font = default(ImFontPtr); - if (info.UseIcon) - { - var ioFonts = ImGui.GetIO().Fonts; - font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont(); - } - else - { - font = ImGui.GetFont(); - } - - if (!font.IsNull) - ImGui.PushFont(font); - - // calculate size for occlusion checking - var baseSize = ImGui.CalcTextSize(info.Text); - var baseFontSize = ImGui.GetFontSize(); - - if (!font.IsNull) - ImGui.PopFont(); - - // scale size based on font size - var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f; - var size = baseSize * scale; - - // label rect for occlusion checking - var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y); - var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y); - - // occlusion check - if (IsOccludedByAnyUi(labelRect)) - continue; - - drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font); - } +#if DEBUG + // --- Publish per-frame debug counters for the UI Debug tab --- + DebugLabelCountLastFrame = copyCount; + DebugUiRectCountLastFrame = _uiRects.Count; + DebugOccludedCountLastFrame = occludedThisFrame; + DebugLastNameplateFrame = _lastNamePlateDrawFrame; +#endif } private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch @@ -705,8 +841,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (scale <= 0f) scale = 1f; - var computed = (int)System.Math.Round(rawHeight * scale); - return System.Math.Max(1, computed); + var computed = (int)Math.Round(rawHeight * scale); + return Math.Max(1, computed); } private static unsafe int GetScaledTextWidth(AtkTextNode* node) @@ -730,12 +866,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Resolves a cached value for the given index. /// - /// - /// - /// - /// - /// - /// private static int ResolveCache( int[] cache, int index, @@ -775,9 +905,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Snapping a position to pixel grid based on DPI scale. /// - /// Position - /// DPI Scale - /// private static Vector2 SnapToPixels(Vector2 p, float dpiScale) { // snap to pixel grid @@ -786,15 +913,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return new Vector2(x, y); } - /// /// Smooths the position using exponential smoothing. /// - /// Nameplate Index - /// Final position - /// Delta Time - /// How responssive the smooting should be - /// private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f) { // exponential smoothing @@ -812,7 +933,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe var a = 1f - MathF.Exp(-responsiveness * dt); // snap if close enough - if (Vector2.DistanceSquared(cur, target) < 0.25f) + if (Vector2.DistanceSquared(cur, target) < 0.25f) return cur; // lerp towards target @@ -821,73 +942,186 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return cur; } - /// - /// Tries to get a valid screen rect for the given addon. - /// - /// Addon UI - /// Screen positioning/param> - /// RectF of Addon - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f)); + private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect) { - // Addon existence rect = default; if (addon == null) return false; - // Visibility check + if (!addon->IsVisible) + return false; + var root = addon->RootNode; if (root == null || !root->IsVisible()) return false; - // Size check - float w = root->Width; - float h = root->Height; - if (w <= 0 || h <= 0) + var nodeCount = addon->UldManager.NodeListCount; + var nodeList = addon->UldManager.NodeList; + if (nodeCount <= 1 || nodeList == null) return false; - // Local scale - float sx = root->ScaleX; if (sx <= 0f) sx = 1f; - float sy = root->ScaleY; if (sy <= 0f) sy = 1f; + float rsx = GetWorldScaleX(root); + float rsy = GetWorldScaleY(root); + if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f; + if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f; - // World/composed scale from Transform - float wsx = GetWorldScaleX(root); - float wsy = GetWorldScaleY(root); - if (wsx <= 0f) wsx = 1f; - if (wsy <= 0f) wsy = 1f; + // clamp insane root scales (rare but prevents explosions) + // clamp insane root scales (rare but prevents explosions) + rsx = MathF.Min(rsx, 6f); + rsy = MathF.Min(rsy, 6f); - // World scale may include parent scaling; use it if meaningfully different. - float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx; - float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy; - - w *= useX; - h *= useY; - - if (w < 4f || h < 4f) + float rw = root->Width * rsx; + float rh = root->Height * rsy; + if (!IsFinite(rw) || !IsFinite(rh) || rw <= 2f || rh <= 2f) return false; - // Screen coords - float l = root->ScreenX; - float t = root->ScreenY; - float r = l + w; - float b = t + h; - - // Drop fullscreen-ish / insane rects - if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f) + float rl = root->ScreenX; + float rt = root->ScreenY; + if (!IsFinite(rl) || !IsFinite(rt)) return false; - // Drop offscreen rects - if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f) + float rr = rl + rw; + float rb = rt + rh; + + // If root is basically fullscreen, it’s not a useful occluder for our purpose. + if (rw >= screen.X * 0.98f && rh >= screen.Y * 0.98f) return false; + // Clip root to screen so it stays sane + float rootL = MathF.Max(0f, rl); + float rootT = MathF.Max(0f, rt); + float rootR = MathF.Min(screen.X, rr); + float rootB = MathF.Min(screen.Y, rb); + if (rootR <= rootL || rootB <= rootT) + return false; + + var rootW = rootR - rootL; + var rootH = rootB - rootT; + + // --- Union of drawable-ish nodes, but constrained by root rect --- + bool any = false; + float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue; + + // Allow a small bleed outside root; some addons draw small bits outside their root container. + const float rootPad = 24f; + float padL = rootL - rootPad; + float padT = rootT - rootPad; + float padR = rootR + rootPad; + float padB = rootB + rootPad; + + for (int i = 1; i < nodeCount; i++) + { + var n = nodeList[i]; + if (!IsProbablyDrawableNode(n)) + continue; + + float w = n->Width; + float h = n->Height; + if (!IsFinite(w) || !IsFinite(h) || w <= 1f || h <= 1f) + continue; + + float sx = GetWorldScaleX(n); + float sy = GetWorldScaleY(n); + + if (!IsFinite(sx) || sx <= 0f) sx = 1f; + if (!IsFinite(sy) || sy <= 0f) sy = 1f; + + sx = MathF.Min(sx, 6f); + sy = MathF.Min(sy, 6f); + + w *= sx; + h *= sy; + + if (!IsFinite(w) || !IsFinite(h) || w < 2f || h < 2f) + continue; + + float nl = n->ScreenX; + float nt = n->ScreenY; + if (!IsFinite(nl) || !IsFinite(nt)) + continue; + + float nr = nl + w; + float nb = nt + h; + + // Must intersect root (with padding). This is the big mitigation. + if (nr <= padL || nb <= padT || nl >= padR || nt >= padB) + continue; + + // Reject nodes that are wildly larger than the root (common on targeting). + if (w > rootW * 2.0f || h > rootH * 2.0f) + continue; + + // Clip node to root and then to screen (prevents offscreen junk stretching union) + float cl = MathF.Max(rootL, nl); + float ct = MathF.Max(rootT, nt); + float cr = MathF.Min(rootR, nr); + float cb = MathF.Min(rootB, nb); + + cl = MathF.Max(0f, cl); + ct = MathF.Max(0f, ct); + cr = MathF.Min(screen.X, cr); + cb = MathF.Min(screen.Y, cb); + + if (cr <= cl || cb <= ct) + continue; + + any = true; + if (cl < l) l = cl; + if (ct < t) t = ct; + if (cr > r) r = cr; + if (cb > b) b = cb; + } + + // If nothing usable, fallback to root rect (still a sane occluder) + if (!any) + { + rect = new RectF(rootL, rootT, rootR, rootB); + return true; + } + + var uw = r - l; + var uh = b - t; + if (uw < 4f || uh < 4f) + { + rect = new RectF(rootL, rootT, rootR, rootB); + return true; + } + + if (uw > rootW * 1.35f || uh > rootH * 1.35f) + { + rect = new RectF(rootL, rootT, rootR, rootB); + return true; + } + rect = new RectF(l, t, r, b); return true; } + private static bool IsProbablyDrawableNode(AtkResNode* n) + { + if (n == null || !n->IsVisible()) + return false; + + if (n->Color.A == 0) + return false; + + return n->Type switch + { + NodeType.Text => true, + NodeType.Image => true, + NodeType.NineGrid => true, + NodeType.Counter => true, + NodeType.Component => true, + _ => false, + }; + } + /// /// Refreshes the cached UI rects for occlusion checking. /// - /// Unit Manager private void RefreshUiRects(RaptureAtkUnitManager* unitMgr) { _uiRects.Clear(); @@ -911,13 +1145,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (TryGetAddonRect(addon, screen, out var r)) _uiRects.Add(r); } + +#if DEBUG + DebugUiRectCountLastFrame = _uiRects.Count; +#endif } /// /// Is the given label rect occluded by any UI rects? /// - /// UI/Label Rect - /// Is occluded or not private bool IsOccludedByAnyUi(RectF labelRect) { for (int i = 0; i < _uiRects.Count; i++) @@ -931,8 +1167,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Gets the world scale X of the given node. /// - /// Node - /// World Scale of node private static float GetWorldScaleX(AtkResNode* n) { var t = n->Transform; @@ -942,8 +1176,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Gets the world scale Y of the given node. /// - /// Node - /// World Scale of node private static float GetWorldScaleY(AtkResNode* n) { var t = n->Transform; @@ -953,8 +1185,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Normalize an icon glyph input into a valid string. /// - /// Raw glyph input - /// Normalized glyph input internal static string NormalizeIconGlyph(string? rawInput) { if (string.IsNullOrWhiteSpace(rawInput)) @@ -982,7 +1212,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Is the nameplate addon visible? /// - /// Is it visible? private bool IsNamePlateAddonVisible() { if (_mpNameplateAddon == null) @@ -992,20 +1221,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return root != null && root->IsVisible(); } - /// - /// Converts raw icon glyph input into an icon editor string. - /// - /// Raw icon glyph input - /// Icon editor string - internal static string ToIconEditorString(string? rawInput) - { - var normalized = NormalizeIconGlyph(rawInput); - var runeEnumerator = normalized.EnumerateRunes(); - return runeEnumerator.MoveNext() - ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture) - : _defaultIconGlyph; - } - private readonly struct NameplateLabelInfo { public NameplateLabelInfo( @@ -1043,6 +1258,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Select(u => (ulong)u.PlayerCharacterId)]; + public int DebugLabelCountLastFrame { get => _debugLabelCountLastFrame; set => _debugLabelCountLastFrame = value; } + public int DebugUiRectCountLastFrame { get => _debugUiRectCountLastFrame; set => _debugUiRectCountLastFrame = value; } + public int DebugOccludedCountLastFrame { get => _debugOccludedCountLastFrame; set => _debugOccludedCountLastFrame = value; } + public uint DebugLastNameplateFrame { get => _debugLastNameplateFrame; set => _debugLastNameplateFrame = value; } + public bool DebugDrawUiRects { get => _debugDrawUiRects; set => _debugDrawUiRects = value; } + public bool DebugDrawLabelRects { get => _debugDrawLabelRects; set => _debugDrawLabelRects = value; } + public bool DebugDisableOcclusion { get => _debugDisableOcclusion; set => _debugDisableOcclusion = value; } + public bool DebugEnabled { get => _debugEnabled; set => _debugEnabled = value; } + public void FlagRefresh() { _needsLabelRefresh = true; @@ -1066,7 +1290,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Update the active broadcasting CIDs. /// - /// Inbound new CIDs public void UpdateBroadcastingCids(IEnumerable cids) { var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); @@ -1096,7 +1319,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe public NameplateBuffers() { TextOffsets = new int[AddonNamePlate.NumNamePlateObjects]; - System.Array.Fill(TextOffsets, int.MinValue); + Array.Fill(TextOffsets, int.MinValue); } public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects]; @@ -1108,23 +1331,20 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects]; public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects]; - public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects]; public void Clear() { - System.Array.Clear(TextWidths, 0, TextWidths.Length); - System.Array.Clear(TextHeights, 0, TextHeights.Length); - System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length); - System.Array.Fill(TextOffsets, int.MinValue); + Array.Clear(TextWidths, 0, TextWidths.Length); + Array.Clear(TextHeights, 0, TextHeights.Length); + Array.Clear(ContainerHeights, 0, ContainerHeights.Length); + Array.Fill(TextOffsets, int.MinValue); } } /// /// Starts the LightFinder Plate Handler. /// - /// Cancellation Token - /// Task Completed public Task StartAsync(CancellationToken cancellationToken) { Init(); @@ -1134,8 +1354,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe /// /// Stops the LightFinder Plate Handler. /// - /// Cancellation Token - /// Task Completed public Task StopAsync(CancellationToken cancellationToken) { Uninit(); @@ -1154,4 +1372,4 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe public bool Intersects(in RectF o) => !(R <= o.L || o.R <= L || B <= o.T || o.B <= T); } -} \ No newline at end of file +} diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index d4b960e..32245d2 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -968,20 +968,25 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase Dictionary> source) { var clone = new Dictionary>(source.Count); + foreach (var (objectKind, entries) in source) { var entryClone = new Dictionary(entries.Count, entries.Comparer); + foreach (var (hash, entry) in entries) { entryClone[hash] = new CharacterAnalyzer.FileDataEntry( - hash, - entry.FileType, - entry.GamePaths.ToList(), - entry.FilePaths.ToList(), - entry.OriginalSize, - entry.CompressedSize, - entry.Triangles); + hash: hash, + fileType: entry.FileType, + gamePaths: entry.GamePaths?.ToList() ?? [], + filePaths: entry.FilePaths?.ToList() ?? [], + originalSize: entry.OriginalSize, + compressedSize: entry.CompressedSize, + triangles: entry.Triangles, + cacheEntries: entry.CacheEntries + ); } + clone[objectKind] = entryClone; } diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index 22911cb..0aecee3 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -23,6 +23,7 @@ namespace LightlessSync.UI private readonly LightFinderService _broadcastService; private readonly UiSharedService _uiSharedService; private readonly LightFinderScannerService _broadcastScannerService; + private readonly LightFinderPlateHandler _lightFinderPlateHandler; private IReadOnlyList _allSyncshells = Array.Empty(); private string _userUid = string.Empty; @@ -38,7 +39,8 @@ namespace LightlessSync.UI UiSharedService uiShared, ApiController apiController, LightFinderScannerService broadcastScannerService - ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) +, + LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; @@ -50,6 +52,7 @@ namespace LightlessSync.UI WindowBuilder.For(this) .SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525)) .Apply(); + _lightFinderPlateHandler = lightFinderPlateHandler; } private void RebuildSyncshellDropdownOptions() @@ -380,9 +383,47 @@ namespace LightlessSync.UI #if DEBUG if (ImGui.BeginTabItem("Debug")) { + if (ImGui.CollapsingHeader("LightFinder Plates", ImGuiTreeNodeFlags.DefaultOpen)) + { + var h = _lightFinderPlateHandler; + + var enabled = h.DebugEnabled; + if (ImGui.Checkbox("Enable LightFinder debug", ref enabled)) + h.DebugEnabled = enabled; + + if (h.DebugEnabled) + { + ImGui.Indent(); + + var disableOcc = h.DebugDisableOcclusion; + if (ImGui.Checkbox("Disable occlusion (force draw)", ref disableOcc)) + h.DebugDisableOcclusion = disableOcc; + + var drawUiRects = h.DebugDrawUiRects; + if (ImGui.Checkbox("Draw UI rects", ref drawUiRects)) + h.DebugDrawUiRects = drawUiRects; + + var drawLabelRects = h.DebugDrawLabelRects; + if (ImGui.Checkbox("Draw label rects", ref drawLabelRects)) + h.DebugDrawLabelRects = drawLabelRects; + + ImGui.Separator(); + ImGui.TextUnformatted($"Labels last frame: {h.DebugLabelCountLastFrame}"); + ImGui.TextUnformatted($"UI rects last frame: {h.DebugUiRectCountLastFrame}"); + ImGui.TextUnformatted($"Occluded last frame: {h.DebugOccludedCountLastFrame}"); + ImGui.TextUnformatted($"Last NamePlate frame: {h.DebugLastNameplateFrame}"); + + ImGui.Unindent(); + } + } + + ImGui.Separator(); + ImGui.Text("Broadcast Cache"); - if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f))) + 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); diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 63757bd..a0c1787 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2587,7 +2587,7 @@ public class SettingsUi : WindowMediatorSubscriberBase var selected = i == _lightfinderIconPresetIndex; if (ImGui.Selectable(preview, selected)) { - _lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(optionGlyph); + _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(optionGlyph); _lightfinderIconPresetIndex = i; } } @@ -4063,7 +4063,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private void RefreshLightfinderIconState() { var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph); - _lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalized); + _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalized); _lightfinderIconInputInitialized = true; _lightfinderIconPresetIndex = -1; @@ -4081,7 +4081,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelIconGlyph = normalizedGlyph; _configService.Save(); - _lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalizedGlyph); + _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalizedGlyph); _lightfinderIconPresetIndex = presetIndex; _lightfinderIconInputInitialized = true; } From a3ea48c6e1b82770e29fc8e305fafb31510ec587 Mon Sep 17 00:00:00 2001 From: cake Date: Sun, 28 Dec 2025 03:15:15 +0100 Subject: [PATCH 2/3] Fixed some comments --- .../LightFinder/LightFinderPlateHandler.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs index e11fd5c..6f0a185 100644 --- a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -679,7 +679,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe ImGui.PopStyleVar(2); - // --- Debug settings (wired via handler fields; no hotkey / no extra debug window here) --- + // Debug flags bool dbgEnabled = false; bool dbgDisableOcc = false; bool dbgDrawUiRects = false; @@ -737,11 +737,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f; var size = baseSize * scale; - // label rect for occlusion checking (in game screen coords, NOT viewport-pos-adjusted) var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y); var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y); - // "Would this be occluded?" (we track this even if we force-draw) bool wouldOcclude = IsOccludedByAnyUi(labelRect); if (wouldOcclude) occludedThisFrame++; @@ -769,7 +767,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font); } - // Debug: draw UI rects (occluders) + // Debug: draw UI rects if any if (dbgEnabled && dbgDrawUiRects && _uiRects.Count > 0) { var useOff = useViewportOffset ? vpPos : Vector2.Zero; @@ -788,7 +786,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe } #if DEBUG - // --- Publish per-frame debug counters for the UI Debug tab --- DebugLabelCountLastFrame = copyCount; DebugUiRectCountLastFrame = _uiRects.Count; DebugOccludedCountLastFrame = occludedThisFrame; @@ -951,13 +948,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (addon == null) return false; + // Addon must be visible if (!addon->IsVisible) return false; + // Root must be visible var root = addon->RootNode; if (root == null || !root->IsVisible()) return false; + // Must have multiple nodes to be useful var nodeCount = addon->UldManager.NodeListCount; var nodeList = addon->UldManager.NodeList; if (nodeCount <= 1 || nodeList == null) @@ -968,7 +968,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f; if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f; - // clamp insane root scales (rare but prevents explosions) // clamp insane root scales (rare but prevents explosions) rsx = MathF.Min(rsx, 6f); rsy = MathF.Min(rsy, 6f); @@ -998,10 +997,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (rootR <= rootL || rootB <= rootT) return false; + // Root dimensions var rootW = rootR - rootL; var rootH = rootB - rootT; - // --- Union of drawable-ish nodes, but constrained by root rect --- + // Find union of all probably-drawable nodes intersecting root bool any = false; float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue; @@ -1082,6 +1082,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return true; } + // Validate final union rect var uw = r - l; var uh = b - t; if (uw < 4f || uh < 4f) @@ -1090,6 +1091,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return true; } + // If union is excessively larger than root, fallback to root rect if (uw > rootW * 1.35f || uh > rootH * 1.35f) { rect = new RectF(rootL, rootT, rootR, rootB); @@ -1105,9 +1107,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (n == null || !n->IsVisible()) return false; - if (n->Color.A == 0) + // Check alpha + if (n->Color.A == 16) return false; + // Check node type return n->Type switch { NodeType.Text => true, From 2abc92fc6184444c107d1456bf9958fe16e4d961 Mon Sep 17 00:00:00 2001 From: cake Date: Sun, 28 Dec 2025 03:17:27 +0100 Subject: [PATCH 3/3] Fixed warnings --- .../LightFinder/LightFinderPlateHandler.cs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs index 6f0a185..97217c8 100644 --- a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -75,16 +75,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe #if DEBUG // Debug controls - private bool _debugEnabled; - private bool _debugDisableOcclusion; - private bool _debugDrawUiRects; - private bool _debugDrawLabelRects = true; // Debug counters (read-only from UI) - private int _debugLabelCountLastFrame; - private int _debugUiRectCountLastFrame; - private int _debugOccludedCountLastFrame; - private uint _debugLastNameplateFrame; #endif private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy; @@ -1262,14 +1254,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Select(u => (ulong)u.PlayerCharacterId)]; - public int DebugLabelCountLastFrame { get => _debugLabelCountLastFrame; set => _debugLabelCountLastFrame = value; } - public int DebugUiRectCountLastFrame { get => _debugUiRectCountLastFrame; set => _debugUiRectCountLastFrame = value; } - public int DebugOccludedCountLastFrame { get => _debugOccludedCountLastFrame; set => _debugOccludedCountLastFrame = value; } - public uint DebugLastNameplateFrame { get => _debugLastNameplateFrame; set => _debugLastNameplateFrame = value; } - public bool DebugDrawUiRects { get => _debugDrawUiRects; set => _debugDrawUiRects = value; } - public bool DebugDrawLabelRects { get => _debugDrawLabelRects; set => _debugDrawLabelRects = value; } - public bool DebugDisableOcclusion { get => _debugDisableOcclusion; set => _debugDisableOcclusion = value; } - public bool DebugEnabled { get => _debugEnabled; set => _debugEnabled = value; } + public int DebugLabelCountLastFrame { get; set; } + public int DebugUiRectCountLastFrame { get; set; } + public int DebugOccludedCountLastFrame { get; set; } + public uint DebugLastNameplateFrame { get; set; } + public bool DebugDrawUiRects { get; set; } + public bool DebugDrawLabelRects { get; set; } = true; + public bool DebugDisableOcclusion { get; set; } + public bool DebugEnabled { get; set; } public void FlagRefresh() {