Added debug mode for lightfinder IMGUI, added caching of file cache entries to reduce load of loading all entries again.

This commit is contained in:
cake
2025-12-28 03:01:02 +01:00
parent 8f32b375dd
commit deb99628f6
8 changed files with 723 additions and 244 deletions

View File

@@ -1,15 +1,23 @@
#nullable disable #nullable disable
using System.Text.Json.Serialization;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
public class FileCacheEntity 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; Size = size;
CompressedSize = compressedSize; CompressedSize = compressedSize;
Hash = hash; Hash = hash;
PrefixedFilePath = path; PrefixedFilePath = prefixedFilePath;
LastModifiedDateTicks = lastModifiedDateTicks; LastModifiedDateTicks = lastModifiedDateTicks;
} }
@@ -23,7 +31,5 @@ public class FileCacheEntity
public long? Size { get; set; } public long? Size { get; set; }
public void SetResolvedFilePath(string filePath) public void SetResolvedFilePath(string filePath)
{ => ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
}
} }

View File

@@ -7,6 +7,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
@@ -31,6 +33,14 @@ public sealed class FileCacheManager : IHostedService
private bool _csvHeaderEnsured; private bool _csvHeaderEnsured;
public string CacheFolder => _configService.Current.CacheFolder; public string CacheFolder => _configService.Current.CacheFolder;
private const string _compressedCacheExtension = ".llz4";
private readonly ConcurrentDictionary<string, SemaphoreSlim> _compressLocks = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, SizeInfo> _sizeCache =
new(StringComparer.OrdinalIgnoreCase);
[StructLayout(LayoutKind.Auto)]
public readonly record struct SizeInfo(long Original, long Compressed);
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator) public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
{ {
_logger = logger; _logger = logger;
@@ -45,6 +55,18 @@ public sealed class FileCacheManager : IHostedService
private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal) private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
.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) private static string NormalizePrefixedPathKey(string prefixedPath)
{ {
if (string.IsNullOrEmpty(prefixedPath)) if (string.IsNullOrEmpty(prefixedPath))
@@ -111,6 +133,114 @@ public sealed class FileCacheManager : IHostedService
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version); 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<FileCacheEntity?> 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<long> 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<byte[]> 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) private string NormalizeToPrefixedPath(string path)
{ {
if (string.IsNullOrEmpty(path)) return string.Empty; 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) 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; var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath;
return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, var raw = await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false);
(int)new FileInfo(fileCache).Length)); var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
UpdateSizeInfo(fileHash, original: raw.LongLength, compressed: compressed.LongLength);
return (fileHash, compressed);
} }
public FileCacheEntity? GetFileCacheByHash(string hash) public FileCacheEntity? GetFileCacheByHash(string hash)
@@ -891,6 +1030,14 @@ public sealed class FileCacheManager : IHostedService
compressed = resultCompressed; 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))); AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -478,7 +478,8 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<LightlessConfigService>(), sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<UiSharedService>(), sp.GetRequiredService<UiSharedService>(),
sp.GetRequiredService<ApiController>(), sp.GetRequiredService<ApiController>(),
sp.GetRequiredService<LightFinderScannerService>())); sp.GetRequiredService<LightFinderScannerService>(),
sp.GetRequiredService<LightFinderPlateHandler>()));
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI( services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(), sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),

View File

