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:
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>>(),
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user