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
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);
}

View File

@@ -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<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)
{
_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<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)
{
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)

View File

@@ -478,7 +478,8 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<UiSharedService>(),
sp.GetRequiredService<ApiController>(),
sp.GetRequiredService<LightFinderScannerService>()));
sp.GetRequiredService<LightFinderScannerService>(),
sp.GetRequiredService<LightFinderPlateHandler>()));
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new 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.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<string> filePaths, CancellationToken token)
{
var normalized = new HashSet<string>(
@@ -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<ObjectKind, CharacterAnalysisObjectSummary>();
@@ -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<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;
public void RefreshFormat()
{
_format = CreateFormatValue();
}
public void RefreshFormat() => _format = CreateFormatValue();
private Lazy<string> CreateFormatValue()
=> new(() =>
{
if (!string.Equals(FileType, "tex", StringComparison.Ordinal))
{
if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
return string.Empty;
}
try
{

View File

@@ -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<RectF> _uiRects = new(128);
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;
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
/// <summary>
/// Draw detour for nameplate addon.
/// </summary>
/// <param name="type"></param>
/// <param name="args"></param>
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
/// </summary>
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
/// <summary>
/// Resolves a cached value for the given index.
/// </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(
int[] cache,
int index,
@@ -775,9 +905,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Snapping a position to pixel grid based on DPI scale.
/// </summary>
/// <param name="p">Position</param>
/// <param name="dpiScale">DPI Scale</param>
/// <returns></returns>
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);
}
/// <summary>
/// Smooths the position using exponential smoothing.
/// </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)
{
// 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;
}
/// <summary>
/// Tries to get a valid screen rect for the given addon.
/// </summary>
/// <param name="addon">Addon UI</param>
/// <param name="screen">Screen positioning/param>
/// <param name="rect">RectF of Addon</param>
/// <returns></returns>
[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<69>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,
};
}
/// <summary>
/// Refreshes the cached UI rects for occlusion checking.
/// </summary>
/// <param name="unitMgr">Unit Manager</param>
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
}
/// <summary>
/// Is the given label rect occluded by any UI rects?
/// </summary>
/// <param name="labelRect">UI/Label Rect</param>
/// <returns>Is occluded or not</returns>
private bool IsOccludedByAnyUi(RectF labelRect)
{
for (int i = 0; i < _uiRects.Count; i++)
@@ -931,8 +1167,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Gets the world scale X of the given node.
/// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleX(AtkResNode* n)
{
var t = n->Transform;
@@ -942,8 +1176,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Gets the world scale Y of the given node.
/// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleY(AtkResNode* n)
{
var t = n->Transform;
@@ -953,8 +1185,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Normalize an icon glyph input into a valid string.
/// </summary>
/// <param name="rawInput">Raw glyph input</param>
/// <returns>Normalized glyph input</returns>
internal static string NormalizeIconGlyph(string? rawInput)
{
if (string.IsNullOrWhiteSpace(rawInput))
@@ -982,7 +1212,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Is the nameplate addon visible?
/// </summary>
/// <returns>Is it visible?</returns>
private bool IsNamePlateAddonVisible()
{
if (_mpNameplateAddon == null)
@@ -992,20 +1221,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
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
{
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
/// <summary>
/// Update the active broadcasting CIDs.
/// </summary>
/// <param name="cids">Inbound new CIDs</param>
public void UpdateBroadcastingCids(IEnumerable<string> 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);
}
}
/// <summary>
/// Starts the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StartAsync(CancellationToken cancellationToken)
{
Init();
@@ -1134,8 +1354,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Stops the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
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);
}
}
}

View File

@@ -968,20 +968,25 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
{
var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);
foreach (var (objectKind, entries) in source)
{
var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(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;
}

View File

@@ -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<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
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);

View File

@@ -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;
}