@@ -1,3 +1,4 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache; using LightlessSync.FileCache;
@@ -98,11 +99,13 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_analysisCts = null; _analysisCts = null;
if (print) PrintAnalysis(); if (print) PrintAnalysis();
} }
public void Dispose() public void Dispose()
{ {
_analysisCts.CancelDispose(); _analysisCts.CancelDispose();
_baseAnalysisCts.Dispose(); _baseAnalysisCts.Dispose();
} }
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token) public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
{ {
var normalized = new HashSet<string>( var normalized = new HashSet<string>(
@@ -125,6 +128,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
} }
} }
} }
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
{ {
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return; if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
@@ -136,29 +140,47 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList(); var fileCacheEntries = (await _fileCacheManager
if (fileCacheEntries.Count == 0) continue; .GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token)
var filePath = fileCacheEntries[0].ResolvedFilepath; .ConfigureAwait(false))
FileInfo fi = new(filePath); .ToList();
string ext = "unk?";
try if (fileCacheEntries.Count == 0)
{ continue;
ext = fi.Extension[1..];
} var resolved = fileCacheEntries[0].ResolvedFilepath;
catch (Exception ex)
{ var extWithDot = Path.GetExtension(resolved);
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath); var ext = string.IsNullOrEmpty(extWithDot) ? "unk?" : extWithDot.TrimStart('.');
}
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false); 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, if (orig <= 0 && cached.Original > 0) orig = cached.Original;
[.. fileEntry.GamePaths], if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed;
[.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)],
entry.Size > 0 ? entry.Size.Value : 0,
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
tris);
} }
data[fileEntry.Hash] = new FileDataEntry(
fileEntry.Hash,
ext,
[.. fileEntry.GamePaths],
distinctFilePaths,
orig,
comp,
tris,
fileCacheEntries);
} }
LastAnalysis[obj.Key] = data; LastAnalysis[obj.Key] = data;
} }
@@ -167,6 +189,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
Mediator.Publish(new CharacterDataAnalyzedMessage()); Mediator.Publish(new CharacterDataAnalyzedMessage());
_lastDataHash = charaData.DataHash.Value; _lastDataHash = charaData.DataHash.Value;
} }
private void RecalculateSummary() private void RecalculateSummary()
{ {
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>(); var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
@@ -192,6 +215,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable()); _latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
} }
private void PrintAnalysis() private void PrintAnalysis()
{ {
if (LastAnalysis.Count == 0) return; 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)))); 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."); 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<string> GamePaths, List<string> 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<string> Format => _format ??= CreateFormatValue();
internal sealed class FileDataEntry
{
public string Hash { get; }
public string FileType { get; }
public List<string> GamePaths { get; }
public List<string> FilePaths { get; }
public long OriginalSize { get; private set; }
public long CompressedSize { get; private set; }
public long Triangles { get; private set; }
public IReadOnlyList<FileCacheEntity> CacheEntries { get; }
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
public FileDataEntry(
string hash,
string fileType,
List<string> gamePaths,
List<string> filePaths,
long originalSize,
long compressedSize,
long triangles,
IReadOnlyList<FileCacheEntity> 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<string> Format => _format ??= CreateFormatValue();
private Lazy<string>? _format; private Lazy<string>? _format;
public void RefreshFormat() public void RefreshFormat() => _format = CreateFormatValue();
{
_format = CreateFormatValue();
}
private Lazy<string> CreateFormatValue() private Lazy<string> CreateFormatValue()
=> new(() => => new(() =>
{ {
if (!string.Equals(FileType, "tex", StringComparison.Ordinal)) if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
{
return string.Empty; return string.Empty;
}
try try
{ {

View File

@@ -23,6 +23,7 @@ using Pictomancy;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Task = System.Threading.Tasks.Task; using Task = System.Threading.Tasks.Task;
@@ -41,6 +42,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
public LightlessMediator Mediator => _mediator; public LightlessMediator Mediator => _mediator;
private readonly IUiBuilder _uiBuilder; private readonly IUiBuilder _uiBuilder;
@@ -61,16 +63,30 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
// / Overlay window flags // / Overlay window flags
private const ImGuiWindowFlags _overlayFlags = private const ImGuiWindowFlags _overlayFlags =
ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoInputs; ImGuiWindowFlags.NoInputs;
private readonly List<RectF> _uiRects = new(128); private readonly List<RectF> _uiRects = new(128);
private ImmutableHashSet<string> _activeBroadcastingCids = []; private ImmutableHashSet<string> _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; private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy;
public LightFinderPlateHandler( public LightFinderPlateHandler(
@@ -96,7 +112,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface)); _uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService)); _ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
_lastRenderer = _configService.Current.LightfinderLabelRenderer; _lastRenderer = _configService.Current.LightfinderLabelRenderer;
} }
private void RefreshRendererState() private void RefreshRendererState()
@@ -187,8 +202,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Draw detour for nameplate addon. /// Draw detour for nameplate addon.
/// </summary> /// </summary>
/// <param name="type"></param>
/// <param name="args"></param>
private void NameplateDrawDetour(AddonEvent type, AddonArgs args) private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
{ {
RefreshRendererState(); RefreshRendererState();
@@ -199,6 +212,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return; 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) if (_clientState.IsGPosing)
{ {
ClearLabelBuffer(); ClearLabelBuffer();
@@ -218,6 +241,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (fw != null) if (fw != null)
_lastNamePlateDrawFrame = fw->FrameCounter; _lastNamePlateDrawFrame = fw->FrameCounter;
#if DEBUG
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
if (_mpNameplateAddon != pNameplateAddon) if (_mpNameplateAddon != pNameplateAddon)
@@ -234,6 +261,13 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// </summary> /// </summary>
private void UpdateNameplateNodes() 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"); var currentHandle = _gameGui.GetAddonByName("NamePlate");
if (currentHandle.Address == nint.Zero) if (currentHandle.Address == nint.Zero)
{ {
@@ -297,7 +331,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
for (int i = 0; i < safeCount; ++i) for (int i = 0; i < safeCount; ++i)
{ {
var objectInfoPtr = vec[i]; var objectInfoPtr = vec[i];
if (objectInfoPtr == null) if (objectInfoPtr == null)
continue; continue;
@@ -314,7 +347,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
continue; continue;
// CID gating // CID gating - only show for active broadcasters
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
if (cid == null || !_activeBroadcastingCids.Contains(cid)) if (cid == null || !_activeBroadcastingCids.Contains(cid))
continue; continue;
@@ -350,12 +383,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible) if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
continue; continue;
// Prepare label content and scaling // Prepare label content and scaling factors
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f); var scaleMultiplier = Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f; var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
var effectiveScale = baseScale * scaleMultiplier; var effectiveScale = baseScale * scaleMultiplier;
var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f; 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 var labelContent = currentConfig.LightfinderLabelUseIcon
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph) ? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
: _defaultLabelText; : _defaultLabelText;
@@ -363,8 +396,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
labelContent = _defaultLabelText; labelContent = _defaultLabelText;
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); var nodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); var nodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
AlignmentType alignment; AlignmentType alignment;
var textScaleY = nameText->AtkResNode.ScaleY; var textScaleY = nameText->AtkResNode.ScaleY;
@@ -374,7 +407,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var blockHeight = ResolveCache( var blockHeight = ResolveCache(
_buffers.TextHeights, _buffers.TextHeights,
nameplateIndex, nameplateIndex,
System.Math.Abs((int)nameplateObject.TextH), Math.Abs((int)nameplateObject.TextH),
() => GetScaledTextHeight(nameText), () => GetScaledTextHeight(nameText),
nodeHeight); nodeHeight);
@@ -384,7 +417,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
(int)nameContainer->Height, (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; return computed <= blockHeight ? blockHeight + 1 : computed;
}, },
blockHeight + 1); blockHeight + 1);
@@ -392,7 +425,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var blockTop = containerHeight - blockHeight; var blockTop = containerHeight - blockHeight;
if (blockTop < 0) if (blockTop < 0)
blockTop = 0; blockTop = 0;
var verticalPadding = (int)System.Math.Round(4 * effectiveScale); var verticalPadding = (int)Math.Round(4 * effectiveScale);
var positionY = blockTop - verticalPadding; var positionY = blockTop - verticalPadding;
@@ -400,21 +433,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var textWidth = ResolveCache( var textWidth = ResolveCache(
_buffers.TextWidths, _buffers.TextWidths,
nameplateIndex, nameplateIndex,
System.Math.Abs(rawTextWidth), Math.Abs(rawTextWidth),
() => GetScaledTextWidth(nameText), () => GetScaledTextWidth(nameText),
nodeWidth); nodeWidth);
// Text offset caching // 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); 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; var res = nameContainer;
// X scale // X scale
@@ -450,7 +476,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX; var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
// alignment based on config // alignment based on config setting
switch (currentConfig.LabelAlignment) switch (currentConfig.LabelAlignment)
{ {
case LabelAlignment.Left: case LabelAlignment.Left:
@@ -469,7 +495,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
else else
{ {
// manual X positioning // manual X positioning with optional cached offset
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex]; var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
var hasCachedOffset = cachedTextOffset != int.MinValue; var hasCachedOffset = cachedTextOffset != int.MinValue;
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
@@ -489,16 +515,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
// final position before smoothing // final position before smoothing
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen); 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(); var fw = Framework.Instance();
float dt = fw->RealFrameDeltaTime; float dt = fw->RealFrameDeltaTime;
//smoothing.. //smoothing.. snap.. smooth.. snap
finalPosition = SnapToPixels(finalPosition, dpiScale); finalPosition = SnapToPixels(finalPosition, dpiScale);
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt); finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
finalPosition = SnapToPixels(finalPosition, dpiScale); finalPosition = SnapToPixels(finalPosition, dpiScale);
// prepare label info // prepare label info for rendering
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon) var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
? AlignmentToPivot(alignment) ? AlignmentToPivot(alignment)
: _defaultPivot; : _defaultPivot;
@@ -545,7 +571,23 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (fw == null) if (fw == null)
return; 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; var frame = fw->FrameCounter;
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1) if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
@@ -553,34 +595,62 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
ClearLabelBuffer(); ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
return; return;
} }
//Gpose Check // Gpose Check - do not render.
if (_clientState.IsGPosing) if (_clientState.IsGPosing)
{ {
ClearLabelBuffer(); ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0; _lastNamePlateDrawFrame = 0;
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = 0;
#endif
return; return;
} }
// If nameplate addon is not visible, skip rendering // If nameplate addon is not visible, skip rendering entirely.
if (!IsNamePlateAddonVisible()) if (!IsNamePlateAddonVisible())
{
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
return; return;
}
int copyCount; int copyCount;
lock (_labelLock) lock (_labelLock)
{ {
copyCount = _labelRenderCount; copyCount = _labelRenderCount;
if (copyCount == 0) if (copyCount == 0)
{
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
return; return;
}
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount); Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
} }
var uiModule = fw != null ? fw->GetUIModule() : null; var uiModule = fw->GetUIModule();
if (uiModule != null) if (uiModule != null)
{ {
var rapture = uiModule->GetRaptureAtkModule(); var rapture = uiModule->GetRaptureAtkModule();
@@ -599,7 +669,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var vpPos = vp.Pos; var vpPos = vp.Pos;
ImGuiHelpers.ForceNextWindowMainViewport(); ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(vp.Pos); ImGui.SetNextWindowPos(vp.Pos);
ImGui.SetNextWindowSize(vp.Size); ImGui.SetNextWindowSize(vp.Size);
@@ -610,54 +679,121 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
ImGui.PopStyleVar(2); ImGui.PopStyleVar(2);
using var drawList = PictoService.Draw(); // --- Debug settings (wired via handler fields; no hotkey / no extra debug window here) ---
if (drawList == null) 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(); ImGui.End();
return;
} }
for (int i = 0; i < copyCount; ++i) #if DEBUG
{ // --- Publish per-frame debug counters for the UI Debug tab ---
ref var info = ref _buffers.LabelCopy[i]; DebugLabelCountLastFrame = copyCount;
DebugUiRectCountLastFrame = _uiRects.Count;
// final draw position with viewport offset DebugOccludedCountLastFrame = occludedThisFrame;
var drawPos = info.ScreenPosition + vpPos; DebugLastNameplateFrame = _lastNamePlateDrawFrame;
var font = default(ImFontPtr); #endif
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);
}
} }
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
@@ -705,8 +841,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (scale <= 0f) if (scale <= 0f)
scale = 1f; scale = 1f;
var computed = (int)System.Math.Round(rawHeight * scale); var computed = (int)Math.Round(rawHeight * scale);
return System.Math.Max(1, computed); return Math.Max(1, computed);
} }
private static unsafe int GetScaledTextWidth(AtkTextNode* node) private static unsafe int GetScaledTextWidth(AtkTextNode* node)
@@ -730,12 +866,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Resolves a cached value for the given index. /// Resolves a cached value for the given index.
/// </summary> /// </summary>
/// <param name="cache"></param>
/// <param name="index"></param>
/// <param name="rawValue"></param>
/// <param name="fallback"></param>
/// <param name="fallbackWhenZero"></param>
/// <returns></returns>
private static int ResolveCache( private static int ResolveCache(
int[] cache, int[] cache,
int index, int index,
@@ -775,9 +905,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Snapping a position to pixel grid based on DPI scale. /// Snapping a position to pixel grid based on DPI scale.
/// </summary> /// </summary>
/// <param name="p">Position</param>
/// <param name="dpiScale">DPI Scale</param>
/// <returns></returns>
private static Vector2 SnapToPixels(Vector2 p, float dpiScale) private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
{ {
// snap to pixel grid // snap to pixel grid
@@ -786,15 +913,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return new Vector2(x, y); return new Vector2(x, y);
} }
/// <summary> /// <summary>
/// Smooths the position using exponential smoothing. /// Smooths the position using exponential smoothing.
/// </summary> /// </summary>
/// <param name="idx">Nameplate Index</param>
/// <param name="target">Final position</param>
/// <param name="dt">Delta Time</param>
/// <param name="responsiveness">How responssive the smooting should be</param>
/// <returns></returns>
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f) private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
{ {
// exponential smoothing // exponential smoothing
@@ -812,7 +933,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var a = 1f - MathF.Exp(-responsiveness * dt); var a = 1f - MathF.Exp(-responsiveness * dt);
// snap if close enough // snap if close enough
if (Vector2.DistanceSquared(cur, target) < 0.25f) if (Vector2.DistanceSquared(cur, target) < 0.25f)
return cur; return cur;
// lerp towards target // lerp towards target
@@ -821,73 +942,186 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return cur; return cur;
} }
/// <summary> [MethodImpl(MethodImplOptions.AggressiveInlining)]
/// Tries to get a valid screen rect for the given addon. private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f));
/// </summary>
/// <param name="addon">Addon UI</param>
/// <param name="screen">Screen positioning/param>
/// <param name="rect">RectF of Addon</param>
/// <returns></returns>
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect) private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
{ {
// Addon existence
rect = default; rect = default;
if (addon == null) if (addon == null)
return false; return false;
// Visibility check if (!addon->IsVisible)
return false;
var root = addon->RootNode; var root = addon->RootNode;
if (root == null || !root->IsVisible()) if (root == null || !root->IsVisible())
return false; return false;
// Size check var nodeCount = addon->UldManager.NodeListCount;
float w = root->Width; var nodeList = addon->UldManager.NodeList;
float h = root->Height; if (nodeCount <= 1 || nodeList == null)
if (w <= 0 || h <= 0)
return false; return false;
// Local scale float rsx = GetWorldScaleX(root);
float sx = root->ScaleX; if (sx <= 0f) sx = 1f; float rsy = GetWorldScaleY(root);
float sy = root->ScaleY; if (sy <= 0f) sy = 1f; if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f;
if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f;
// World/composed scale from Transform // clamp insane root scales (rare but prevents explosions)
float wsx = GetWorldScaleX(root); // clamp insane root scales (rare but prevents explosions)
float wsy = GetWorldScaleY(root); rsx = MathF.Min(rsx, 6f);
if (wsx <= 0f) wsx = 1f; rsy = MathF.Min(rsy, 6f);
if (wsy <= 0f) wsy = 1f;
// World scale may include parent scaling; use it if meaningfully different. float rw = root->Width * rsx;
float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx; float rh = root->Height * rsy;
float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy; if (!IsFinite(rw) || !IsFinite(rh) || rw <= 2f || rh <= 2f)
w *= useX;
h *= useY;
if (w < 4f || h < 4f)
return false; return false;
// Screen coords float rl = root->ScreenX;
float l = root->ScreenX; float rt = root->ScreenY;
float t = root->ScreenY; if (!IsFinite(rl) || !IsFinite(rt))
float r = l + w;
float b = t + h;
// Drop fullscreen-ish / insane rects
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
return false; return false;
// Drop offscreen rects float rr = rl + rw;
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f) float rb = rt + rh;
// If root is basically fullscreen, it<69>s not a useful occluder for our purpose.
if (rw >= screen.X * 0.98f && rh >= screen.Y * 0.98f)
return false; 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); rect = new RectF(l, t, r, b);
return true; 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,
};
}
/// <summary> /// <summary>
/// Refreshes the cached UI rects for occlusion checking. /// Refreshes the cached UI rects for occlusion checking.
/// </summary> /// </summary>
/// <param name="unitMgr">Unit Manager</param>
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr) private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
{ {
_uiRects.Clear(); _uiRects.Clear();
@@ -911,13 +1145,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (TryGetAddonRect(addon, screen, out var r)) if (TryGetAddonRect(addon, screen, out var r))
_uiRects.Add(r); _uiRects.Add(r);
} }
#if DEBUG
DebugUiRectCountLastFrame = _uiRects.Count;
#endif
} }
/// <summary> /// <summary>
/// Is the given label rect occluded by any UI rects? /// Is the given label rect occluded by any UI rects?
/// </summary> /// </summary>
/// <param name="labelRect">UI/Label Rect</param>
/// <returns>Is occluded or not</returns>
private bool IsOccludedByAnyUi(RectF labelRect) private bool IsOccludedByAnyUi(RectF labelRect)
{ {
for (int i = 0; i < _uiRects.Count; i++) for (int i = 0; i < _uiRects.Count; i++)
@@ -931,8 +1167,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Gets the world scale X of the given node. /// Gets the world scale X of the given node.
/// </summary> /// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleX(AtkResNode* n) private static float GetWorldScaleX(AtkResNode* n)
{ {
var t = n->Transform; var t = n->Transform;
@@ -942,8 +1176,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Gets the world scale Y of the given node. /// Gets the world scale Y of the given node.
/// </summary> /// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleY(AtkResNode* n) private static float GetWorldScaleY(AtkResNode* n)
{ {
var t = n->Transform; var t = n->Transform;
@@ -953,8 +1185,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Normalize an icon glyph input into a valid string. /// Normalize an icon glyph input into a valid string.
/// </summary> /// </summary>
/// <param name="rawInput">Raw glyph input</param>
/// <returns>Normalized glyph input</returns>
internal static string NormalizeIconGlyph(string? rawInput) internal static string NormalizeIconGlyph(string? rawInput)
{ {
if (string.IsNullOrWhiteSpace(rawInput)) if (string.IsNullOrWhiteSpace(rawInput))
@@ -982,7 +1212,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Is the nameplate addon visible? /// Is the nameplate addon visible?
/// </summary> /// </summary>
/// <returns>Is it visible?</returns>
private bool IsNamePlateAddonVisible() private bool IsNamePlateAddonVisible()
{ {
if (_mpNameplateAddon == null) if (_mpNameplateAddon == null)
@@ -992,20 +1221,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return root != null && root->IsVisible(); return root != null && root->IsVisible();
} }
/// <summary>
/// Converts raw icon glyph input into an icon editor string.
/// </summary>
/// <param name="rawInput">Raw icon glyph input</param>
/// <returns>Icon editor string</returns>
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 private readonly struct NameplateLabelInfo
{ {
public NameplateLabelInfo( public NameplateLabelInfo(
@@ -1043,6 +1258,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
.Select(u => (ulong)u.PlayerCharacterId)]; .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() public void FlagRefresh()
{ {
_needsLabelRefresh = true; _needsLabelRefresh = true;
@@ -1066,7 +1290,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Update the active broadcasting CIDs. /// Update the active broadcasting CIDs.
/// </summary> /// </summary>
/// <param name="cids">Inbound new CIDs</param>
public void UpdateBroadcastingCids(IEnumerable<string> cids) public void UpdateBroadcastingCids(IEnumerable<string> cids)
{ {
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
@@ -1096,7 +1319,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public NameplateBuffers() public NameplateBuffers()
{ {
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects]; TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
System.Array.Fill(TextOffsets, int.MinValue); Array.Fill(TextOffsets, int.MinValue);
} }
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects]; 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 NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects]; public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects]; public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects];
public void Clear() public void Clear()
{ {
System.Array.Clear(TextWidths, 0, TextWidths.Length); Array.Clear(TextWidths, 0, TextWidths.Length);
System.Array.Clear(TextHeights, 0, TextHeights.Length); Array.Clear(TextHeights, 0, TextHeights.Length);
System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length); Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
System.Array.Fill(TextOffsets, int.MinValue); Array.Fill(TextOffsets, int.MinValue);
} }
} }
/// <summary> /// <summary>
/// Starts the LightFinder Plate Handler. /// Starts the LightFinder Plate Handler.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Init(); Init();
@@ -1134,8 +1354,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary> /// <summary>
/// Stops the LightFinder Plate Handler. /// Stops the LightFinder Plate Handler.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
Uninit(); Uninit();
@@ -1154,4 +1372,4 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public bool Intersects(in RectF o) => public bool Intersects(in RectF o) =>
!(R <= o.L || o.R <= L || B <= o.T || o.B <= T); !(R <= o.L || o.R <= L || B <= o.T || o.B <= T);
} }
} }

View File

@@ -968,20 +968,25 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source) Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
{ {
var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count); var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);
foreach (var (objectKind, entries) in source) foreach (var (objectKind, entries) in source)
{ {
var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer); var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer);
foreach (var (hash, entry) in entries) foreach (var (hash, entry) in entries)
{ {
entryClone[hash] = new CharacterAnalyzer.FileDataEntry( entryClone[hash] = new CharacterAnalyzer.FileDataEntry(
hash, hash: hash,
entry.FileType, fileType: entry.FileType,
entry.GamePaths.ToList(), gamePaths: entry.GamePaths?.ToList() ?? [],
entry.FilePaths.ToList(), filePaths: entry.FilePaths?.ToList() ?? [],
entry.OriginalSize, originalSize: entry.OriginalSize,
entry.CompressedSize, compressedSize: entry.CompressedSize,
entry.Triangles); triangles: entry.Triangles,
cacheEntries: entry.CacheEntries
);
} }
clone[objectKind] = entryClone; clone[objectKind] = entryClone;
} }

View File

@@ -23,6 +23,7 @@ namespace LightlessSync.UI
private readonly LightFinderService _broadcastService; private readonly LightFinderService _broadcastService;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly LightFinderScannerService _broadcastScannerService; private readonly LightFinderScannerService _broadcastScannerService;
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>(); private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
private string _userUid = string.Empty; private string _userUid = string.Empty;
@@ -38,7 +39,8 @@ namespace LightlessSync.UI
UiSharedService uiShared, UiSharedService uiShared,
ApiController apiController, ApiController apiController,
LightFinderScannerService broadcastScannerService LightFinderScannerService broadcastScannerService
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) ,
LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
{ {
_broadcastService = broadcastService; _broadcastService = broadcastService;
_uiSharedService = uiShared; _uiSharedService = uiShared;
@@ -50,6 +52,7 @@ namespace LightlessSync.UI
WindowBuilder.For(this) WindowBuilder.For(this)
.SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525)) .SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525))
.Apply(); .Apply();
_lightFinderPlateHandler = lightFinderPlateHandler;
} }
private void RebuildSyncshellDropdownOptions() private void RebuildSyncshellDropdownOptions()
@@ -380,9 +383,47 @@ namespace LightlessSync.UI
#if DEBUG #if DEBUG
if (ImGui.BeginTabItem("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"); 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("CID", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);

View File

@@ -2587,7 +2587,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var selected = i == _lightfinderIconPresetIndex; var selected = i == _lightfinderIconPresetIndex;
if (ImGui.Selectable(preview, selected)) if (ImGui.Selectable(preview, selected))
{ {
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(optionGlyph); _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(optionGlyph);
_lightfinderIconPresetIndex = i; _lightfinderIconPresetIndex = i;
} }
} }
@@ -4063,7 +4063,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private void RefreshLightfinderIconState() private void RefreshLightfinderIconState()
{ {
var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph); var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph);
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalized); _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalized);
_lightfinderIconInputInitialized = true; _lightfinderIconInputInitialized = true;
_lightfinderIconPresetIndex = -1; _lightfinderIconPresetIndex = -1;
@@ -4081,7 +4081,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph; _configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
_configService.Save(); _configService.Save();
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalizedGlyph); _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalizedGlyph);
_lightfinderIconPresetIndex = presetIndex; _lightfinderIconPresetIndex = presetIndex;
_lightfinderIconInputInitialized = true; _lightfinderIconInputInitialized = true;
} }