From 3f1037dade28133c9af1e9be35fbc6767b95eef4 Mon Sep 17 00:00:00 2001 From: defnotken Date: Thu, 13 Nov 2025 23:40:22 -0600 Subject: [PATCH 01/21] Initialize migration. --- LightlessSync/LightlessSync.csproj | 2 +- LightlessSync/packages.lock.json | 18 +++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 726f2ef..5f4ecc4 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -10,7 +10,7 @@ - net9.0-windows7.0 + net10.0-windows x64 enable latest diff --git a/LightlessSync/packages.lock.json b/LightlessSync/packages.lock.json index a7576db..e2bb034 100644 --- a/LightlessSync/packages.lock.json +++ b/LightlessSync/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - "net9.0-windows7.0": { + "net10.0-windows7.0": { "DalamudPackager": { "type": "Direct", "requested": "[13.1.0, )", @@ -173,8 +173,7 @@ "dependencies": { "Microsoft.AspNetCore.Http.Connections.Common": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3", - "System.Net.ServerSentEvents": "9.0.3" + "Microsoft.Extensions.Options": "9.0.3" } }, "Microsoft.AspNetCore.Http.Connections.Common": { @@ -193,8 +192,7 @@ "Microsoft.AspNetCore.SignalR.Common": "9.0.3", "Microsoft.AspNetCore.SignalR.Protocols.Json": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "System.Threading.Channels": "9.0.3" + "Microsoft.Extensions.Logging": "9.0.3" } }, "Microsoft.AspNetCore.SignalR.Common": { @@ -508,16 +506,6 @@ "resolved": "9.0.3", "contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw==" }, - "System.Net.ServerSentEvents": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "Vs/C2V27bjtwLqYag9ATzHilcUn8VQTICre4jSBMGFUeSTxEZffTjb+xZwjcmPsVAjmSZmBI5N7Ezq8UFvqQQg==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "Ao0iegVONKYVw0eWxJv0ArtMVfkFjgyyYKtUXru6xX5H95flSZWW3QCavD4PAgwpc0ETP38kGHaYbPzSE7sw2w==" - }, "lightlesssync.api": { "type": "Project", "dependencies": { -- 2.49.1 From c3597b5789de1e638725093c9f3cdfa7c8a699a0 Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 14 Nov 2025 23:56:39 +0100 Subject: [PATCH 02/21] Compactor multi-threaded, fixed many of IDE warnings --- LightlessSync/FileCache/FileCacheManager.cs | 21 +- LightlessSync/FileCache/FileCompactor.cs | 468 +++++++++++------- LightlessSync/Interop/DalamudLogger.cs | 5 +- .../Compactor/BatchFileFragService.cs | 28 +- LightlessSync/UI/BroadcastUI.cs | 2 +- LightlessSync/UI/CharaDataHubUi.Functions.cs | 8 +- LightlessSync/UI/CompactUI.cs | 11 +- LightlessSync/UI/EditProfileUi.cs | 1 - LightlessSync/UI/Handlers/IdDisplayHandler.cs | 6 +- LightlessSync/UI/StandaloneProfileUi.cs | 2 +- LightlessSync/UI/SyncshellAdminUI.cs | 9 +- LightlessSync/UI/TopTabMenu.cs | 69 +-- LightlessSync/UI/UpdateNotesUi.cs | 9 +- LightlessSync/Utils/FileSystemHelper.cs | 43 +- 14 files changed, 361 insertions(+), 321 deletions(-) diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 7ee6c99..29d2aa7 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -134,13 +134,9 @@ public sealed class FileCacheManager : IHostedService chosenLength = penumbraMatch; } - if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch)) + if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch) && cacheMatch > chosenLength) { - if (cacheMatch > chosenLength) - { - chosenPrefixed = cachePrefixed; - chosenLength = cacheMatch; - } + chosenPrefixed = cachePrefixed; } return NormalizePrefixedPathKey(chosenPrefixed ?? normalized); @@ -602,7 +598,6 @@ public sealed class FileCacheManager : IHostedService private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) { var resultingFileCache = ReplacePathPrefixes(fileCache); - //_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath); resultingFileCache = Validate(resultingFileCache); return resultingFileCache; } @@ -644,7 +639,7 @@ public sealed class FileCacheManager : IHostedService return fileCache; } - public Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting FileCacheManager"); @@ -695,14 +690,14 @@ public sealed class FileCacheManager : IHostedService try { _logger.LogInformation("Attempting to read {csvPath}", _csvPath); - entries = File.ReadAllLines(_csvPath); + entries = await File.ReadAllLinesAsync(_csvPath, cancellationToken).ConfigureAwait(false); success = true; } catch (Exception ex) { attempts++; _logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath); - Task.Delay(100, cancellationToken); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } @@ -823,12 +818,12 @@ public sealed class FileCacheManager : IHostedService _logger.LogInformation("Started FileCacheManager"); - return Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { WriteOutFullCsv(); - return Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); } } \ No newline at end of file diff --git a/LightlessSync/FileCache/FileCompactor.cs b/LightlessSync/FileCache/FileCompactor.cs index 3edf96a..c321f94 100644 --- a/LightlessSync/FileCache/FileCompactor.cs +++ b/LightlessSync/FileCache/FileCompactor.cs @@ -11,7 +11,7 @@ using static LightlessSync.Utils.FileSystemHelper; namespace LightlessSync.FileCache; -public sealed class FileCompactor : IDisposable +public sealed partial class FileCompactor : IDisposable { public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const ulong WOF_PROVIDER_FILE = 2UL; @@ -29,23 +29,26 @@ public sealed class FileCompactor : IDisposable private readonly SemaphoreSlim _globalGate; //Limit btrfs gate on half of threads given to compactor. - private static readonly SemaphoreSlim _btrfsGate = new(4, 4); + private readonly SemaphoreSlim _btrfsGate; private readonly BatchFilefragService _fragBatch; - private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo = new() + private readonly bool _isWindows; + private readonly int _workerCount; + + private readonly WofFileCompressionInfoV1 _efInfo = new() { Algorithm = (int)CompressionAlgorithm.XPRESS8K, Flags = 0 }; [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct WOF_FILE_COMPRESSION_INFO_V1 + private struct WofFileCompressionInfoV1 { public int Algorithm; public ulong Flags; } - private enum CompressionAlgorithm + private enum CompressionAlgorithm { NO_COMPRESSION = -2, LZNT1 = -1, @@ -61,6 +64,7 @@ public sealed class FileCompactor : IDisposable _logger = logger; _lightlessConfigService = lightlessConfigService; _dalamudUtilService = dalamudUtilService; + _isWindows = OperatingSystem.IsWindows(); _compactionQueue = Channel.CreateUnbounded(new UnboundedChannelOptions { @@ -68,29 +72,36 @@ public sealed class FileCompactor : IDisposable SingleWriter = false }); + //Amount of threads given for the compactor int workers = Math.Clamp(Math.Min(Environment.ProcessorCount / 2, 4), 1, 8); + //Setup gates for the threads and setup worker count _globalGate = new SemaphoreSlim(workers, workers); - int workerCount = Math.Max(workers * 2, workers); + _btrfsGate = new SemaphoreSlim(workers / 2, workers / 2); + _workerCount = Math.Max(workers * 2, workers); - for (int i = 0; i < workerCount; i++) + //Setup workers on the queue + for (int i = 0; i < _workerCount; i++) { + int workerId = i; + _workers.Add(Task.Factory.StartNew( - () => ProcessQueueWorkerAsync(_compactionCts.Token), + () => ProcessQueueWorkerAsync(_compactionCts.Token, workerId), _compactionCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap()); } + //Uses an batching service for the filefrag command on Linux _fragBatch = new BatchFilefragService( useShell: _dalamudUtilService.IsWine, log: _logger, batchSize: 64, - flushMs: 25, + flushMs: 25, runDirect: RunProcessDirect, runShell: RunProcessShell ); - _logger.LogInformation("FileCompactor started with {workers} workers", workerCount); + _logger.LogInformation("FileCompactor started with {workers} workers", _workerCount); } public bool MassCompactRunning { get; private set; } @@ -100,37 +111,90 @@ public sealed class FileCompactor : IDisposable /// Compact the storage of the Cache Folder /// /// Used to check if files needs to be compressed - public void CompactStorage(bool compress) + public void CompactStorage(bool compress, int? maxDegree = null) { MassCompactRunning = true; + try { - var allFiles = Directory.EnumerateFiles(_lightlessConfigService.Current.CacheFolder).ToList(); - int total = allFiles.Count; - int current = 0; - - foreach (var file in allFiles) + var folder = _lightlessConfigService.Current.CacheFolder; + if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) { - current++; - Progress = $"{current}/{total}"; + _logger.LogWarning("Filecompacator couldnt find your Cache folder: {folder}", folder); + Progress = "0/0"; + return; + } + + var files = Directory.EnumerateFiles(folder).ToArray(); + var total = files.Length; + Progress = $"0/{total}"; + if (total == 0) return; + + var degree = maxDegree ?? Math.Clamp(Environment.ProcessorCount / 2, 1, 8); + + var done = 0; + int workerCounter = -1; + var po = new ParallelOptions + { + MaxDegreeOfParallelism = degree, + CancellationToken = _compactionCts.Token + }; + + Parallel.ForEach(files, po, localInit: () => Interlocked.Increment(ref workerCounter), body: (file, state, workerId) => + { + _globalGate.WaitAsync(po.CancellationToken).GetAwaiter().GetResult(); + + if (!_pendingCompactions.TryAdd(file, 0)) + return -1; try { - // Compress or decompress files - if (compress) - CompactFile(file); - else - DecompressFile(file); + try + { + if (compress) + { + if (_lightlessConfigService.Current.UseCompactor) + CompactFile(file, workerId); + } + else + { + DecompressFile(file, workerId); + } + } + catch (IOException ioEx) + { + _logger.LogDebug(ioEx, "[W{worker}] File being read/written, skipping file: {file}", workerId, file); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "[W{worker}] Error processing file: {file}", workerId, file); + } + finally + { + var n = Interlocked.Increment(ref done); + Progress = $"{n}/{total}"; + } } - catch (IOException ioEx) + finally { - _logger.LogDebug(ioEx, "File {file} locked or busy, skipping", file); + _pendingCompactions.TryRemove(file, out _); + _globalGate.Release(); } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error compacting/decompressing file {file}", file); - } - } + + return workerId; + }, + localFinally: _ => + { + //Ignore local finally for now + }); + } + catch (OperationCanceledException ex) + { + _logger.LogDebug(ex, "Mass compaction call got cancelled, shutting off compactor."); } finally { @@ -139,6 +203,7 @@ public sealed class FileCompactor : IDisposable } } + /// /// Write all bytes into a directory async /// @@ -197,24 +262,32 @@ public sealed class FileCompactor : IDisposable { try { - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - var (_, linuxPath) = ResolvePathsForBtrfs(fileInfo.FullName); + bool isWine = _dalamudUtilService?.IsWine ?? false; - var (ok, output, err, code) = - isWindowsProc - ? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000) - : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000); + string linuxPath = isWine ? ToLinuxPathIfWine(fileInfo.FullName, isWine) + : fileInfo.FullName; - if (ok && long.TryParse(output.Trim(), out long blocks)) - return (false, blocks * 512L); // st_blocks are always 512B units + (bool ok, string so, string se, int code) res; - _logger.LogDebug("Btrfs size probe failed for {linux} (stat {code}, err {err}). Falling back to Length.", linuxPath, code, err); - return (false, fileInfo.Length); + res = isWine + ? RunProcessShell($"stat -c %b -- {QuoteSingle(linuxPath)}", timeoutMs: 10000) + : RunProcessDirect("stat", ["-c", "%b", "--", linuxPath], "/", 10000); + + var outTrim = res.so?.Trim() ?? ""; + + if (res.ok && long.TryParse(outTrim, out long blocks) && blocks >= 0) + { + // st_blocks are 512-byte units + return (flowControl: false, value: blocks * 512L); + } + + _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim()); + return (flowControl: false, value: fileInfo.Length); } catch (Exception ex) { _logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); - return (false, fileInfo.Length); + return (flowControl: true, value: fileInfo.Length); } } @@ -243,19 +316,20 @@ public sealed class FileCompactor : IDisposable /// /// Compressing the given path with BTRFS or NTFS file system. /// - /// Path of the decompressed/normal file - private void CompactFile(string filePath) + /// Path of the decompressed/normal file + /// Worker/Process Id + private void CompactFile(string filePath, int workerId) { var fi = new FileInfo(filePath); if (!fi.Exists) { - _logger.LogTrace("Skip compaction: missing {file}", filePath); + _logger.LogTrace("[W{worker}] Skip compaction: missing {file}", workerId, filePath); return; } var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); var oldSize = fi.Length; - int blockSize = GetBlockSizeForPath(fi.FullName, _logger, _dalamudUtilService.IsWine); + int blockSize = (int)(GetFileSizeOnDisk(fi) / 512); // We skipping small files (128KiB) as they slow down the system a lot for BTRFS. as BTRFS has a different blocksize it requires an different calculation. long minSizeBytes = fsType == FilesystemType.Btrfs @@ -264,7 +338,7 @@ public sealed class FileCompactor : IDisposable if (oldSize < minSizeBytes) { - _logger.LogTrace("Skip compaction: {file} ({size} B) < threshold ({th} B)", filePath, oldSize, minSizeBytes); + _logger.LogTrace("[W{worker}] Skip compaction: {file} ({size} B) < threshold ({th} B)", workerId, filePath, oldSize, minSizeBytes); return; } @@ -272,20 +346,19 @@ public sealed class FileCompactor : IDisposable { if (!IsWOFCompactedFile(filePath)) { - _logger.LogDebug("NTFS compaction XPRESS8K: {file}", filePath); if (WOFCompressFile(filePath)) { var newSize = GetFileSizeOnDisk(fi); - _logger.LogDebug("NTFS compressed {file} {old} -> {new}", filePath, oldSize, newSize); + _logger.LogDebug("[W{worker}] NTFS compressed XPRESS8K {file} {old} -> {new}", workerId, filePath, oldSize, newSize); } else { - _logger.LogWarning("NTFS compression failed or unavailable for {file}", filePath); + _logger.LogWarning("[W{worker}] NTFS compression failed or unavailable for {file}", workerId, filePath); } } else { - _logger.LogTrace("Already NTFS-compressed: {file}", filePath); + _logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath); } return; } @@ -294,41 +367,41 @@ public sealed class FileCompactor : IDisposable { if (!IsBtrfsCompressedFile(filePath)) { - _logger.LogDebug("Btrfs compression zstd: {file}", filePath); if (BtrfsCompressFile(filePath)) { var newSize = GetFileSizeOnDisk(fi); - _logger.LogDebug("Btrfs compressed {file} {old} -> {new}", filePath, oldSize, newSize); + _logger.LogDebug("[W{worker}] Btrfs compressed clzo {file} {old} -> {new}", workerId, filePath, oldSize, newSize); } else { - _logger.LogWarning("Btrfs compression failed or unavailable for {file}", filePath); + _logger.LogWarning("[W{worker}] Btrfs compression failed or unavailable for {file}", workerId, filePath); } } else { - _logger.LogTrace("Already Btrfs-compressed: {file}", filePath); + _logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath); } return; } - _logger.LogTrace("Skip compact: unsupported FS for {file}", filePath); + _logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath); } /// /// Decompressing the given path with BTRFS file system or NTFS file system. /// - /// Path of the compressed file - private void DecompressFile(string path) + /// Path of the decompressed/normal file + /// Worker/Process Id + private void DecompressFile(string filePath, int workerId) { - _logger.LogDebug("Decompress request: {file}", path); - var fsType = GetFilesystemType(path, _dalamudUtilService.IsWine); + _logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath); + var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine) { try { - bool flowControl = DecompressWOFFile(path); + bool flowControl = DecompressWOFFile(filePath, workerId); if (!flowControl) { return; @@ -336,7 +409,7 @@ public sealed class FileCompactor : IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "NTFS decompress error {file}", path); + _logger.LogWarning(ex, "[W{worker}] NTFS decompress error {file}", workerId, filePath); } } @@ -344,7 +417,7 @@ public sealed class FileCompactor : IDisposable { try { - bool flowControl = DecompressBtrfsFile(path); + bool flowControl = DecompressBtrfsFile(filePath); if (!flowControl) { return; @@ -352,7 +425,7 @@ public sealed class FileCompactor : IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "Btrfs decompress error {file}", path); + _logger.LogWarning(ex, "[W{worker}] Btrfs decompress error {file}", workerId, filePath); } } } @@ -372,51 +445,48 @@ public sealed class FileCompactor : IDisposable string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path; var opts = GetMountOptionsForPath(linuxPath); - bool hasCompress = opts.Contains("compress", StringComparison.OrdinalIgnoreCase); - bool hasCompressForce = opts.Contains("compress-force", StringComparison.OrdinalIgnoreCase); + if (!string.IsNullOrEmpty(opts)) + _logger.LogTrace("Mount opts for {file}: {opts}", linuxPath, opts); - if (hasCompressForce) + var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000); + var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout); + if (!_btrfsAvailable) + _logger.LogWarning("btrfs cli not found in path. Compression will be skipped."); + + var prop = isWine + ? RunProcessShell($"btrfs property set -- {QuoteSingle(linuxPath)} compression none", timeoutMs: 15000) + : RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"], "/", 15000); + + if (prop.ok) _logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath); + else _logger.LogTrace("btrfs property set failed for {file} (exit {code}): {err}", linuxPath, prop.exitCode, prop.stderr); + + var defrag = isWine + ? RunProcessShell($"btrfs filesystem defragment -f -- {QuoteSingle(linuxPath)}", timeoutMs: 60000) + : RunProcessDirect("btrfs", ["filesystem", "defragment", "-f", "--", linuxPath], "/", 60000); + + if (!defrag.ok) { - _logger.LogWarning("Cannot safely decompress {file}: mount options contains compress-force ({opts}).", linuxPath, opts); + _logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit {code}): {err}", + linuxPath, defrag.exitCode, defrag.stderr); return false; } - if (hasCompress) - { - var setCmd = $"btrfs property set -- {QuoteDouble(linuxPath)} compression none"; - var (okSet, _, errSet, codeSet) = isWine - ? RunProcessShell(setCmd) - : RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"]); - - if (!okSet) - { - _logger.LogWarning("Failed to set 'compression none' on {file}, please check drive options (exit code is: {code}): {err}", linuxPath, codeSet, errSet); - return false; - } - _logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath); - } - - if (!IsBtrfsCompressedFile(linuxPath)) - { - _logger.LogTrace("{file} is not compressed, skipping decompression completely", linuxPath); - return true; - } - - var (ok, stdout, stderr, code) = isWine - ? RunProcessShell($"btrfs filesystem defragment -- {QuoteDouble(linuxPath)}") - : RunProcessDirect("btrfs", ["filesystem", "defragment", "--", linuxPath]); - - if (!ok) - { - _logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit code is: {code}): {stderr}", - linuxPath, code, stderr); - return false; - } - - if (!string.IsNullOrWhiteSpace(stdout)) - _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, stdout.Trim()); + if (!string.IsNullOrWhiteSpace(defrag.stdout)) + _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, defrag.stdout.Trim()); _logger.LogInformation("Decompressed (rewritten uncompressed) Btrfs file: {file}", linuxPath); + + try + { + if (_fragBatch != null) + { + var compressed = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token).GetAwaiter().GetResult(); + if (compressed) + _logger.LogTrace("Post-check: {file} still shows 'compressed' flag (may be stale).", linuxPath); + } + } + catch { /* ignore verification noisy */ } + return true; } catch (Exception ex) @@ -432,18 +502,18 @@ public sealed class FileCompactor : IDisposable /// /// Path of the compressed file /// Decompressing state - private bool DecompressWOFFile(string path) + private bool DecompressWOFFile(string path, int workerID) { //Check if its already been compressed if (TryIsWofExternal(path, out bool isExternal, out int algo)) { if (!isExternal) { - _logger.LogTrace("Already decompressed file: {file}", path); + _logger.LogTrace("[W{worker}] Already decompressed file: {file}", workerID, path); return true; } var compressString = ((CompressionAlgorithm)algo).ToString(); - _logger.LogTrace("WOF compression (algo={algo}) detected for {file}", compressString, path); + _logger.LogTrace("[W{worker}] WOF compression (algo={algo}) detected for {file}", workerID, compressString, path); } //This will attempt to start WOF thread. @@ -457,15 +527,15 @@ public sealed class FileCompactor : IDisposable // 342 error code means its been decompressed after the control, we handle it as it succesfully been decompressed. if (err == 342) { - _logger.LogTrace("Successfully decompressed NTFS file {file}", path); + _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path); return true; } - _logger.LogWarning("DeviceIoControl failed for {file} with Win32 error {err}", path, err); + _logger.LogWarning("[W{worker}] DeviceIoControl failed for {file} with Win32 error {err}", workerID, path, err); return false; } - _logger.LogTrace("Successfully decompressed NTFS file {file}", path); + _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path); return true; }); } @@ -478,6 +548,7 @@ public sealed class FileCompactor : IDisposable /// Converted path to be used in Linux private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true) { + //Return if not wine if (!isWine || !IsProbablyWine()) return path; @@ -539,7 +610,7 @@ public sealed class FileCompactor : IDisposable /// Compessing state private bool WOFCompressFile(string path) { - int size = Marshal.SizeOf(); + int size = Marshal.SizeOf(); IntPtr efInfoPtr = Marshal.AllocHGlobal(size); try @@ -592,7 +663,7 @@ public sealed class FileCompactor : IDisposable { try { - uint buf = (uint)Marshal.SizeOf(); + uint buf = (uint)Marshal.SizeOf(); int result = WofIsExternalFile(filePath, out int isExternal, out _, out var info, ref buf); if (result != 0 || isExternal == 0) return false; @@ -621,7 +692,7 @@ public sealed class FileCompactor : IDisposable algorithm = 0; try { - uint buf = (uint)Marshal.SizeOf(); + uint buf = (uint)Marshal.SizeOf(); int hr = WofIsExternalFile(path, out int ext, out _, out var info, ref buf); if (hr == 0 && ext != 0) { @@ -630,13 +701,13 @@ public sealed class FileCompactor : IDisposable } return true; } - catch (DllNotFoundException) + catch (DllNotFoundException) { - return false; + return false; } - catch (EntryPointNotFoundException) - { - return false; + catch (EntryPointNotFoundException) + { + return false; } } @@ -651,8 +722,7 @@ public sealed class FileCompactor : IDisposable { try { - bool windowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - string linuxPath = windowsProc ? ResolveLinuxPathForWine(path) : path; + string linuxPath = _isWindows ? ResolveLinuxPathForWine(path) : path; var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token); @@ -685,7 +755,6 @@ public sealed class FileCompactor : IDisposable try { var (winPath, linuxPath) = ResolvePathsForBtrfs(path); - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); if (IsBtrfsCompressedFile(linuxPath)) { @@ -699,8 +768,13 @@ public sealed class FileCompactor : IDisposable return false; } + var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000); + var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout); + if (!_btrfsAvailable) + _logger.LogWarning("btrfs cli not found in path. Compression will be skipped."); + (bool ok, string stdout, string stderr, int code) = - isWindowsProc + _isWindows ? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}") : RunProcessDirect("btrfs", ["filesystem", "defragment", "-clzo", "--", linuxPath]); @@ -783,9 +857,10 @@ public sealed class FileCompactor : IDisposable RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, + WorkingDirectory = workingDir ?? "/", }; - if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir; + foreach (var a in args) psi.ArgumentList.Add(a); EnsureUnixPathEnv(psi); @@ -799,8 +874,18 @@ public sealed class FileCompactor : IDisposable } int code; - try { code = proc.ExitCode; } catch { code = -1; } - return (code == 0, so2, se2, code); + try { code = proc.ExitCode; } + catch { code = -1; } + + bool ok = code == 0; + + if (!ok && code == -1 && + string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2)) + { + ok = true; + } + + return (ok, so2, se2, code); } /// @@ -811,15 +896,14 @@ public sealed class FileCompactor : IDisposable /// State of the process, output of the process and error with exit code private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000) { - var psi = new ProcessStartInfo("/bin/bash") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, + WorkingDirectory = workingDir ?? "/", }; - if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir; // Use a Login shell so PATH includes /usr/sbin etc. AKA -lc for login shell psi.ArgumentList.Add("-lc"); @@ -836,65 +920,72 @@ public sealed class FileCompactor : IDisposable } int code; - try { code = proc.ExitCode; } catch { code = -1; } - return (code == 0, so2, se2, code); + try { code = proc.ExitCode; } + catch { code = -1; } + + bool ok = code == 0; + + if (!ok && code == -1 && string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2)) + { + ok = true; + } + + return (ok, so2, se2, code); } /// /// Checking the process result for shell or direct processes /// /// Process - /// How long when timeout is gotten + /// How long when timeout goes over threshold /// Cancellation Token /// Multiple variables - private (bool success, string testy, string testi) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token) + private (bool success, string output, string errorCode) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token) { var outTask = proc.StandardOutput.ReadToEndAsync(token); var errTask = proc.StandardError.ReadToEndAsync(token); var bothTasks = Task.WhenAll(outTask, errTask); - //On wine, we dont wanna use waitforexit as it will be always broken and giving an error. - if (_dalamudUtilService.IsWine) - { - var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult(); - if (finished != bothTasks) - { - try - { - proc.Kill(entireProcessTree: true); - Task.WaitAll([outTask, errTask], 1000, token); - } - catch - { - // ignore this - } - var so = outTask.IsCompleted ? outTask.Result : ""; - var se = errTask.IsCompleted ? errTask.Result : "timeout"; - return (false, so, se); - } + var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult(); - var stderr = errTask.Result; - var ok = string.IsNullOrWhiteSpace(stderr); - return (ok, outTask.Result, stderr); + if (token.IsCancellationRequested) + return KillProcess(proc, outTask, errTask, token); + + if (finished != bothTasks) + return KillProcess(proc, outTask, errTask, token); + + bool isWine = _dalamudUtilService?.IsWine ?? false; + if (!isWine) + { + try { proc.WaitForExit(); } catch { /* ignore quirks */ } + } + else + { + var sw = Stopwatch.StartNew(); + while (!proc.HasExited && sw.ElapsedMilliseconds < 75) + Thread.Sleep(5); } - // On linux, we can use it as we please - if (!proc.WaitForExit(timeoutMs)) - { - try - { - proc.Kill(entireProcessTree: true); - Task.WaitAll([outTask, errTask], 1000, token); - } - catch - { - // ignore this - } - return (false, outTask.IsCompleted ? outTask.Result : "", "timeout"); - } + var stdout = outTask.Status == TaskStatus.RanToCompletion ? outTask.Result : ""; + var stderr = errTask.Status == TaskStatus.RanToCompletion ? errTask.Result : ""; - Task.WaitAll(outTask, errTask); - return (true, outTask.Result, errTask.Result); + int code = -1; + try { if (proc.HasExited) code = proc.ExitCode; } catch { /* Wine may still throw */ } + + bool ok = code == 0 || (isWine && string.IsNullOrWhiteSpace(stderr)); + + return (ok, stdout, stderr); + + static (bool success, string output, string errorCode) KillProcess( + Process proc, Task outTask, Task errTask, CancellationToken token) + { + try { proc.Kill(entireProcessTree: true); } catch { /* ignore */ } + try { Task.WaitAll([outTask, errTask], 1000, token); } catch { /* ignore */ } + + var so = outTask.IsCompleted ? outTask.Result : ""; + var se = errTask.IsCompleted ? errTask.Result : "canceled/timeout"; + return (false, so, se); + } } /// @@ -957,7 +1048,7 @@ public sealed class FileCompactor : IDisposable /// Process the queue with, meant for a worker/thread /// /// Cancellation token for the worker whenever it needs to be stopped - private async Task ProcessQueueWorkerAsync(CancellationToken token) + private async Task ProcessQueueWorkerAsync(CancellationToken token, int workerId) { try { @@ -973,7 +1064,7 @@ public sealed class FileCompactor : IDisposable try { if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath)) - CompactFile(filePath); + CompactFile(filePath, workerId); } finally { @@ -992,8 +1083,8 @@ public sealed class FileCompactor : IDisposable } } } - catch (OperationCanceledException) - { + catch (OperationCanceledException) + { // Shutting down worker, this exception is expected } } @@ -1005,7 +1096,7 @@ public sealed class FileCompactor : IDisposable /// Linux path to be used in Linux private string ResolveLinuxPathForWine(string windowsPath) { - var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", null, 5000); + var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", workingDir: null, 5000); if (ok && !string.IsNullOrWhiteSpace(outp)) return outp.Trim(); return ToLinuxPathIfWine(windowsPath, isWine: true); } @@ -1029,9 +1120,7 @@ public sealed class FileCompactor : IDisposable /// private (string windowsPath, string linuxPath) ResolvePathsForBtrfs(string path) { - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - if (!isWindowsProc) + if (!_isWindows) return (path, path); var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(path)}", workingDir: null, 5000); @@ -1050,7 +1139,7 @@ public sealed class FileCompactor : IDisposable { try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (_isWindows) { using var _ = new FileStream(winePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); } @@ -1085,17 +1174,18 @@ public sealed class FileCompactor : IDisposable } - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); + [LibraryImport("kernel32.dll", SetLastError = true)] + private static partial uint GetCompressedFileSizeW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName, out uint lpFileSizeHigh); - [DllImport("kernel32.dll")] - private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName, [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); - [DllImport("WofUtil.dll")] - private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WOF_FILE_COMPRESSION_INFO_V1 Info, ref uint BufferLength); + [LibraryImport("WofUtil.dll")] + private static partial int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WofFileCompressionInfoV1 Info, ref uint BufferLength); - [DllImport("WofUtil.dll", SetLastError = true)] - private static extern int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); + [LibraryImport("WofUtil.dll")] + private static partial int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'"; @@ -1103,7 +1193,11 @@ public sealed class FileCompactor : IDisposable public void Dispose() { + //Cleanup of gates and frag service _fragBatch?.Dispose(); + _btrfsGate?.Dispose(); + _globalGate?.Dispose(); + _compactionQueue.Writer.TryComplete(); _compactionCts.Cancel(); @@ -1111,8 +1205,8 @@ public sealed class FileCompactor : IDisposable { Task.WaitAll([.. _workers.Where(t => t != null)], TimeSpan.FromSeconds(5)); } - catch - { + catch + { // Ignore this catch on the dispose } finally diff --git a/LightlessSync/Interop/DalamudLogger.cs b/LightlessSync/Interop/DalamudLogger.cs index 3a833b9..24fcac2 100644 --- a/LightlessSync/Interop/DalamudLogger.cs +++ b/LightlessSync/Interop/DalamudLogger.cs @@ -20,7 +20,10 @@ internal sealed class DalamudLogger : ILogger _hasModifiedGameFiles = hasModifiedGameFiles; } - public IDisposable BeginScope(TState state) => default!; + IDisposable? ILogger.BeginScope(TState state) + { + return default!; + } public bool IsEnabled(LogLevel logLevel) { diff --git a/LightlessSync/Services/Compactor/BatchFileFragService.cs b/LightlessSync/Services/Compactor/BatchFileFragService.cs index b31919e..b99934b 100644 --- a/LightlessSync/Services/Compactor/BatchFileFragService.cs +++ b/LightlessSync/Services/Compactor/BatchFileFragService.cs @@ -92,13 +92,13 @@ namespace LightlessSync.Services.Compactor } if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break; - try - { - await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); + try + { + await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); } - catch - { - break; + catch + { + break; } } @@ -124,8 +124,8 @@ namespace LightlessSync.Services.Compactor } } } - catch (OperationCanceledException) - { + catch (OperationCanceledException) + { //Shutting down worker, exception called } } @@ -145,17 +145,13 @@ namespace LightlessSync.Services.Compactor if (_useShell) { - var inner = "filefrag -v " + string.Join(' ', list.Select(QuoteSingle)); + var inner = "filefrag -v -- " + string.Join(' ', list.Select(QuoteSingle)); res = _runShell(inner, timeoutMs: 15000, workingDir: "/"); } else { - var args = new List { "-v" }; - foreach (var path in list) - { - args.Add(' ' + path); - } - + var args = new List { "-v", "--" }; + args.AddRange(list); res = _runDirect("filefrag", args, workingDir: "/", timeoutMs: 15000); } @@ -200,7 +196,7 @@ namespace LightlessSync.Services.Compactor /// Regex of the File Size return on the Linux/Wine systems, giving back the amount /// /// Regex of the File Size - [GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant,matchTimeoutMilliseconds: 500)] + [GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 500)] private static partial Regex SizeRegex(); /// diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index c760a45..60e064f 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -22,7 +22,7 @@ namespace LightlessSync.UI private readonly UiSharedService _uiSharedService; private readonly BroadcastScannerService _broadcastScannerService; - private IReadOnlyList _allSyncshells; + private IReadOnlyList _allSyncshells = Array.Empty(); private string _userUid = string.Empty; private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); diff --git a/LightlessSync/UI/CharaDataHubUi.Functions.cs b/LightlessSync/UI/CharaDataHubUi.Functions.cs index 665e640..ccef174 100644 --- a/LightlessSync/UI/CharaDataHubUi.Functions.cs +++ b/LightlessSync/UI/CharaDataHubUi.Functions.cs @@ -13,13 +13,15 @@ internal sealed partial class CharaDataHubUi AccessTypeDto.AllPairs => "All Pairs", AccessTypeDto.ClosePairs => "Direct Pairs", AccessTypeDto.Individuals => "Specified", - AccessTypeDto.Public => "Everyone" + AccessTypeDto.Public => "Everyone", + _ => throw new NotSupportedException() }; private static string GetShareTypeString(ShareTypeDto dto) => dto switch { ShareTypeDto.Private => "Code Only", - ShareTypeDto.Shared => "Shared" + ShareTypeDto.Shared => "Shared", + _ => throw new NotSupportedException() }; private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry) @@ -31,7 +33,7 @@ internal sealed partial class CharaDataHubUi private void GposeMetaInfoAction(Action gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); sb.AppendLine(actionDescription); bool isDisabled = false; diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index cc8d326..723d3ae 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -29,6 +29,7 @@ using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; +using System.Runtime.InteropServices; namespace LightlessSync.UI; @@ -105,7 +106,7 @@ public class CompactUi : WindowMediatorSubscriberBase _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; _broadcastService = broadcastService; - _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); + _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService); AllowPinning = true; AllowClickthrough = false; @@ -285,11 +286,10 @@ public class CompactUi : WindowMediatorSubscriberBase private void DrawPairs() { - var ySize = _transferPartHeight == 0 + float ySize = Math.Abs(_transferPartHeight) < 0.0001f ? 1 - : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y - + ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY(); - + : ((ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y + + ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY()); ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false); foreach (var item in _drawFolders) @@ -470,6 +470,7 @@ public class CompactUi : WindowMediatorSubscriberBase return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); } + [StructLayout(LayoutKind.Auto)] private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) { public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 5dedf81..ef64f7f 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -297,7 +297,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(5)); var font = UiBuilder.MonoFont; - var playerUID = _apiController.UID; var playerDisplay = _apiController.DisplayName; var previewTextColor = textEnabled ? textColor : Vector4.One; diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 4d362a9..4955b48 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -154,13 +154,11 @@ public class IdDisplayHandler Vector2 itemMin; Vector2 itemMax; - Vector2 textSize; using (ImRaii.PushFont(font, textIsUid)) { SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); itemMin = ImGui.GetItemRectMin(); itemMax = ImGui.GetItemRectMax(); - //textSize = itemMax - itemMin; } if (useHighlight) @@ -202,7 +200,7 @@ public class IdDisplayHandler if (ImGui.IsItemHovered()) { - if (!string.Equals(_lastMouseOverUid, id)) + if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal)) { _popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay); } @@ -223,7 +221,7 @@ public class IdDisplayHandler } else { - if (string.Equals(_lastMouseOverUid, id)) + if (string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal)) { _mediator.Publish(new ProfilePopoutToggle(Pair: null)); _lastMouseOverUid = string.Empty; diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index 6ef21d5..ebe5a0b 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -139,7 +139,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase } } - if (Pair.UserPair.Groups.Any()) + if (Pair.UserPair?.Groups?.Count > 0) { ImGui.TextUnformatted("Paired through Syncshells:"); foreach (var group in Pair.UserPair.Groups) diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index be8e1d4..0967290 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -222,6 +222,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase { ImGui.Dummy(new Vector2(5)); + if (_profileData == null) + { + UiSharedService.ColorTextWrapped("Failed to load profile data.", ImGuiColors.DalamudRed); + ImGui.TreePop(); + return; + } + if (!_profileImage.SequenceEqual(_profileData.ImageData.Value)) { _profileImage = _profileData.ImageData.Value; @@ -379,7 +386,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("Clears your profile description text"); ImGui.Separator(); ImGui.TextUnformatted($"Profile Options:"); - var isNsfw = _profileData.IsNsfw; + var isNsfw = _profileData?.IsNsfw ?? false; if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) { _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: isNsfw, IsDisabled: null)); diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index b4327c0..f0036ba 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -9,7 +9,6 @@ using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; -using LightlessSync.Utils; using LightlessSync.WebAPI; using System.Numerics; @@ -24,27 +23,21 @@ public class TopTabMenu private readonly PairManager _pairManager; private readonly PairRequestService _pairRequestService; - private readonly DalamudUtilService _dalamudUtilService; private readonly HashSet _pendingPairRequestActions = new(StringComparer.Ordinal); - private bool _pairRequestsExpanded; // useless for now - private int _lastRequestCount; private readonly UiSharedService _uiSharedService; - private readonly NotificationService _lightlessNotificationService; private string _filter = string.Empty; private int _globalControlCountdown = 0; private float _pairRequestsHeight = 150f; private string _pairToAdd = string.Empty; - private SelectedTab _selectedTab = SelectedTab.None; - public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) + + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService) { _lightlessMediator = lightlessMediator; _apiController = apiController; _pairManager = pairManager; _pairRequestService = pairRequestService; - _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; - _lightlessNotificationService = lightlessNotificationService; } private enum SelectedTab @@ -70,13 +63,17 @@ public class TopTabMenu _filter = value; } } - private SelectedTab TabSelection + + private SelectedTab GetTabSelection() { - get => _selectedTab; set - { - _selectedTab = value; - } + return _selectedTab; } + + private void SetTabSelection(SelectedTab value) + { + _selectedTab = value; + } + public void Draw() { var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; @@ -85,7 +82,7 @@ public class TopTabMenu var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y; var buttonSize = new Vector2(buttonX, buttonY); var drawList = ImGui.GetWindowDrawList(); - var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator); + var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))); ImGuiHelpers.ScaledDummy(spacing.Y / 2f); @@ -95,11 +92,11 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual; + SetTabSelection(GetTabSelection() == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Individual) + if (GetTabSelection() == SelectedTab.Individual) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -111,11 +108,11 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell; + SetTabSelection(GetTabSelection() == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Syncshell) + if (GetTabSelection() == SelectedTab.Syncshell) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -128,12 +125,12 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder; + SetTabSelection(GetTabSelection() == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Lightfinder) + if (GetTabSelection() == SelectedTab.Lightfinder) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -146,12 +143,12 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig; + SetTabSelection(GetTabSelection() == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.UserConfig) + if (GetTabSelection() == SelectedTab.UserConfig) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -161,7 +158,7 @@ public class TopTabMenu ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) { - var x = ImGui.GetCursorScreenPos(); + ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) { _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); @@ -175,26 +172,26 @@ public class TopTabMenu ImGuiHelpers.ScaledDummy(spacing); - if (TabSelection == SelectedTab.Individual) + if (GetTabSelection() == SelectedTab.Individual) { DrawAddPair(availableWidth, spacing.X); DrawGlobalIndividualButtons(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.Syncshell) + else if (GetTabSelection() == SelectedTab.Syncshell) { DrawSyncshellMenu(availableWidth, spacing.X); DrawGlobalSyncshellButtons(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.Lightfinder) + else if (GetTabSelection() == SelectedTab.Lightfinder) { DrawLightfinderMenu(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.UserConfig) + else if (GetTabSelection() == SelectedTab.UserConfig) { DrawUserConfig(availableWidth, spacing.X); } - if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); + if (GetTabSelection() != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); DrawIncomingPairRequests(availableWidth); @@ -227,17 +224,9 @@ public class TopTabMenu var count = requests.Count; if (count == 0) { - _pairRequestsExpanded = false; - _lastRequestCount = 0; return; } - if (count > _lastRequestCount) - { - _pairRequestsExpanded = true; - } - _lastRequestCount = count; - var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader"; using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple"))) @@ -245,16 +234,12 @@ public class TopTabMenu using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple"))) { bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen); - _pairRequestsExpanded = open; if (ImGui.IsItemHovered()) UiSharedService.AttachToolTip("Expand to view incoming pair requests."); if (open) { - var lineHeight = ImGui.GetTextLineHeightWithSpacing(); - //var desiredHeight = Math.Clamp(count * lineHeight * 2f, 130f * ImGuiHelpers.GlobalScale, 185f * ImGuiHelpers.GlobalScale); we use resize bar instead - ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f); using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple"))) @@ -300,7 +285,6 @@ public class TopTabMenu { float playerColWidth = 207f * ImGuiHelpers.GlobalScale; float receivedColWidth = 73f * ImGuiHelpers.GlobalScale; - float actionsColWidth = 50f * ImGuiHelpers.GlobalScale; ImGui.Separator(); ImGui.TextUnformatted("Player"); @@ -385,7 +369,6 @@ public class TopTabMenu try { - var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false); _pairRequestService.RemoveRequest(request.HashedCid); diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 02e0b4d..54a45f0 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -25,7 +25,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private ChangelogFile _changelog = new(); private CreditsFile _credits = new(); private bool _scrollToTop; - private int _selectedTab; private bool _hasInitializedCollapsingHeaders; private struct Particle @@ -160,7 +159,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase DrawParticleEffects(headerStart, extendedParticleSize); } - private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) + private static void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) { var drawList = ImGui.GetWindowDrawList(); @@ -188,7 +187,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } } - private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) + private static void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) { var drawList = ImGui.GetWindowDrawList(); var gradientHeight = 60f; @@ -513,7 +512,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { if (changelogTab) { - _selectedTab = 0; DrawChangelog(); } } @@ -524,7 +522,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { if (creditsTab) { - _selectedTab = 1; DrawCredits(); } } @@ -558,7 +555,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } } - private void DrawCreditCategory(CreditCategory category) + private static void DrawCreditCategory(CreditCategory category) { DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue")); diff --git a/LightlessSync/Utils/FileSystemHelper.cs b/LightlessSync/Utils/FileSystemHelper.cs index d63b3b9..b27fb1c 100644 --- a/LightlessSync/Utils/FileSystemHelper.cs +++ b/LightlessSync/Utils/FileSystemHelper.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -using System.Diagnostics; using System.Runtime.InteropServices; namespace LightlessSync.Utils @@ -32,7 +31,7 @@ namespace LightlessSync.Utils { string rootPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) + if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine)) { var info = new FileInfo(filePath); var dir = info.Directory ?? new DirectoryInfo(filePath); @@ -50,7 +49,7 @@ namespace LightlessSync.Utils FilesystemType detected; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) + if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine)) { var root = new DriveInfo(rootPath); var format = root.DriveFormat?.ToUpperInvariant() ?? string.Empty; @@ -157,7 +156,7 @@ namespace LightlessSync.Utils return mountOptions; } - catch (Exception ex) + catch (Exception) { return string.Empty; } @@ -214,7 +213,7 @@ namespace LightlessSync.Utils if (_blockSizeCache.TryGetValue(root, out int cached)) return cached; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !isWine) + if (OperatingSystem.IsWindows() && !isWine) { int result = GetDiskFreeSpaceW(root, out uint sectorsPerCluster, @@ -234,40 +233,6 @@ namespace LightlessSync.Utils return clusterSize; } - string realPath = fi.FullName; - if (isWine && realPath.StartsWith("Z:\\", StringComparison.OrdinalIgnoreCase)) - { - realPath = "/" + realPath.Substring(3).Replace('\\', '/'); - } - - var psi = new ProcessStartInfo - { - FileName = "/bin/bash", - Arguments = $"-c \"stat -f -c %s '{realPath.Replace("'", "'\\''")}'\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = "/" - }; - - using var proc = Process.Start(psi); - - string stdout = proc?.StandardOutput.ReadToEnd().Trim() ?? ""; - string _stderr = proc?.StandardError.ReadToEnd() ?? ""; - - try { proc?.WaitForExit(); } - catch (Exception ex) { logger?.LogTrace(ex, "stat WaitForExit failed under Wine; ignoring"); } - - if (!(!int.TryParse(stdout, out int block) || block <= 0)) - { - _blockSizeCache[root] = block; - logger?.LogTrace("Filesystem block size via stat for {root}: {block}", root, block); - return block; - } - - logger?.LogTrace("stat did not return valid block size for {file}, output: {out}", fi.FullName, stdout); - _blockSizeCache[root] = _defaultBlockSize; return _defaultBlockSize; } catch (Exception ex) -- 2.49.1 From acc85939960f7778f2cd99df9a2a21ea4ac76940 Mon Sep 17 00:00:00 2001 From: cake Date: Sat, 15 Nov 2025 00:03:26 +0100 Subject: [PATCH 03/21] Removal Dalamudpackager as its included in sdk. --- LightlessSync/FileCache/FileState.cs | 1 + LightlessSync/LightlessSync.csproj | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/FileCache/FileState.cs b/LightlessSync/FileCache/FileState.cs index dfad917..0a1088b 100644 --- a/LightlessSync/FileCache/FileState.cs +++ b/LightlessSync/FileCache/FileState.cs @@ -5,4 +5,5 @@ public enum FileState Valid, RequireUpdate, RequireDeletion, + RequireRehash } \ No newline at end of file diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 5f4ecc4..abe4a58 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -27,7 +27,6 @@ - -- 2.49.1 From e4a28c70ebcb236721d6d9c89d3ed56c7de8a811 Mon Sep 17 00:00:00 2001 From: cake Date: Sat, 15 Nov 2025 00:46:14 +0100 Subject: [PATCH 04/21] Warning cleansing process --- LightlessSync/FileCache/FileCompactor.cs | 53 +++++++++++-------- LightlessSync/Plugin.cs | 2 +- .../Services/BroadcastScanningService.cs | 45 ++++++++-------- LightlessSync/UI/EditProfileUi.cs | 39 +++++++------- LightlessSync/Utils/VariousExtensions.cs | 4 +- 5 files changed, 80 insertions(+), 63 deletions(-) diff --git a/LightlessSync/FileCache/FileCompactor.cs b/LightlessSync/FileCache/FileCompactor.cs index c321f94..c1a0532 100644 --- a/LightlessSync/FileCache/FileCompactor.cs +++ b/LightlessSync/FileCache/FileCompactor.cs @@ -85,7 +85,7 @@ public sealed partial class FileCompactor : IDisposable int workerId = i; _workers.Add(Task.Factory.StartNew( - () => ProcessQueueWorkerAsync(_compactionCts.Token, workerId), + () => ProcessQueueWorkerAsync(workerId, _compactionCts.Token), _compactionCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap()); @@ -120,7 +120,8 @@ public sealed partial class FileCompactor : IDisposable var folder = _lightlessConfigService.Current.CacheFolder; if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) { - _logger.LogWarning("Filecompacator couldnt find your Cache folder: {folder}", folder); + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Filecompacator couldnt find your Cache folder: {folder}", folder); Progress = "0/0"; return; } @@ -281,12 +282,15 @@ public sealed partial class FileCompactor : IDisposable return (flowControl: false, value: blocks * 512L); } - _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim()); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim()); + return (flowControl: false, value: fileInfo.Length); } catch (Exception ex) { - _logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); return (flowControl: true, value: fileInfo.Length); } } @@ -307,7 +311,8 @@ public sealed partial class FileCompactor : IDisposable } catch (Exception ex) { - _logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName); } return (flowControl: true, value: default); @@ -323,7 +328,8 @@ public sealed partial class FileCompactor : IDisposable var fi = new FileInfo(filePath); if (!fi.Exists) { - _logger.LogTrace("[W{worker}] Skip compaction: missing {file}", workerId, filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Skip compaction: missing {file}", workerId, filePath); return; } @@ -338,7 +344,8 @@ public sealed partial class FileCompactor : IDisposable if (oldSize < minSizeBytes) { - _logger.LogTrace("[W{worker}] Skip compaction: {file} ({size} B) < threshold ({th} B)", workerId, filePath, oldSize, minSizeBytes); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Skip compaction: {file} ({size} B) < threshold ({th} B)", workerId, filePath, oldSize, minSizeBytes); return; } @@ -358,7 +365,8 @@ public sealed partial class FileCompactor : IDisposable } else { - _logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath); } return; } @@ -379,12 +387,14 @@ public sealed partial class FileCompactor : IDisposable } else { - _logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath); } return; } - _logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath); } /// @@ -1045,10 +1055,10 @@ public sealed partial class FileCompactor : IDisposable } /// - /// Process the queue with, meant for a worker/thread + /// Process the queue, meant for a worker/thread /// /// Cancellation token for the worker whenever it needs to be stopped - private async Task ProcessQueueWorkerAsync(CancellationToken token, int workerId) + private async Task ProcessQueueWorkerAsync(int workerId, CancellationToken token) { try { @@ -1139,17 +1149,18 @@ public sealed partial class FileCompactor : IDisposable { try { - if (_isWindows) - { - using var _ = new FileStream(winePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - } - else - { - using var _ = new FileStream(linuxPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - } + var pathToOpen = _isWindows ? winePath : linuxPath; + + if (string.IsNullOrEmpty(pathToOpen) || !File.Exists(pathToOpen)) + return false; + + using var _ = new FileStream(pathToOpen, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return true; } - catch { return false; } + catch + { + return false; + } } /// diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 01a4de4..19e2bd0 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -227,7 +227,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService>(), clientState, objectTable, framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddSingleton(s => new BroadcastScannerService(s.GetRequiredService>(), objectTable, framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); // add scoped services diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScanningService.cs index 95abdae..f9bf5ee 100644 --- a/LightlessSync/Services/BroadcastScanningService.cs +++ b/LightlessSync/Services/BroadcastScanningService.cs @@ -1,14 +1,13 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using LightlessSync.API.Dto.User; -using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; namespace LightlessSync.Services; -public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable +public class BroadcastScannerService : DisposableMediatorSubscriberBase { private readonly ILogger _logger; private readonly IObjectTable _objectTable; @@ -17,20 +16,19 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos private readonly BroadcastService _broadcastService; private readonly NameplateHandler _nameplateHandler; - private readonly ConcurrentDictionary _broadcastCache = new(); + private readonly ConcurrentDictionary _broadcastCache = new(StringComparer.Ordinal); private readonly Queue _lookupQueue = new(); - private readonly HashSet _lookupQueuedCids = new(); - private readonly HashSet _syncshellCids = new(); + private readonly HashSet _lookupQueuedCids = []; + private readonly HashSet _syncshellCids = []; private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4); private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); private readonly CancellationTokenSource _cleanupCts = new(); - private Task? _cleanupTask; + private readonly Task? _cleanupTask; private readonly int _checkEveryFrames = 20; private int _frameCounter = 0; - private int _lookupsThisFrame = 0; private const int MaxLookupsPerFrame = 30; private const int MaxQueueSize = 100; @@ -40,14 +38,11 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); public BroadcastScannerService(ILogger logger, - IClientState clientState, IObjectTable objectTable, IFramework framework, BroadcastService broadcastService, LightlessMediator mediator, - NameplateHandler nameplateHandler, - DalamudUtilService dalamudUtil, - LightlessConfigService configService) : base(logger, mediator) + NameplateHandler nameplateHandler) : base(logger, mediator) { _logger = logger; _objectTable = objectTable; @@ -69,7 +64,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos public void Update() { _frameCounter++; - _lookupsThisFrame = 0; + var lookupsThisFrame = 0; if (!_broadcastService.IsBroadcasting) return; @@ -91,12 +86,12 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0) { var cidsToLookup = new List(); - while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame) + while (_lookupQueue.Count > 0 && lookupsThisFrame < MaxLookupsPerFrame) { var cid = _lookupQueue.Dequeue(); _lookupQueuedCids.Remove(cid); cidsToLookup.Add(cid); - _lookupsThisFrame++; + lookupsThisFrame++; } if (cidsToLookup.Count > 0 && !_batchRunning) @@ -156,7 +151,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos var newSet = _broadcastCache .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) .Select(e => e.Key) - .ToHashSet(); + .ToHashSet(StringComparer.Ordinal); if (!_syncshellCids.SetEquals(newSet)) { @@ -172,7 +167,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos { var now = DateTime.UtcNow; - return _broadcastCache + return [.. _broadcastCache .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) .Select(e => new BroadcastStatusInfoDto { @@ -180,8 +175,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos IsBroadcasting = true, TTL = e.Value.ExpiryTime - now, GID = e.Value.GID - }) - .ToList(); + })]; } private async Task ExpiredBroadcastCleanupLoop() @@ -192,7 +186,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos { while (!token.IsCancellationRequested) { - await Task.Delay(TimeSpan.FromSeconds(10), token); + await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false); var now = DateTime.UtcNow; foreach (var (cid, entry) in _broadcastCache.ToArray()) @@ -202,7 +196,10 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos } } } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + // No action needed when cancelled + } catch (Exception ex) { _logger.LogError(ex, "Broadcast cleanup loop crashed"); @@ -235,8 +232,14 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos { base.Dispose(disposing); _framework.Update -= OnFrameworkUpdate; + if (_cleanupTask != null) + { + _cleanupTask?.Wait(100, _cleanupCts.Token); + } + _cleanupCts.Cancel(); - _cleanupTask?.Wait(100); + _cleanupCts.Dispose(); + _nameplateHandler.Uninit(); } } diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index ef64f7f..0588797 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -41,8 +41,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase private Vector4 textColor; private Vector4 glowColor; - private record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); - private VanityState _savedVanity; + private sealed record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); + private VanityState? _savedVanity; public EditProfileUi(ILogger logger, LightlessMediator mediator, ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, @@ -189,25 +189,28 @@ public class EditProfileUi : WindowMediatorSubscriberBase if (!success) return; _ = Task.Run(async () => { - var fileContent = File.ReadAllBytes(file); - using MemoryStream ms = new(fileContent); - var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + var fileContent = await File.ReadAllBytesAsync(file).ConfigureAwait(false); + MemoryStream ms = new(fileContent); + await using (ms.ConfigureAwait(false)) { - _showFileDialogError = true; - return; - } - using var image = Image.Load(fileContent); + var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); + if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) + { + _showFileDialogError = true; + return; + } + using var image = Image.Load(fileContent); - if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024)) - { - _showFileDialogError = true; - return; - } + if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024)) + { + _showFileDialogError = true; + return; + } - _showFileDialogError = false; - await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), BannerPictureBase64: null, Description: null, Tags: null)) - .ConfigureAwait(false); + _showFileDialogError = false; + await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), BannerPictureBase64: null, Description: null, Tags: null)) + .ConfigureAwait(false); + } }); }); } diff --git a/LightlessSync/Utils/VariousExtensions.cs b/LightlessSync/Utils/VariousExtensions.cs index 9215893..41e3cad 100644 --- a/LightlessSync/Utils/VariousExtensions.cs +++ b/LightlessSync/Utils/VariousExtensions.cs @@ -114,9 +114,9 @@ public static class VariousExtensions .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); - var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) + var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.Ordinal) && !g.EndsWith("tex", StringComparison.Ordinal) && !g.EndsWith("mtrl", StringComparison.Ordinal))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); - var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) + var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.Ordinal) && !g.EndsWith("tex", StringComparison.Ordinal) && !g.EndsWith("mtrl", StringComparison.Ordinal))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase, -- 2.49.1 From b56813c1de2f6cbcb3e28d5f00e7c2ce97251e25 Mon Sep 17 00:00:00 2001 From: cake Date: Sat, 15 Nov 2025 04:46:07 +0100 Subject: [PATCH 05/21] Fixed many warnings, moved some classes to their own files. --- LightlessSync/Plugin.cs | 2 +- ...gService.cs => BroadcastScannerService.cs} | 0 LightlessSync/Services/BroadcastService.cs | 5 +- .../CharaData/CharacterAnalysisSummary.cs | 19 + .../Models/CharacterAnalysisObjectSummary.cs | 8 + LightlessSync/Services/CharacterAnalyzer.cs | 29 +- LightlessSync/Services/ContextMenuService.cs | 6 +- LightlessSync/Services/DalamudUtilService.cs | 12 +- .../Services/LightlessProfileManager.cs | 1 + LightlessSync/Services/NameplateHandler.cs | 1063 ++++++++--------- LightlessSync/Services/NameplateService.cs | 1 - LightlessSync/Services/NotificationService.cs | 45 +- .../PairProcessingLimiterSnapshot.cs | 9 + .../Services/PairProcessingLimiter.cs | 21 +- .../LightlessGroupProfileData.cs | 2 +- .../LightlessUserProfileData.cs | 2 +- LightlessSync/UI/DownloadUi.cs | 1 + LightlessSync/UI/EditProfileUi.cs | 2 - LightlessSync/UI/IntroUI.cs | 6 +- LightlessSync/UI/SyncshellAdminUI.cs | 2 +- 20 files changed, 622 insertions(+), 614 deletions(-) rename LightlessSync/Services/{BroadcastScanningService.cs => BroadcastScannerService.cs} (100%) create mode 100644 LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs create mode 100644 LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs create mode 100644 LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs rename LightlessSync/Services/{ => Profiles}/LightlessGroupProfileData.cs (84%) rename LightlessSync/Services/{ => Profiles}/LightlessUserProfileData.cs (90%) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 19e2bd0..50b50cb 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -279,7 +279,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService())); collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), namePlateGui, clientState, s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), + collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), s.GetRequiredService(), clientState, s.GetRequiredService())); collection.AddHostedService(p => p.GetRequiredService()); diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScannerService.cs similarity index 100% rename from LightlessSync/Services/BroadcastScanningService.cs rename to LightlessSync/Services/BroadcastScannerService.cs diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index cca9af6..dfcd975 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -11,7 +11,6 @@ using LightlessSync.WebAPI; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Threading; namespace LightlessSync.Services; public class BroadcastService : IHostedService, IMediatorSubscriber @@ -58,7 +57,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber { if (!_apiController.IsConnected) { - _logger.LogDebug(context + " skipped, not connected"); + _logger.LogDebug("{context} skipped, not connected", context); return; } await action().ConfigureAwait(false); @@ -372,7 +371,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber public async Task> AreUsersBroadcastingAsync(List hashedCids) { - Dictionary result = new(); + Dictionary result = new(StringComparer.Ordinal); await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () => { diff --git a/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs b/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs new file mode 100644 index 0000000..0eaf312 --- /dev/null +++ b/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs @@ -0,0 +1,19 @@ +using LightlessSync.API.Data.Enum; +using LightlessSync.Services.CharaData.Models; +using System.Collections.Immutable; +namespace LightlessSync.Services.CharaData; + +public sealed class CharacterAnalysisSummary +{ + public static CharacterAnalysisSummary Empty { get; } = + new(ImmutableDictionary.Empty); + + internal CharacterAnalysisSummary(IImmutableDictionary objects) + { + Objects = objects; + } + + public IImmutableDictionary Objects { get; } + + public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries); +} \ No newline at end of file diff --git a/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs b/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs new file mode 100644 index 0000000..aa42394 --- /dev/null +++ b/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; +namespace LightlessSync.Services.CharaData.Models; + +[StructLayout(LayoutKind.Auto)] +public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes) +{ + public bool HasEntries => EntryCount > 0; +} diff --git a/LightlessSync/Services/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index 27235f6..8b87c99 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -1,16 +1,14 @@ using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.FileCache; +using LightlessSync.Services.CharaData; +using LightlessSync.Services.CharaData.Models; using LightlessSync.Services.Mediator; using LightlessSync.UI; using LightlessSync.Utils; using Lumina.Data.Files; using Microsoft.Extensions.Logging; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace LightlessSync.Services; public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable @@ -99,6 +97,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public void Dispose() { _analysisCts.CancelDispose(); + _baseAnalysisCts.Dispose(); } private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) @@ -135,7 +134,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, [.. fileEntry.GamePaths], - fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(), + [.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)], entry.Size > 0 ? entry.Size.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0, tris); @@ -269,23 +268,3 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable }); } } - -public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes) -{ - public bool HasEntries => EntryCount > 0; -} - -public sealed class CharacterAnalysisSummary -{ - public static CharacterAnalysisSummary Empty { get; } = - new(ImmutableDictionary.Empty); - - internal CharacterAnalysisSummary(IImmutableDictionary objects) - { - Objects = objects; - } - - public IImmutableDictionary Objects { get; } - - public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries); -} \ No newline at end of file diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 464fee1..4d50074 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -95,7 +95,7 @@ internal class ContextMenuService : IHostedService //Check if it is a real target. IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); - if (targetData == null || targetData.Address == nint.Zero) + if (targetData == null || targetData.Address == nint.Zero || _clientState.LocalPlayer == null) return; //Check if user is directly paired or is own. @@ -120,7 +120,7 @@ internal class ContextMenuService : IHostedService PrefixChar = 'L', UseDefaultPrefix = false, PrefixColor = 708, - OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false) + OnClicked = _ => HandleSelection(args).ConfigureAwait(false).GetAwaiter().GetResult() }); } @@ -200,8 +200,6 @@ internal class ContextMenuService : IHostedService private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF; - public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId)); - public static bool IsWorldValid(World world) { var name = world.Name.ToString(); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index e5fd735..f3bf08d 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -140,14 +140,14 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsWine { get; init; } - public unsafe GameObject* GposeTarget + public static unsafe GameObject* GposeTarget { get => TargetSystem.Instance()->GPoseTarget; set => TargetSystem.Instance()->GPoseTarget = value; } - private unsafe bool HasGposeTarget => GposeTarget != null; - private unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex; + private static unsafe bool HasGposeTarget => GposeTarget != null; + private static unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex; public async Task GetGposeTargetGameObjectAsync() { @@ -513,15 +513,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); curWaitTime += tick; - await Task.Delay(tick).ConfigureAwait(true); + await Task.Delay(tick, ct.Value).ConfigureAwait(true); } logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime); } - catch (NullReferenceException ex) - { - logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); - } catch (AccessViolationException ex) { logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); diff --git a/LightlessSync/Services/LightlessProfileManager.cs b/LightlessSync/Services/LightlessProfileManager.cs index 00b610b..a324a9a 100644 --- a/LightlessSync/Services/LightlessProfileManager.cs +++ b/LightlessSync/Services/LightlessProfileManager.cs @@ -2,6 +2,7 @@ using LightlessSync.API.Data.Comparer; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; +using LightlessSync.Services.Profiles; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using Serilog.Core; diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs index 11af974..5b2246f 100644 --- a/LightlessSync/Services/NameplateHandler.cs +++ b/LightlessSync/Services/NameplateHandler.cs @@ -16,7 +16,6 @@ using LightlessSync.UtilsEnum.Enum; // Created using https://github.com/PunishedPineapple/Distance as a reference, thank you! using Microsoft.Extensions.Logging; -using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; @@ -28,7 +27,6 @@ public unsafe class NameplateHandler : IMediatorSubscriber private readonly IAddonLifecycle _addonLifecycle; private readonly IGameGui _gameGui; private readonly IClientState _clientState; - private readonly DalamudUtilService _dalamudUtil; private readonly LightlessConfigService _configService; private readonly PairManager _pairManager; private readonly LightlessMediator _mediator; @@ -46,17 +44,15 @@ public unsafe class NameplateHandler : IMediatorSubscriber internal const uint mNameplateNodeIDBase = 0x7D99D500; private const string DefaultLabelText = "LightFinder"; private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn; - private const int _containerOffsetX = 50; private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon); private ImmutableHashSet _activeBroadcastingCids = []; - public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) + public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) { _logger = logger; _addonLifecycle = addonLifecycle; _gameGui = gameGui; - _dalamudUtil = dalamudUtil; _configService = configService; _mediator = mediator; _clientState = clientState; @@ -118,577 +114,580 @@ public unsafe class NameplateHandler : IMediatorSubscriber { if (args.Addon.Address == nint.Zero) { - _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); return; } var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; - if (_mpNameplateAddon != pNameplateAddon) - { - for (int i = 0; i < _mTextNodes.Length; ++i) _mTextNodes[i] = null; - System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); - System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); - System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); - _mpNameplateAddon = pNameplateAddon; - if (_mpNameplateAddon != null) CreateNameplateNodes(); - } - - UpdateNameplateNodes(); - } - - private void CreateNameplateNodes() + if (_mpNameplateAddon != pNameplateAddon) { - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) - { - var nameplateObject = GetNameplateObject(i); - if (nameplateObject == null) - continue; - - var rootNode = nameplateObject.Value.RootComponentNode; - if (rootNode == null || rootNode->Component == null) - continue; - - var pNameplateResNode = nameplateObject.Value.NameContainer; - if (pNameplateResNode == null) - continue; - if (pNameplateResNode->ChildNode == null) - continue; - - var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare); - - if (pNewNode != null) - { - var pLastChild = pNameplateResNode->ChildNode; - while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode; - pNewNode->AtkResNode.NextSiblingNode = pLastChild; - pNewNode->AtkResNode.ParentNode = pNameplateResNode; - pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode; - rootNode->Component->UldManager.UpdateDrawNodeList(); - pNewNode->AtkResNode.SetUseDepthBasedPriority(true); - _mTextNodes[i] = pNewNode; - } - } - } - - private void DestroyNameplateNodes() - { - var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); - if (currentHandle.Address == nint.Zero) - { - _logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available."); - return; - } - - var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null) - return; - - if (_mpNameplateAddon != pCurrentNameplateAddon) - { - _logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon); - return; - } - - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) - { - var pTextNode = _mTextNodes[i]; - var pNameplateNode = GetNameplateComponentNode(i); - if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null)) - { - _logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); - continue; - } - - if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null) - { - try - { - if (pTextNode->AtkResNode.PrevSiblingNode != null) - pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode; - if (pTextNode->AtkResNode.NextSiblingNode != null) - pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; - pNameplateNode->Component->UldManager.UpdateDrawNodeList(); - pTextNode->AtkResNode.Destroy(true); - _mTextNodes[i] = null; - } - catch (Exception e) - { - _logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}"); - } - } - } - + for (int i = 0; i < _mTextNodes.Length; ++i) _mTextNodes[i] = null; System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); + _mpNameplateAddon = pNameplateAddon; + if (_mpNameplateAddon != null) CreateNameplateNodes(); } - private void HideAllNameplateNodes() + UpdateNameplateNodes(); +} + +private void CreateNameplateNodes() +{ + for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) { - for (int i = 0; i < _mTextNodes.Length; ++i) + var nameplateObject = GetNameplateObject(i); + if (nameplateObject == null) + continue; + + var rootNode = nameplateObject.Value.RootComponentNode; + if (rootNode == null || rootNode->Component == null) + continue; + + var pNameplateResNode = nameplateObject.Value.NameContainer; + if (pNameplateResNode == null) + continue; + if (pNameplateResNode->ChildNode == null) + continue; + + var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare); + + if (pNewNode != null) { - HideNameplateTextNode(i); + var pLastChild = pNameplateResNode->ChildNode; + while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode; + pNewNode->AtkResNode.NextSiblingNode = pLastChild; + pNewNode->AtkResNode.ParentNode = pNameplateResNode; + pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode; + rootNode->Component->UldManager.UpdateDrawNodeList(); + pNewNode->AtkResNode.SetUseDepthBasedPriority(true); + _mTextNodes[i] = pNewNode; + } + } +} + +private void DestroyNameplateNodes() +{ + var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); + if (currentHandle.Address == nint.Zero) + { + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available."); + return; + } + + var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address; + if (_mpNameplateAddon == null) + return; + + if (_mpNameplateAddon != pCurrentNameplateAddon) + { + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon); + return; + } + + for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) + { + var pTextNode = _mTextNodes[i]; + var pNameplateNode = GetNameplateComponentNode(i); + if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null)) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); + continue; + } + + if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null) + { + try + { + if (pTextNode->AtkResNode.PrevSiblingNode != null) + pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode; + if (pTextNode->AtkResNode.NextSiblingNode != null) + pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; + pNameplateNode->Component->UldManager.UpdateDrawNodeList(); + pTextNode->AtkResNode.Destroy(free: true); + _mTextNodes[i] = null; + } + catch (Exception e) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError("Unknown error while removing text node 0x{textNode} for nameplate {i} on component node 0x{nameplateNode}:\n{e}", (IntPtr)pTextNode, i, (IntPtr)pNameplateNode, e); + } } } - private void UpdateNameplateNodes() + System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); + System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); + System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); + System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); +} + +private void HideAllNameplateNodes() +{ + for (int i = 0; i < _mTextNodes.Length; ++i) { - var currentHandle = _gameGui.GetAddonByName("NamePlate"); - if (currentHandle.Address == nint.Zero) - { + HideNameplateTextNode(i); + } +} + +private void UpdateNameplateNodes() +{ + var currentHandle = _gameGui.GetAddonByName("NamePlate"); + if (currentHandle.Address == nint.Zero) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); - return; - } + return; + } - var currentAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) - { - if (_mpNameplateAddon != null) - _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); - return; - } + var currentAddon = (AddonNamePlate*)currentHandle.Address; + if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) + { + if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); + return; + } - var framework = Framework.Instance(); - if (framework == null) - { + var framework = Framework.Instance(); + if (framework == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); - return; - } + return; + } - var uiModule = framework->GetUIModule(); - if (uiModule == null) - { + var uiModule = framework->GetUIModule(); + if (uiModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("UI module unavailable during nameplate update, skipping."); - return; - } + return; + } - var ui3DModule = uiModule->GetUI3DModule(); - if (ui3DModule == null) - { + var ui3DModule = uiModule->GetUI3DModule(); + if (ui3DModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); - return; + return; + } + + var vec = ui3DModule->NamePlateObjectInfoPointers; + if (vec.IsEmpty) + return; + + var visibleUserIdsSnapshot = VisibleUserIds; + + var safeCount = System.Math.Min( + ui3DModule->NamePlateObjectInfoCount, + vec.Length + ); + + for (int i = 0; i < safeCount; ++i) + { + var config = _configService.Current; + + var objectInfoPtr = vec[i]; + if (objectInfoPtr == null) + continue; + + var objectInfo = objectInfoPtr.Value; + if (objectInfo == null || objectInfo->GameObject == null) + continue; + + var nameplateIndex = objectInfo->NamePlateIndex; + if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) + continue; + + var pNode = _mTextNodes[nameplateIndex]; + if (pNode == null) + continue; + + var gameObject = objectInfo->GameObject; + if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) + { + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - var vec = ui3DModule->NamePlateObjectInfoPointers; - if (vec.IsEmpty) - return; - - var visibleUserIdsSnapshot = VisibleUserIds; - - var safeCount = System.Math.Min( - ui3DModule->NamePlateObjectInfoCount, - vec.Length - ); - - for (int i = 0; i < safeCount; ++i) + // CID gating + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); + if (cid == null || !_activeBroadcastingCids.Contains(cid)) { - var config = _configService.Current; - - var objectInfoPtr = vec[i]; - if (objectInfoPtr == null) - continue; - - var objectInfo = objectInfoPtr.Value; - if (objectInfo == null || objectInfo->GameObject == null) - continue; - - var nameplateIndex = objectInfo->NamePlateIndex; - if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) - continue; - - var pNode = _mTextNodes[nameplateIndex]; - if (pNode == null) - continue; - - var gameObject = objectInfo->GameObject; - if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - // CID gating - var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); - if (cid == null || !_activeBroadcastingCids.Contains(cid)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var local = _clientState.LocalPlayer; - if (!config.LightfinderLabelShowOwn && local != null && - objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var hidePaired = !config.LightfinderLabelShowPaired; - - var goId = (ulong)gameObject->GetGameObjectId(); - if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; - var root = nameplateObject.RootComponentNode; - var nameContainer = nameplateObject.NameContainer; - var nameText = nameplateObject.NameText; - var marker = nameplateObject.MarkerIcon; - - if (root == null || root->Component == null || nameContainer == null || nameText == null) - { - _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex); - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - root->Component->UldManager.UpdateDrawNodeList(); - - bool isVisible = - ((marker != null) && marker->AtkResNode.IsVisible()) || - (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || - config.LightfinderLabelShowHidden; - - pNode->AtkResNode.ToggleVisibility(isVisible); - if (!isVisible) - continue; - - var labelColor = UIColors.Get("Lightfinder"); - var edgeColor = UIColors.Get("LightfinderEdge"); - - var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); - var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f; - var effectiveScale = baseScale * scaleMultiplier; - var labelContent = config.LightfinderLabelUseIcon - ? NormalizeIconGlyph(config.LightfinderLabelIconGlyph) - : DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - pNode->AtkResNode.SetScale(effectiveScale, effectiveScale); - var nodeWidth = (int)pNode->AtkResNode.GetWidth(); - if (nodeWidth <= 0) - nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); - var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f; - var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); - pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255); - AlignmentType alignment; - - var textScaleY = nameText->AtkResNode.ScaleY; - if (textScaleY <= 0f) - textScaleY = 1f; - - var blockHeight = System.Math.Abs((int)nameplateObject.TextH); - if (blockHeight > 0) - { - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - else - { - blockHeight = _cachedNameplateTextHeights[nameplateIndex]; - } - - if (blockHeight <= 0) - { - blockHeight = GetScaledTextHeight(nameText); - if (blockHeight <= 0) - blockHeight = nodeHeight; - - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - - var containerHeight = (int)nameContainer->Height; - if (containerHeight > 0) - { - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - else - { - containerHeight = _cachedNameplateContainerHeights[nameplateIndex]; - } - - if (containerHeight <= 0) - { - containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY); - if (containerHeight <= blockHeight) - containerHeight = blockHeight + 1; - - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - - var blockTop = containerHeight - blockHeight; - if (blockTop < 0) - blockTop = 0; - var verticalPadding = (int)System.Math.Round(4 * effectiveScale); - - var positionY = blockTop - verticalPadding - nodeHeight; - - var textWidth = System.Math.Abs((int)nameplateObject.TextW); - if (textWidth <= 0) - { - textWidth = GetScaledTextWidth(nameText); - if (textWidth <= 0) - textWidth = nodeWidth; - } - - if (textWidth > 0) - { - _cachedNameplateTextWidths[nameplateIndex] = textWidth; - } - - var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); - var hasValidOffset = true; - - if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0) - { - _cachedNameplateTextOffsets[nameplateIndex] = textOffset; - } - else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue) - { - textOffset = _cachedNameplateTextOffsets[nameplateIndex]; - } - else - { - hasValidOffset = false; - } - int positionX; - - - if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) - labelContent = DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - - pNode->SetText(labelContent); - - if (!config.LightfinderLabelUseIcon) - { - pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = (int)pNode->AtkResNode.GetWidth(); - if (nodeWidth <= 0) - nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - pNode->AtkResNode.Width = (ushort)nodeWidth; - } - else - { - pNode->TextFlags |= TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = pNode->AtkResNode.GetWidth(); - } - - - if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset) - { - var nameplateWidth = (int)nameContainer->Width; - - int leftPos = nameplateWidth / 8; - int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8); - int centrePos = (nameplateWidth - nodeWidth) / 2; - int staticMargin = 24; - int calcMargin = (int)(nameplateWidth * 0.08f); - - switch (config.LabelAlignment) - { - case LabelAlignment.Left: - positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos; - alignment = AlignmentType.BottomLeft; - break; - case LabelAlignment.Right: - positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin; - alignment = AlignmentType.BottomRight; - break; - default: - positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin; - alignment = AlignmentType.Bottom; - break; - } - } - else - { - positionX = 58 + config.LightfinderLabelOffsetX; - alignment = AlignmentType.Bottom; - } - - positionY += config.LightfinderLabelOffsetY; - - alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); - pNode->AtkResNode.SetUseDepthBasedPriority(enable: true); - - pNode->AtkResNode.Color.A = 255; - - pNode->TextColor.R = (byte)(labelColor.X * 255); - pNode->TextColor.G = (byte)(labelColor.Y * 255); - pNode->TextColor.B = (byte)(labelColor.Z * 255); - pNode->TextColor.A = (byte)(labelColor.W * 255); - - pNode->EdgeColor.R = (byte)(edgeColor.X * 255); - pNode->EdgeColor.G = (byte)(edgeColor.Y * 255); - pNode->EdgeColor.B = (byte)(edgeColor.Z * 255); - pNode->EdgeColor.A = (byte)(edgeColor.W * 255); - - - if(!config.LightfinderLabelUseIcon) - { - pNode->AlignmentType = AlignmentType.Bottom; - } - else - { - pNode->AlignmentType = alignment; - } - pNode->AtkResNode.SetPositionShort( - (short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue), - (short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue) - ); - var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier); - pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue); - pNode->CharSpacing = 1; - pNode->TextFlags = config.LightfinderLabelUseIcon - ? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize - : TextFlags.Edge | TextFlags.Glare; + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - } - private static unsafe int GetScaledTextHeight(AtkTextNode* node) - { - if (node == null) - return 0; - - var resNode = &node->AtkResNode; - var rawHeight = (int)resNode->GetHeight(); - if (rawHeight <= 0 && node->LineSpacing > 0) - rawHeight = node->LineSpacing; - if (rawHeight <= 0) - rawHeight = AtkNodeHelpers.DefaultTextNodeHeight; - - var scale = resNode->ScaleY; - if (scale <= 0f) - scale = 1f; - - var computed = (int)System.Math.Round(rawHeight * scale); - return System.Math.Max(1, computed); - } - - private static unsafe int GetScaledTextWidth(AtkTextNode* node) - { - if (node == null) - return 0; - - var resNode = &node->AtkResNode; - var rawWidth = (int)resNode->GetWidth(); - if (rawWidth <= 0) - rawWidth = AtkNodeHelpers.DefaultTextNodeWidth; - - var scale = resNode->ScaleX; - if (scale <= 0f) - scale = 1f; - - var computed = (int)System.Math.Round(rawWidth * scale); - return System.Math.Max(1, computed); - } - - internal static string NormalizeIconGlyph(string? rawInput) - { - if (string.IsNullOrWhiteSpace(rawInput)) - return DefaultIconGlyph; - - var trimmed = rawInput.Trim(); - - if (Enum.TryParse(trimmed, true, out var iconEnum)) - return SeIconCharExtensions.ToIconString(iconEnum); - - var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? trimmed[2..] - : trimmed; - - if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue)) - return char.ConvertFromUtf32(hexValue); - - var enumerator = trimmed.EnumerateRunes(); - if (enumerator.MoveNext()) - return enumerator.Current.ToString(); - - return DefaultIconGlyph; - } - - 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 void HideNameplateTextNode(int i) - { - var pNode = _mTextNodes[i]; - if (pNode != null) + var local = _clientState.LocalPlayer; + if (!config.LightfinderLabelShowOwn && local != null && + objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) { - pNode->AtkResNode.ToggleVisibility(false); + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - } - private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) - { - if (i < AddonNamePlate.NumNamePlateObjects && - _mpNameplateAddon != null && - _mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) + var hidePaired = !config.LightfinderLabelShowPaired; + + var goId = (ulong)gameObject->GetGameObjectId(); + if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) { - return _mpNameplateAddon->NamePlateObjectArray[i]; + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; + } + + var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; + var root = nameplateObject.RootComponentNode; + var nameContainer = nameplateObject.NameContainer; + var nameText = nameplateObject.NameText; + var marker = nameplateObject.MarkerIcon; + + if (root == null || root->Component == null || nameContainer == null || nameText == null) + { + _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex); + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; + } + + root->Component->UldManager.UpdateDrawNodeList(); + + bool isVisible = + ((marker != null) && marker->AtkResNode.IsVisible()) || + (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || + config.LightfinderLabelShowHidden; + + pNode->AtkResNode.ToggleVisibility(isVisible); + if (!isVisible) + continue; + + var labelColor = UIColors.Get("Lightfinder"); + var edgeColor = UIColors.Get("LightfinderEdge"); + + var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); + var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f; + var effectiveScale = baseScale * scaleMultiplier; + var labelContent = config.LightfinderLabelUseIcon + ? NormalizeIconGlyph(config.LightfinderLabelIconGlyph) + : DefaultLabelText; + + pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; + pNode->AtkResNode.SetScale(effectiveScale, effectiveScale); + var nodeWidth = (int)pNode->AtkResNode.GetWidth(); + if (nodeWidth <= 0) + nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); + var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); + var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f; + var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); + pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255); + AlignmentType alignment; + + var textScaleY = nameText->AtkResNode.ScaleY; + if (textScaleY <= 0f) + textScaleY = 1f; + + var blockHeight = System.Math.Abs((int)nameplateObject.TextH); + if (blockHeight > 0) + { + _cachedNameplateTextHeights[nameplateIndex] = blockHeight; } else { - return null; + blockHeight = _cachedNameplateTextHeights[nameplateIndex]; } - } - private AtkComponentNode* GetNameplateComponentNode(int i) - { - var nameplateObject = GetNameplateObject(i); - return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; - } - - private HashSet VisibleUserIds => [.. _pairManager.GetOnlineUserPairs() - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId)]; - - - public void FlagRefresh() - { - _needsLabelRefresh = true; - } - - public void OnTick(PriorityFrameworkUpdateMessage _) - { - if (_needsLabelRefresh) + if (blockHeight <= 0) { - UpdateNameplateNodes(); - _needsLabelRefresh = false; + blockHeight = GetScaledTextHeight(nameText); + if (blockHeight <= 0) + blockHeight = nodeHeight; + + _cachedNameplateTextHeights[nameplateIndex] = blockHeight; } - } - public void UpdateBroadcastingCids(IEnumerable cids) - { - var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); - if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) - return; + var containerHeight = (int)nameContainer->Height; + if (containerHeight > 0) + { + _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; + } + else + { + containerHeight = _cachedNameplateContainerHeights[nameplateIndex]; + } - _activeBroadcastingCids = newSet; - _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); - FlagRefresh(); - } + if (containerHeight <= 0) + { + containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY); + if (containerHeight <= blockHeight) + containerHeight = blockHeight + 1; - public void ClearNameplateCaches() - { - System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); - System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); - System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); + _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; + } + + var blockTop = containerHeight - blockHeight; + if (blockTop < 0) + blockTop = 0; + var verticalPadding = (int)System.Math.Round(4 * effectiveScale); + + var positionY = blockTop - verticalPadding - nodeHeight; + + var textWidth = System.Math.Abs((int)nameplateObject.TextW); + if (textWidth <= 0) + { + textWidth = GetScaledTextWidth(nameText); + if (textWidth <= 0) + textWidth = nodeWidth; + } + + if (textWidth > 0) + { + _cachedNameplateTextWidths[nameplateIndex] = textWidth; + } + + var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); + var hasValidOffset = true; + + if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0) + { + _cachedNameplateTextOffsets[nameplateIndex] = textOffset; + } + else + { + hasValidOffset = false; + } + int positionX; + + + if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) + labelContent = DefaultLabelText; + + pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; + + pNode->SetText(labelContent); + + if (!config.LightfinderLabelUseIcon) + { + pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize; + pNode->AtkResNode.Width = 0; + nodeWidth = (int)pNode->AtkResNode.GetWidth(); + if (nodeWidth <= 0) + nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); + pNode->AtkResNode.Width = (ushort)nodeWidth; + } + else + { + pNode->TextFlags |= TextFlags.AutoAdjustNodeSize; + pNode->AtkResNode.Width = 0; + nodeWidth = pNode->AtkResNode.GetWidth(); + } + + + if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset) + { + var nameplateWidth = (int)nameContainer->Width; + + int leftPos = nameplateWidth / 8; + int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8); + int centrePos = (nameplateWidth - nodeWidth) / 2; + int staticMargin = 24; + int calcMargin = (int)(nameplateWidth * 0.08f); + + switch (config.LabelAlignment) + { + case LabelAlignment.Left: + positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos; + alignment = AlignmentType.BottomLeft; + break; + case LabelAlignment.Right: + positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin; + alignment = AlignmentType.BottomRight; + break; + default: + positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin; + alignment = AlignmentType.Bottom; + break; + } + } + else + { + positionX = 58 + config.LightfinderLabelOffsetX; + alignment = AlignmentType.Bottom; + } + + positionY += config.LightfinderLabelOffsetY; + + alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); + pNode->AtkResNode.SetUseDepthBasedPriority(enable: true); + + pNode->AtkResNode.Color.A = 255; + + pNode->TextColor.R = (byte)(labelColor.X * 255); + pNode->TextColor.G = (byte)(labelColor.Y * 255); + pNode->TextColor.B = (byte)(labelColor.Z * 255); + pNode->TextColor.A = (byte)(labelColor.W * 255); + + pNode->EdgeColor.R = (byte)(edgeColor.X * 255); + pNode->EdgeColor.G = (byte)(edgeColor.Y * 255); + pNode->EdgeColor.B = (byte)(edgeColor.Z * 255); + pNode->EdgeColor.A = (byte)(edgeColor.W * 255); + + + if(!config.LightfinderLabelUseIcon) + { + pNode->AlignmentType = AlignmentType.Bottom; + } + else + { + pNode->AlignmentType = alignment; + } + pNode->AtkResNode.SetPositionShort( + (short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue), + (short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue) + ); + var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier); + pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue); + pNode->CharSpacing = 1; + pNode->TextFlags = config.LightfinderLabelUseIcon + ? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize + : TextFlags.Edge | TextFlags.Glare; } } + +private static unsafe int GetScaledTextHeight(AtkTextNode* node) +{ + if (node == null) + return 0; + + var resNode = &node->AtkResNode; + var rawHeight = (int)resNode->GetHeight(); + if (rawHeight <= 0 && node->LineSpacing > 0) + rawHeight = node->LineSpacing; + if (rawHeight <= 0) + rawHeight = AtkNodeHelpers.DefaultTextNodeHeight; + + var scale = resNode->ScaleY; + if (scale <= 0f) + scale = 1f; + + var computed = (int)System.Math.Round(rawHeight * scale); + return System.Math.Max(1, computed); +} + +private static unsafe int GetScaledTextWidth(AtkTextNode* node) +{ + if (node == null) + return 0; + + var resNode = &node->AtkResNode; + var rawWidth = (int)resNode->GetWidth(); + if (rawWidth <= 0) + rawWidth = AtkNodeHelpers.DefaultTextNodeWidth; + + var scale = resNode->ScaleX; + if (scale <= 0f) + scale = 1f; + + var computed = (int)System.Math.Round(rawWidth * scale); + return System.Math.Max(1, computed); +} + +internal static string NormalizeIconGlyph(string? rawInput) +{ + if (string.IsNullOrWhiteSpace(rawInput)) + return DefaultIconGlyph; + + var trimmed = rawInput.Trim(); + + if (Enum.TryParse(trimmed, true, out var iconEnum)) + return SeIconCharExtensions.ToIconString(iconEnum); + + var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? trimmed[2..] + : trimmed; + + if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue)) + return char.ConvertFromUtf32(hexValue); + + var enumerator = trimmed.EnumerateRunes(); + if (enumerator.MoveNext()) + return enumerator.Current.ToString(); + + return DefaultIconGlyph; +} + +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 void HideNameplateTextNode(int i) +{ + var pNode = _mTextNodes[i]; + if (pNode != null) + { + pNode->AtkResNode.ToggleVisibility(false); + } +} + +private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) +{ + if (i < AddonNamePlate.NumNamePlateObjects && + _mpNameplateAddon != null && + _mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) + { + return _mpNameplateAddon->NamePlateObjectArray[i]; + } + return null; +} + +private AtkComponentNode* GetNameplateComponentNode(int i) +{ + var nameplateObject = GetNameplateObject(i); + return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; +} + +private HashSet VisibleUserIds => [.. _pairManager.GetOnlineUserPairs() + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId)]; + + +public void FlagRefresh() +{ + _needsLabelRefresh = true; +} + +public void OnTick(PriorityFrameworkUpdateMessage _) +{ + if (_needsLabelRefresh) + { + UpdateNameplateNodes(); + _needsLabelRefresh = false; + } +} + +public void UpdateBroadcastingCids(IEnumerable cids) +{ + var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); + if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) + return; + + _activeBroadcastingCids = newSet; + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); + FlagRefresh(); +} + +public void ClearNameplateCaches() +{ + System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); + System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); + System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); + System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); +} +} diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 8ccc362..0961663 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -47,7 +47,6 @@ public class NameplateService : DisposableMediatorSubscriberBase .Select(u => (ulong)u.PlayerCharacterId) .ToHashSet(); - var now = DateTime.UtcNow; var colors = _configService.Current.NameplateColors; foreach (var handler in handlers) diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 8709710..cbe0ee4 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -23,7 +23,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private readonly INotificationManager _notificationManager; private readonly IChatGui _chatGui; private readonly PairRequestService _pairRequestService; - private readonly HashSet _shownPairRequestNotifications = new(); + private readonly HashSet _shownPairRequestNotifications = []; public NotificationService( ILogger logger, @@ -59,7 +59,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ { var notification = CreateNotification(title, message, type, duration, actions, soundEffectId); - if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) + if (_configService.Current.AutoDismissOnAction && notification.Actions.Count != 0) { WrapActionsWithAutoDismiss(notification); } @@ -104,7 +104,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ } } - private void DismissNotification(LightlessNotification notification) + private static void DismissNotification(LightlessNotification notification) { notification.IsDismissed = true; notification.IsAnimatingOut = true; @@ -208,10 +208,12 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } - private string FormatDownloadCompleteMessage(string fileName, int fileCount) => - fileCount > 1 + private static string FormatDownloadCompleteMessage(string fileName, int fileCount) + { + return fileCount > 1 ? $"Downloaded {fileCount} files successfully." : $"Downloaded {fileName} successfully."; + } private List CreateDownloadCompleteActions(Action? onOpenFolder) { @@ -257,8 +259,10 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } - private string FormatErrorMessage(string message, Exception? exception) => - exception != null ? $"{message}\n\nError: {exception.Message}" : message; + private static string FormatErrorMessage(string message, Exception? exception) + { + return exception != null ? $"{message}\n\nError: {exception.Message}" : message; + } private List CreateErrorActions(Action? onRetry, Action? onViewLog) { @@ -332,8 +336,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}")); } - private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) => - download.Status switch + private static string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) + { + return download.Status switch { "downloading" => $"{download.Progress:P0}", "decompressing" => "decompressing", @@ -341,6 +346,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ "waiting" => "waiting for slot", _ => download.Status }; + } private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch { @@ -478,13 +484,16 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ }); } - private Dalamud.Interface.ImGuiNotification.NotificationType - ConvertToDalamudNotificationType(NotificationType type) => type switch + private static Dalamud.Interface.ImGuiNotification.NotificationType + ConvertToDalamudNotificationType(NotificationType type) { - NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, - NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, - _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info - }; + return type switch + { + NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, + NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, + _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info + }; + } private void ShowChat(NotificationMessage msg) { @@ -568,7 +577,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _) { var activeRequests = _pairRequestService.GetActiveRequests(); - var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(); + var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(StringComparer.Ordinal); // Dismiss notifications for requests that are no longer active (expired) var notificationsToRemove = _shownPairRequestNotifications @@ -585,7 +594,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void HandlePairDownloadStatus(PairDownloadStatusMessage msg) { - var userDownloads = msg.DownloadStatus.Where(x => x.PlayerName != "Pair Queue").ToList(); + var userDownloads = msg.DownloadStatus.Where(x => !string.Equals(x.PlayerName, "Pair Queue", StringComparison.Ordinal)).ToList(); var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f; var message = BuildPairDownloadMessage(userDownloads, msg.QueueWaiting); @@ -734,7 +743,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return actions; } - private string GetUserDisplayName(UserData userData, string playerName) + private static string GetUserDisplayName(UserData userData, string playerName) { if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal)) { diff --git a/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs b/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs new file mode 100644 index 0000000..64cc6b0 --- /dev/null +++ b/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace LightlessSync.Services.PairProcessing; + +[StructLayout(LayoutKind.Auto)] +public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting) +{ + public int Remaining => Math.Max(0, Limit - InFlight); +} diff --git a/LightlessSync/Services/PairProcessingLimiter.cs b/LightlessSync/Services/PairProcessingLimiter.cs index 239ba75..35b6d1c 100644 --- a/LightlessSync/Services/PairProcessingLimiter.cs +++ b/LightlessSync/Services/PairProcessingLimiter.cs @@ -1,15 +1,13 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase { - private const int HardLimit = 32; + private const int _hardLimit = 32; private readonly LightlessConfigService _configService; private readonly object _limitLock = new(); private readonly SemaphoreSlim _semaphore; @@ -24,8 +22,8 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase { _configService = configService; _currentLimit = CalculateLimit(); - var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : HardLimit; - _semaphore = new SemaphoreSlim(initialCount, HardLimit); + var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : _hardLimit; + _semaphore = new SemaphoreSlim(initialCount, _hardLimit); Mediator.Subscribe(this, _ => UpdateSemaphoreLimit()); } @@ -88,7 +86,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase if (!enabled) { - var releaseAmount = HardLimit - _semaphore.CurrentCount; + var releaseAmount = _hardLimit - _semaphore.CurrentCount; if (releaseAmount > 0) { TryReleaseSemaphore(releaseAmount); @@ -110,7 +108,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase var increment = desiredLimit - _currentLimit; _pendingIncrements += increment; - var available = HardLimit - _semaphore.CurrentCount; + var available = _hardLimit - _semaphore.CurrentCount; var toRelease = Math.Min(_pendingIncrements, available); if (toRelease > 0 && TryReleaseSemaphore(toRelease)) { @@ -148,7 +146,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase private int CalculateLimit() { var configured = _configService.Current.MaxConcurrentPairApplications; - return Math.Clamp(configured, 1, HardLimit); + return Math.Clamp(configured, 1, _hardLimit); } private bool TryReleaseSemaphore(int count = 1) @@ -248,8 +246,3 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase } } } - -public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting) -{ - public int Remaining => Math.Max(0, Limit - InFlight); -} diff --git a/LightlessSync/Services/LightlessGroupProfileData.cs b/LightlessSync/Services/Profiles/LightlessGroupProfileData.cs similarity index 84% rename from LightlessSync/Services/LightlessGroupProfileData.cs rename to LightlessSync/Services/Profiles/LightlessGroupProfileData.cs index 1b27b40..1a6d212 100644 --- a/LightlessSync/Services/LightlessGroupProfileData.cs +++ b/LightlessSync/Services/Profiles/LightlessGroupProfileData.cs @@ -1,4 +1,4 @@ -namespace LightlessSync.Services; +namespace LightlessSync.Services.Profiles; public record LightlessGroupProfileData(string Base64ProfilePicture, string Description, int[] Tags, bool IsNsfw, bool IsDisabled) { diff --git a/LightlessSync/Services/LightlessUserProfileData.cs b/LightlessSync/Services/Profiles/LightlessUserProfileData.cs similarity index 90% rename from LightlessSync/Services/LightlessUserProfileData.cs rename to LightlessSync/Services/Profiles/LightlessUserProfileData.cs index 3319043..ebad3fe 100644 --- a/LightlessSync/Services/LightlessUserProfileData.cs +++ b/LightlessSync/Services/Profiles/LightlessUserProfileData.cs @@ -1,4 +1,4 @@ -namespace LightlessSync.Services; +namespace LightlessSync.Services.Profiles; public record LightlessUserProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description) { diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index aa3132a..337fb41 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -5,6 +5,7 @@ using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 0588797..880a0d2 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -35,7 +35,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase private bool _wasOpen; private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); - private bool vanityInitialized; // useless for now private bool textEnabled; private bool glowEnabled; private Vector4 textColor; @@ -86,7 +85,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); - vanityInitialized = true; } protected override void DrawInternal() diff --git a/LightlessSync/UI/IntroUI.cs b/LightlessSync/UI/IntroUI.cs index 470cadb..97935c2 100644 --- a/LightlessSync/UI/IntroUI.cs +++ b/LightlessSync/UI/IntroUI.cs @@ -267,7 +267,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase { UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); } - else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey)) + else if (_secretKey.Length == 64 && !SecretRegex().IsMatch(_secretKey)) { UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed); } @@ -360,6 +360,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase _tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6]; } - [GeneratedRegex("^([A-F0-9]{2})+")] - private static partial Regex HexRegex(); + [GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex SecretRegex(); } \ No newline at end of file diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 0967290..7497e78 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -13,13 +13,13 @@ using LightlessSync.API.Dto.User; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.Profiles; using LightlessSync.UI.Handlers; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System.Globalization; -using System.Linq; using System.Numerics; namespace LightlessSync.UI; -- 2.49.1 From a869b369aae22684d9811f690231c891ba366fd5 Mon Sep 17 00:00:00 2001 From: cake Date: Sat, 15 Nov 2025 05:11:19 +0100 Subject: [PATCH 06/21] Warnings fixed --- LightlessSync/Services/DalamudUtilService.cs | 6 +++--- LightlessSync/Services/PerformanceCollectorService.cs | 6 +++--- LightlessSync/Services/PlayerPerformanceService.cs | 10 +++++----- LightlessSync/UI/CharaDataHubUi.McdOnline.cs | 6 +++--- LightlessSync/UI/CharaDataHubUi.cs | 6 ++++-- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index f3bf08d..f476076 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -140,14 +140,14 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsWine { get; init; } - public static unsafe GameObject* GposeTarget + public unsafe GameObject* GposeTarget { get => TargetSystem.Instance()->GPoseTarget; set => TargetSystem.Instance()->GPoseTarget = value; } - private static unsafe bool HasGposeTarget => GposeTarget != null; - private static unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex; + private unsafe bool HasGposeTarget => GposeTarget != null; + private unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex; public async Task GetGposeTargetGameObjectAsync() { diff --git a/LightlessSync/Services/PerformanceCollectorService.cs b/LightlessSync/Services/PerformanceCollectorService.cs index 877cc1c..d2b4c46 100644 --- a/LightlessSync/Services/PerformanceCollectorService.cs +++ b/LightlessSync/Services/PerformanceCollectorService.cs @@ -135,13 +135,13 @@ public sealed class PerformanceCollectorService : IHostedService if (pastEntries.Any()) { - sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries.Last().Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); + sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries[^1].Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); sb.Append('|'); sb.Append((" " + TimeSpan.FromTicks(pastEntries.Max(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); sb.Append('|'); sb.Append((" " + TimeSpan.FromTicks((long)pastEntries.Average(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); sb.Append('|'); - sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries.Last().Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' ')); + sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries[^1].Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' ')); sb.Append('|'); sb.Append((" " + pastEntries.Count).PadRight(10)); sb.Append('|'); @@ -183,7 +183,7 @@ public sealed class PerformanceCollectorService : IHostedService { try { - var last = entries.Value.ToList().Last(); + var last = entries.Value.ToList()[^1]; if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _)) { _logger.LogDebug("Could not remove performance counter {counter}", entries.Key); diff --git a/LightlessSync/Services/PlayerPerformanceService.cs b/LightlessSync/Services/PlayerPerformanceService.cs index 7db92e1..89a1e8e 100644 --- a/LightlessSync/Services/PlayerPerformanceService.cs +++ b/LightlessSync/Services/PlayerPerformanceService.cs @@ -97,7 +97,7 @@ public class PlayerPerformanceService warningText, pairHandler.Pair.UserData, pairHandler.Pair.IsPaused, - pairHandler.Pair.PlayerName)); + pairHandler.Pair.PlayerName ?? string.Empty)); } return true; @@ -148,8 +148,8 @@ public class PlayerPerformanceService $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", message, pair.UserData, - true, - pair.PlayerName)); + IsPaused: true, + pair.PlayerName ?? string.Empty)); _mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, $"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)"))); @@ -228,8 +228,8 @@ public class PlayerPerformanceService $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", message, pair.UserData, - true, - pair.PlayerName)); + IsPaused: true, + pair.PlayerName ?? string.Empty)); _mediator.Publish(new PauseMessage(pair.UserData)); diff --git a/LightlessSync/UI/CharaDataHubUi.McdOnline.cs b/LightlessSync/UI/CharaDataHubUi.McdOnline.cs index e86ef10..2703acc 100644 --- a/LightlessSync/UI/CharaDataHubUi.McdOnline.cs +++ b/LightlessSync/UI/CharaDataHubUi.McdOnline.cs @@ -406,7 +406,7 @@ internal sealed partial class CharaDataHubUi { _uiSharedService.BigText("Poses"); var poseCount = updateDto.PoseList.Count(); - using (ImRaii.Disabled(poseCount >= maxPoses)) + using (ImRaii.Disabled(poseCount >= _maxPoses)) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose")) { @@ -414,8 +414,8 @@ internal sealed partial class CharaDataHubUi } } ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == maxPoses)) - ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached"); + using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == _maxPoses)) + ImGui.TextUnformatted($"{poseCount}/{_maxPoses} poses attached"); ImGuiHelpers.ScaledDummy(5); using var indent = ImRaii.PushIndent(10f); diff --git a/LightlessSync/UI/CharaDataHubUi.cs b/LightlessSync/UI/CharaDataHubUi.cs index 51723b9..8eb85d5 100644 --- a/LightlessSync/UI/CharaDataHubUi.cs +++ b/LightlessSync/UI/CharaDataHubUi.cs @@ -20,7 +20,7 @@ namespace LightlessSync.UI; internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase { - private const int maxPoses = 10; + private const int _maxPoses = 10; private readonly CharaDataManager _charaDataManager; private readonly CharaDataNearbyManager _charaDataNearbyManager; private readonly CharaDataConfigService _configService; @@ -32,7 +32,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiSharedService; private CancellationTokenSource _closalCts = new(); private bool _disableUI = false; - private CancellationTokenSource _disposalCts = new(); + private readonly CancellationTokenSource _disposalCts = new(); private string _exportDescription = string.Empty; private string _filterCodeNote = string.Empty; private string _filterDescription = string.Empty; @@ -144,6 +144,8 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase { _closalCts.CancelDispose(); _disposalCts.CancelDispose(); + _disposalCts.Dispose(); + _closalCts.Dispose(); } base.Dispose(disposing); -- 2.49.1 From f3a6c7b5b96bfeb28f943ab230c1158d38d001ae Mon Sep 17 00:00:00 2001 From: cake Date: Sun, 16 Nov 2025 07:57:59 +0100 Subject: [PATCH 07/21] Fixed many warnings --- LightlessSync/UI/BroadcastUI.cs | 6 +- LightlessSync/UI/CompactUI.cs | 4 +- LightlessSync/UI/DataAnalysisUi.cs | 6 +- LightlessSync/UI/DrawEntityFactory.cs | 9 +- LightlessSync/UI/DtrEntry.cs | 2 +- LightlessSync/UI/EditProfileUi.cs | 6 +- LightlessSync/UI/JoinSyncshellUI.cs | 1 - LightlessSync/UI/LightlessNotificationUI.cs | 64 +++++------ LightlessSync/UI/SettingsUi.cs | 120 ++++++++++---------- LightlessSync/UI/SyncshellAdminUI.cs | 6 +- LightlessSync/UI/SyncshellFinderUI.cs | 4 +- LightlessSync/UI/UIColors.cs | 6 +- LightlessSync/UI/UISharedService.cs | 4 +- LightlessSync/Utils/Crypto.cs | 2 +- 14 files changed, 112 insertions(+), 128 deletions(-) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 60e064f..3dc1b89 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -192,7 +192,7 @@ namespace LightlessSync.UI ImGui.PopStyleVar(); ImGuiHelpers.ScaledDummy(3f); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (_configService.Current.BroadcastEnabled) { @@ -288,7 +288,7 @@ namespace LightlessSync.UI _uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue")); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); ImGui.PushTextWrapPos(); ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder."); @@ -296,7 +296,7 @@ namespace LightlessSync.UI ImGui.PopTextWrapPos(); ImGuiHelpers.ScaledDummy(0.2f); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled; bool isBroadcasting = _broadcastService.IsBroadcasting; diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 723d3ae..1d035e1 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -223,7 +223,7 @@ public class CompactUi : WindowMediatorSubscriberBase } using (ImRaii.PushId("header")) DrawUIDHeader(); - _uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f); + UiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f); using (ImRaii.PushId("serverstatus")) DrawServerStatus(); ImGui.Separator(); @@ -540,7 +540,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.PopStyleColor(); ImGuiHelpers.ScaledDummy(0.2f); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (_configService.Current.BroadcastEnabled) { diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 5b750f3..cd13515 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -581,10 +581,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.TableNextRow(); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):"); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TableNextColumn(); var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); @@ -651,7 +651,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.PopStyleVar(2); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); _uiSharedService.MediumText("Selected file:", UIColors.Get("LightlessBlue")); ImGui.SameLine(); diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index d1410ad..c53af92 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -7,14 +7,12 @@ using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Components; using LightlessSync.UI.Handlers; using LightlessSync.WebAPI; -using Microsoft.Extensions.Logging; using System.Collections.Immutable; namespace LightlessSync.UI; public class DrawEntityFactory { - private readonly ILogger _logger; private readonly ApiController _apiController; private readonly LightlessMediator _mediator; private readonly SelectPairForTagUi _selectPairForTagUi; @@ -25,19 +23,16 @@ public class DrawEntityFactory private readonly SelectTagForPairUi _selectTagForPairUi; private readonly RenamePairTagUi _renamePairTagUi; private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; - private readonly RenameSyncshellTagUi _renameSyncshellTagUi; - private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; private readonly TagHandler _tagHandler; private readonly IdDisplayHandler _uidDisplayHandler; - public DrawEntityFactory(ILogger logger, ApiController apiController, IdDisplayHandler uidDisplayHandler, + public DrawEntityFactory(ApiController apiController, IdDisplayHandler uidDisplayHandler, SelectTagForPairUi selectTagForPairUi, RenamePairTagUi renamePairTagUi, LightlessMediator mediator, TagHandler tagHandler, SelectPairForTagUi selectPairForTagUi, ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService, PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager, SelectTagForSyncshellUi selectTagForSyncshellUi, RenameSyncshellTagUi renameSyncshellTagUi, SelectSyncshellForTagUi selectSyncshellForTagUi) { - _logger = logger; _apiController = apiController; _uidDisplayHandler = uidDisplayHandler; _selectTagForPairUi = selectTagForPairUi; @@ -50,8 +45,6 @@ public class DrawEntityFactory _playerPerformanceConfigService = playerPerformanceConfigService; _charaDataManager = charaDataManager; _selectTagForSyncshellUi = selectTagForSyncshellUi; - _renameSyncshellTagUi = renameSyncshellTagUi; - _selectSyncshellForTagUi = selectSyncshellForTagUi; } public DrawFolderGroup CreateDrawGroupFolder(GroupFullInfoDto groupFullInfoDto, diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 17bc871..680aa29 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -445,7 +445,7 @@ public sealed class DtrEntry : IDisposable, IHostedService return ($"{icon} OFF", colors, tooltip.ToString()); } - private (string, Colors, string) FormatTooltip(string title, IEnumerable names, string icon, Colors color) + private static (string, Colors, string) FormatTooltip(string title, IEnumerable names, string icon, Colors color) { var list = names.Where(x => !string.IsNullOrEmpty(x)).ToList(); var tooltip = new StringBuilder() diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 880a0d2..58db2ff 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -171,7 +171,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.Checkbox("Is NSFW", ref nsfw); ImGui.EndDisabled(); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.EndTabItem(); } @@ -293,7 +293,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.BeginDisabled(); } - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); _uiSharedService.MediumText("Colored UID", UIColors.Get("LightlessPurple")); ImGui.Dummy(new Vector2(5)); @@ -377,7 +377,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.EndDisabled(); ImGui.Dummy(new Vector2(5)); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); if (!hasVanity) ImGui.EndDisabled(); diff --git a/LightlessSync/UI/JoinSyncshellUI.cs b/LightlessSync/UI/JoinSyncshellUI.cs index b02a84e..ffa4e23 100644 --- a/LightlessSync/UI/JoinSyncshellUI.cs +++ b/LightlessSync/UI/JoinSyncshellUI.cs @@ -1,5 +1,4 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 8cb6922..64fe3a2 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -1,8 +1,6 @@ using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Interface.Windowing; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services; @@ -27,11 +25,11 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase private const float _titleMessageSpacing = 4f; private const float _actionButtonSpacing = 8f; - private readonly List _notifications = new(); + private readonly List _notifications = []; private readonly object _notificationLock = new(); private readonly LightlessConfigService _configService; - private readonly Dictionary _notificationYOffsets = new(); - private readonly Dictionary _notificationTargetYOffsets = new(); + private readonly Dictionary _notificationYOffsets = []; + private readonly Dictionary _notificationTargetYOffsets = []; public LightlessNotificationUi(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) @@ -68,7 +66,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase { lock (_notificationLock) { - var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id); + var existingNotification = _notifications.FirstOrDefault(n => string.Equals(n.Id, notification.Id, StringComparison.Ordinal)); if (existingNotification != null) { UpdateExistingNotification(existingNotification, notification); @@ -103,7 +101,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase { lock (_notificationLock) { - var notification = _notifications.FirstOrDefault(n => n.Id == id); + var notification = _notifications.FirstOrDefault(n => string.Equals(n.Id, id, StringComparison.Ordinal)); if (notification != null) { StartOutAnimation(notification); @@ -122,13 +120,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private void StartOutAnimation(LightlessNotification notification) + private static void StartOutAnimation(LightlessNotification notification) { notification.IsAnimatingOut = true; notification.IsAnimatingIn = false; } - private bool ShouldRemoveNotification(LightlessNotification notification) => + private static bool ShouldRemoveNotification(LightlessNotification notification) => notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f; protected override void DrawInternal() @@ -185,7 +183,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase ImGui.SetCursorPosY(startY + yOffset); } - DrawNotification(notification, i); + DrawNotification(notification); } } @@ -304,7 +302,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0); } - private void DrawNotification(LightlessNotification notification, int index) + private void DrawNotification(LightlessNotification notification) { var alpha = notification.AnimationProgress; if (alpha <= 0f) return; @@ -339,7 +337,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered()); var accentColor = GetNotificationAccentColor(notification.Type); accentColor.W *= alpha; - + DrawShadow(drawList, windowPos, windowSize, alpha); HandleClickToDismiss(notification); DrawBackground(drawList, windowPos, windowSize, bgColor); @@ -370,7 +368,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return bgColor; } - private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha) + private static void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha) { var shadowOffset = new Vector2(1f, 1f); var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha); @@ -394,7 +392,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor) + private static void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor) { drawList.AddRectFilled( windowPos, @@ -431,14 +429,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase ); } - private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) + private static void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) { var progress = CalculateDurationProgress(notification); var progressBarColor = UIColors.Get("LightlessBlue"); var progressHeight = 2f; var progressY = windowPos.Y + windowSize.Y - progressHeight; var progressWidth = windowSize.X * progress; - + DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha); if (progress > 0) @@ -447,7 +445,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) + private static void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) { var progress = Math.Clamp(notification.Progress, 0f, 1f); var progressBarColor = UIColors.Get("LightlessGreen"); @@ -455,7 +453,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase // Position above the duration bar (2px duration bar + 1px spacing) var progressY = windowPos.Y + windowSize.Y - progressHeight - 3f; var progressWidth = windowSize.X * progress; - + DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha); if (progress > 0) @@ -464,14 +462,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private float CalculateDurationProgress(LightlessNotification notification) + private static float CalculateDurationProgress(LightlessNotification notification) { // Calculate duration timer progress var elapsed = DateTime.UtcNow - notification.CreatedAt; return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); } - private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha) + private static void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha) { var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha); drawList.AddRectFilled( @@ -482,7 +480,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase ); } - private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha) + private static void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha) { var progressColor = progressBarColor; progressColor.W *= alpha; @@ -512,13 +510,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private float CalculateContentWidth(float windowWidth) => + private static float CalculateContentWidth(float windowWidth) => windowWidth - (_contentPaddingX * 2); - private bool HasActions(LightlessNotification notification) => + private static bool HasActions(LightlessNotification notification) => notification.Actions.Count > 0; - private void PositionActionsAtBottom(float windowHeight) + private static void PositionActionsAtBottom(float windowHeight) { var actionHeight = ImGui.GetFrameHeight(); var bottomY = windowHeight - _contentPaddingY - actionHeight; @@ -546,7 +544,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return $"[{timestamp}] {notification.Title}"; } - private float DrawWrappedText(string text, float wrapWidth) + private static float DrawWrappedText(string text, float wrapWidth) { ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth); var startY = ImGui.GetCursorPosY(); @@ -556,7 +554,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return height; } - private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha) + private static void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha) { if (string.IsNullOrEmpty(notification.Message)) return; @@ -591,13 +589,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private float CalculateActionButtonWidth(int actionCount, float availableWidth) + private static float CalculateActionButtonWidth(int actionCount, float availableWidth) { var totalSpacing = (actionCount - 1) * _actionButtonSpacing; return (availableWidth - totalSpacing) / actionCount; } - private void PositionActionButton(int index, float startX, float buttonWidth) + private static void PositionActionButton(int index, float startX, float buttonWidth) { var xPosition = startX + index * (buttonWidth + _actionButtonSpacing); ImGui.SetCursorPosX(xPosition); @@ -625,7 +623,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase if (action.Icon != FontAwesomeIcon.None) { - buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha); + buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth); } else { @@ -650,10 +648,10 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha) + private static bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width) { var drawList = ImGui.GetWindowDrawList(); - var cursorPos = ImGui.GetCursorScreenPos(); + ImGui.GetCursorScreenPos(); var frameHeight = ImGui.GetFrameHeight(); Vector2 iconSize; @@ -729,7 +727,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return ImGui.CalcTextSize(titleText, true, contentWidth).Y; } - private float CalculateMessageHeight(LightlessNotification notification, float contentWidth) + private static float CalculateMessageHeight(LightlessNotification notification, float contentWidth) { if (string.IsNullOrEmpty(notification.Message)) return 0f; @@ -737,7 +735,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return 4f + messageHeight; } - private Vector4 GetNotificationAccentColor(NotificationType type) + private static Vector4 GetNotificationAccentColor(NotificationType type) { return type switch { diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 7cdeb38..60de533 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -539,7 +539,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } } - private bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null) + private static bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null) { using var id = ImRaii.PushId($"reset-{key}"); using var disabled = ImRaii.Disabled(!hasOverride); @@ -663,7 +663,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Controls how many uploads can run at once."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (ImGui.Checkbox("Enable Pair Download Limiter", ref limitPairApplications)) { @@ -710,7 +710,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TextColored(ImGuiColors.DalamudGrey, "Pair apply limiter is disabled."); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload)) { @@ -826,13 +826,10 @@ public class SettingsUi : WindowMediatorSubscriberBase using var tree = ImRaii.TreeNode("Speed Test to Servers"); if (tree) { - if (_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && - (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) + if ((_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && + (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) && _uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List")) { - if (_uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List")) - { - _downloadServersTask = GetDownloadServerList(); - } + _downloadServersTask = GetDownloadServerList(); } if (_downloadServersTask != null && _downloadServersTask.IsCompleted && @@ -1063,9 +1060,9 @@ public class SettingsUi : WindowMediatorSubscriberBase .DeserializeAsync>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false)) .ConfigureAwait(false); } - catch (Exception ex) + catch (Exception) { - _logger.LogWarning(ex, "Failed to get download server list"); + _logger.LogWarning("Failed to get download server list"); throw; } } @@ -1146,7 +1143,7 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.TooltipSeparator + "Keeping LOD enabled can lead to more crashes. Use at your own risk."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f); } private void DrawFileStorageSettings() @@ -1348,7 +1345,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } } - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } @@ -1380,7 +1377,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } catch (IOException ex) { - _logger.LogWarning(ex, $"Could not delete file {file} because it is in use."); + _logger.LogWarning(ex, "Could not delete file {file} because it is in use.", file); } } }); @@ -1398,7 +1395,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndDisabled(); ImGui.Unindent(); - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); ImGui.TreePop(); } } @@ -1411,8 +1408,6 @@ public class SettingsUi : WindowMediatorSubscriberBase } _lastTab = "General"; - //UiSharedService.FontText("Experimental", _uiShared.UidFont); - //ImGui.Separator(); _uiShared.UnderlinedBigText("General Settings", UIColors.Get("LightlessBlue")); ImGui.Dummy(new Vector2(10)); @@ -1449,7 +1444,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGuiColors.DalamudRed); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -1477,7 +1472,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "This will automatically populate user notes using the first encountered player name if the note was not set prior"); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -1545,7 +1540,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -1585,7 +1580,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Lightfinder Nameplate Colors"); if (ImGui.BeginTable("##LightfinderColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) @@ -1641,7 +1636,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Lightfinder Info Bar"); if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) @@ -1737,7 +1732,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.EndDisabled(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Alignment"); ImGui.BeginDisabled(autoAlign); @@ -1862,7 +1857,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Visibility"); var showOwn = _configService.Current.LightfinderLabelShowOwn; @@ -1900,7 +1895,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("Toggles Lightfinder label when no nameplate(s) is visible."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Label"); var useIcon = _configService.Current.LightfinderLabelUseIcon; @@ -2006,7 +2001,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _lightfinderIconPresetIndex = -1; } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -2094,7 +2089,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Server Info Bar Colors"); @@ -2146,7 +2141,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Nameplate Colors"); @@ -2199,7 +2194,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("UI Theme"); @@ -2221,7 +2216,7 @@ public class SettingsUi : WindowMediatorSubscriberBase DrawThemeOverridesSection(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -2319,7 +2314,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -2362,7 +2357,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } ImGui.Separator(); @@ -2444,7 +2439,7 @@ public class SettingsUi : WindowMediatorSubscriberBase + "Default: 165 thousand"); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -2548,7 +2543,7 @@ public class SettingsUi : WindowMediatorSubscriberBase + "Default: 250 thousand"); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -2696,7 +2691,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndPopup(); } - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); ImGui.TreePop(); } @@ -3274,15 +3269,13 @@ public class SettingsUi : WindowMediatorSubscriberBase private int _lastSelectedServerIndex = -1; private Task<(bool Success, bool PartialSuccess, string Result)>? _secretKeysConversionTask = null; - private CancellationTokenSource _secretKeysConversionCts = new CancellationTokenSource(); + private CancellationTokenSource _secretKeysConversionCts = new(); private async Task<(bool Success, bool partialSuccess, string Result)> ConvertSecretKeysToUIDs( ServerStorage serverStorage, CancellationToken token) { - List failedConversions = serverStorage.Authentications - .Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID)).ToList(); - List conversionsToAttempt = serverStorage.Authentications - .Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)).ToList(); + List failedConversions = [.. serverStorage.Authentications.Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID))]; + List conversionsToAttempt = [.. serverStorage.Authentications.Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID))]; List successfulConversions = []; Dictionary> secretKeyMapping = new(StringComparer.Ordinal); foreach (var authEntry in conversionsToAttempt) @@ -3352,6 +3345,7 @@ public class SettingsUi : WindowMediatorSubscriberBase sb.Append(string.Join(", ", failedConversions.Select(k => k.CharacterName))); } + _secretKeysConversionCts.Dispose(); return (true, failedConversions.Count != 0, sb.ToString()); } @@ -3720,7 +3714,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Unindent(); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -3762,7 +3756,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Click anywhere on a notification to dismiss it. Notifications with action buttons (like pair requests) are excluded."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -3925,7 +3919,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SetTooltip("Right click to reset to default (3)."); _uiShared.DrawHelpText("Width of the colored accent bar on the left side."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } } @@ -4020,7 +4014,7 @@ public class SettingsUi : WindowMediatorSubscriberBase if (ImGui.IsItemHovered()) ImGui.SetTooltip("Right click to reset to default (20)."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -4035,7 +4029,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "Configure which sounds play for each notification type. Use the play button to preview sounds."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -4083,7 +4077,7 @@ public class SettingsUi : WindowMediatorSubscriberBase "Only show online notifications for pairs where you have set an individual note."); ImGui.Unindent(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f); ImGui.TreePop(); } @@ -4099,7 +4093,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "When you receive a pair request, show Accept/Decline buttons in the notification."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -4115,7 +4109,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "When a player exceeds performance thresholds or is auto-paused, show Pause/Unpause buttons in the notification."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f); ImGui.TreePop(); } @@ -4130,7 +4124,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Disable warning notifications for missing optional plugins."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } @@ -4140,32 +4134,32 @@ public class SettingsUi : WindowMediatorSubscriberBase } } - private NotificationLocation[] GetLightlessNotificationLocations() + private static NotificationLocation[] GetLightlessNotificationLocations() { - return new[] - { + return + [ NotificationLocation.LightlessUi, NotificationLocation.Chat, NotificationLocation.ChatAndLightlessUi, NotificationLocation.Nowhere - }; + ]; } - private NotificationLocation[] GetDownloadNotificationLocations() + private static NotificationLocation[] GetDownloadNotificationLocations() { - return new[] - { + return + [ NotificationLocation.LightlessUi, NotificationLocation.TextOverlay, NotificationLocation.Nowhere - }; + ]; } - private NotificationLocation[] GetClassicNotificationLocations() + private static NotificationLocation[] GetClassicNotificationLocations() { - return new[] - { + return + [ NotificationLocation.Toast, NotificationLocation.Chat, NotificationLocation.Both, NotificationLocation.Nowhere - }; + ]; } - private string GetNotificationLocationLabel(NotificationLocation location) + private static string GetNotificationLocationLabel(NotificationLocation location) { return location switch { @@ -4180,7 +4174,7 @@ public class SettingsUi : WindowMediatorSubscriberBase }; } - private string GetNotificationCornerLabel(NotificationCorner corner) + private static string GetNotificationCornerLabel(NotificationCorner corner) { return corner switch { diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 7497e78..c4cb28c 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -552,7 +552,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } } } - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } ImGui.Separator(); @@ -643,7 +643,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed."); } } - _uiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); ImGui.TreePop(); } ImGui.Separator(); @@ -689,7 +689,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } ImGui.EndTable(); } - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } ImGui.Separator(); diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index d7f5605..3181513 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -73,7 +73,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase protected override void DrawInternal() { _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); - _uiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); if (_nearbySyncshells.Count == 0) { @@ -82,7 +82,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase if (!_broadcastService.IsBroadcasting) { - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active."); ImGuiHelpers.ScaledDummy(0.5f); diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index 3c1eabd..dadb9f0 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -40,7 +40,7 @@ namespace LightlessSync.UI return HexToRgba(customColorHex); if (!DefaultHexColors.TryGetValue(name, out var hex)) - throw new ArgumentException($"Color '{name}' not found in UIColors."); + throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); return HexToRgba(hex); } @@ -48,7 +48,7 @@ namespace LightlessSync.UI public static void Set(string name, Vector4 color) { if (!DefaultHexColors.ContainsKey(name)) - throw new ArgumentException($"Color '{name}' not found in UIColors."); + throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); if (_configService != null) { @@ -78,7 +78,7 @@ namespace LightlessSync.UI public static Vector4 GetDefault(string name) { if (!DefaultHexColors.TryGetValue(name, out var hex)) - throw new ArgumentException($"Color '{name}' not found in UIColors."); + throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); return HexToRgba(hex); } diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index eb3acce..ae9c7a9 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -475,7 +475,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ); } - public void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f) + public static void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f) { var drawList = ImGui.GetWindowDrawList(); var min = ImGui.GetCursorScreenPos(); @@ -494,7 +494,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Dummy(new Vector2(0, thickness * ImGuiHelpers.GlobalScale)); } - public void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f) + public static void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f) { float scale = ImGuiHelpers.GlobalScale; diff --git a/LightlessSync/Utils/Crypto.cs b/LightlessSync/Utils/Crypto.cs index c31f82f..11f08fd 100644 --- a/LightlessSync/Utils/Crypto.cs +++ b/LightlessSync/Utils/Crypto.cs @@ -17,7 +17,7 @@ public static class Crypto { using SHA1 sha1 = SHA1.Create(); using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - return BitConverter.ToString(sha1.ComputeHash(stream)).Replace("-", "", StringComparison.Ordinal); + return Convert.ToHexString(sha1.ComputeHash(stream)); } public static async Task GetFileHashAsync(string filePath, CancellationToken cancellationToken = default) -- 2.49.1 From 1615f2433b3e1288b98d3d04b03f78fbef721cc5 Mon Sep 17 00:00:00 2001 From: cake Date: Mon, 17 Nov 2025 04:06:20 +0100 Subject: [PATCH 08/21] Smoothed download bar, fixed many warnings --- LightlessSync.sln | 16 +-- LightlessSync/FileCache/CacheMonitor.cs | 114 ++++++++++-------- LightlessSync/FileCache/FileCacheManager.cs | 5 +- LightlessSync/UI/CharaDataHubUi.McdOnline.cs | 12 +- LightlessSync/UI/DownloadUi.cs | 15 ++- LightlessSync/UI/Models/Changelog.cs | 43 ------- LightlessSync/UI/Models/ChangelogEntry.cs | 12 ++ LightlessSync/UI/Models/ChangelogFile.cs | 10 ++ LightlessSync/UI/Models/ChangelogVersion.cs | 8 ++ LightlessSync/UI/Models/CreditCategory.cs | 8 ++ LightlessSync/UI/Models/CreditItem.cs | 8 ++ LightlessSync/UI/Models/CreditsFile.cs | 7 ++ .../UI/Models/LightlessNotification.cs | 14 +-- .../UI/Models/LightlessNotificationAction.cs | 15 +++ LightlessSync/UI/UIColors.cs | 8 +- LightlessSync/UI/UISharedService.cs | 8 +- LightlessSync/UI/UpdateNotesUi.cs | 4 +- LightlessSync/Utils/Crypto.cs | 4 +- LightlessSync/Utils/SeStringUtils.cs | 11 +- 19 files changed, 181 insertions(+), 141 deletions(-) delete mode 100644 LightlessSync/UI/Models/Changelog.cs create mode 100644 LightlessSync/UI/Models/ChangelogEntry.cs create mode 100644 LightlessSync/UI/Models/ChangelogFile.cs create mode 100644 LightlessSync/UI/Models/ChangelogVersion.cs create mode 100644 LightlessSync/UI/Models/CreditCategory.cs create mode 100644 LightlessSync/UI/Models/CreditItem.cs create mode 100644 LightlessSync/UI/Models/CreditsFile.cs create mode 100644 LightlessSync/UI/Models/LightlessNotificationAction.cs diff --git a/LightlessSync.sln b/LightlessSync.sln index 5b7ca3c..768301b 100644 --- a/LightlessSync.sln +++ b/LightlessSync.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32328.378 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}" ProjectSection(SolutionItems) = preProject @@ -22,24 +22,24 @@ Global Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Release|x64 - {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Release|x64 + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Debug|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.ActiveCfg = Debug|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.Build.0 = Debug|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.ActiveCfg = Release|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.Build.0 = Release|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.ActiveCfg = Release|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.Build.0 = Release|x64 - {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Release|Any CPU + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.ActiveCfg = Debug|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.Build.0 = Debug|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.Build.0 = Release|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.ActiveCfg = Release|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.Build.0 = Release|Any CPU - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.ActiveCfg = Debug|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.Build.0 = Debug|x64 + {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.ActiveCfg = Release|x64 + {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.Build.0 = Release|x64 {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.ActiveCfg = Debug|x64 {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.Build.0 = Debug|x64 {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|Any CPU.ActiveCfg = Release|x64 diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index 486e11e..0dc0cb7 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -72,7 +72,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested) { - await Task.Delay(1).ConfigureAwait(false); + await Task.Delay(1, token).ConfigureAwait(false); } RecalculateFileCacheSize(token); @@ -101,8 +101,8 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null); - private readonly Dictionary _watcherChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _lightlessChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _watcherChanges = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _lightlessChanges = new(StringComparer.OrdinalIgnoreCase); public void StopMonitoring() { @@ -128,7 +128,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } var fsType = FileSystemHelper.GetFilesystemType(_configService.Current.CacheFolder, _dalamudUtil.IsWine); - if (fsType == FileSystemHelper.FilesystemType.NTFS) + if (fsType == FileSystemHelper.FilesystemType.NTFS && !_dalamudUtil.IsWine) { StorageisNTFS = true; Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS); @@ -403,94 +403,99 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase public void RecalculateFileCacheSize(CancellationToken token) { - if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || - !Directory.Exists(_configService.Current.CacheFolder)) + var folder = _configService.Current.CacheFolder; + if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) { FileCacheSize = 0; return; } FileCacheSize = -1; - bool isWine = _dalamudUtil?.IsWine ?? false; try { var drive = DriveInfo.GetDrives() - .FirstOrDefault(d => _configService.Current.CacheFolder - .StartsWith(d.Name, StringComparison.OrdinalIgnoreCase)); - + .FirstOrDefault(d => folder.StartsWith(d.Name, StringComparison.OrdinalIgnoreCase)); if (drive != null) FileCacheDriveFree = drive.AvailableFreeSpace; } catch (Exception ex) { - Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder); + Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", folder); } - var files = Directory.EnumerateFiles(_configService.Current.CacheFolder) - .Select(f => new FileInfo(f)) - .OrderBy(f => f.LastAccessTime) - .ToList(); - - long totalSize = 0; + List files; + try + { + files = [.. new DirectoryInfo(folder) + .EnumerateFiles("*", SearchOption.TopDirectoryOnly) + .OrderBy(f => f.LastAccessTimeUtc)]; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to enumerate files in {folder}", folder); + FileCacheSize = 0; + return; + } + + var entries = new List<(FileInfo fi, long size)>(files.Count); + long total = 0; foreach (var f in files) { token.ThrowIfCancellationRequested(); - try + long size; + if (_configService.Current.UseCompactor) { - long size = 0; - - if (!isWine) + try { - try - { - size = _fileCompactor.GetFileSizeOnDisk(f); - } - catch (Exception ex) - { - Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName); - size = f.Length; - } + size = _fileCompactor.GetFileSizeOnDisk(f); + if (size < 0) size = f.Length; } - else + catch (Exception ex) { + Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using Length", f.FullName); size = f.Length; } - - totalSize += size; - } - catch (Exception ex) + } + else { - Logger.LogTrace(ex, "Error getting size for {file}", f.FullName); + size = f.Length; } + + + entries.Add((f, size)); + total += size; } - FileCacheSize = totalSize; + FileCacheSize = total; - var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); - if (FileCacheSize < maxCacheInBytes) - return; + var maxCacheBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); + if (FileCacheSize < maxCacheBytes) return; - var maxCacheBuffer = maxCacheInBytes * 0.05d; + var buffer = (long)(maxCacheBytes * 0.05d); + var target = maxCacheBytes - buffer; - while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer && files.Count > 0) + var i = 0; + while (i < entries.Count && FileCacheSize > target) { - var oldestFile = files[0]; + token.ThrowIfCancellationRequested(); + var (fi, sz) = entries[i]; try { - long fileSize = oldestFile.Length; - File.Delete(oldestFile.FullName); - FileCacheSize -= fileSize; + File.Delete(fi.FullName); + FileCacheSize -= sz; } catch (Exception ex) { - Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullName); + Logger.LogTrace(ex, "Failed to delete old file {file}", fi.FullName); + } + finally + { + i++; } - - files.RemoveAt(0); } } @@ -510,12 +515,19 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase protected override void Dispose(bool disposing) { base.Dispose(disposing); - _scanCancellationTokenSource?.Cancel(); + // Disposing of file system watchers PenumbraWatcher?.Dispose(); LightlessWatcher?.Dispose(); + + // Disposing of cancellation token sources + _scanCancellationTokenSource?.CancelDispose(); + _scanCancellationTokenSource?.Dispose(); _penumbraFswCts?.CancelDispose(); + _penumbraFswCts?.Dispose(); _lightlessFswCts?.CancelDispose(); + _lightlessFswCts?.Dispose(); _periodicCalculationTokenSource?.CancelDispose(); + _periodicCalculationTokenSource?.Dispose(); } private void FullFileScan(CancellationToken ct) @@ -593,7 +605,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase List entitiesToRemove = []; List entitiesToUpdate = []; - object sync = new(); + Lock sync = new(); Thread[] workerThreads = new Thread[threadCount]; ConcurrentQueue fileCaches = new(_fileDbManager.GetAllFileCaches()); diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 29d2aa7..40feede 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -41,11 +41,8 @@ public sealed class FileCacheManager : IHostedService private string CsvBakPath => _csvPath + ".bak"; - private static string NormalizeSeparators(string path) - { - return path.Replace("/", "\\", StringComparison.Ordinal) + private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal) .Replace("\\\\", "\\", StringComparison.Ordinal); - } private static string NormalizePrefixedPathKey(string prefixedPath) { diff --git a/LightlessSync/UI/CharaDataHubUi.McdOnline.cs b/LightlessSync/UI/CharaDataHubUi.McdOnline.cs index 2703acc..d4aeaa9 100644 --- a/LightlessSync/UI/CharaDataHubUi.McdOnline.cs +++ b/LightlessSync/UI/CharaDataHubUi.McdOnline.cs @@ -463,12 +463,16 @@ internal sealed partial class CharaDataHubUi else { var desc = pose.Description; - if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100)) + if (desc != null) { - pose.Description = desc; - updateDto.UpdatePoseList(); + if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100)) + { + pose.Description = desc; + updateDto.UpdatePoseList(); + } + ImGui.SameLine(); } - ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete")) { updateDto.RemovePose(pose); diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index 337fb41..1902124 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -23,6 +23,7 @@ public class DownloadUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiShared; private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ConcurrentDictionary _uploadingPlayers = new(); + private readonly Dictionary _smoothed = []; private bool _notificationDismissed = true; private int _lastDownloadStateHash = 0; @@ -204,8 +205,18 @@ public class DownloadUi : WindowMediatorSubscriberBase foreach (var transfer in _currentDownloads.ToList()) { - var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GetGameObject()); - if (screenPos == Vector2.Zero) continue; + var transferKey = transfer.Key; + var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject()); + //If RawPos is zero, remove it from smoothed dictionary + if (rawPos == Vector2.Zero) + { + _smoothed.Remove(transferKey); + continue; + } + //Smoothing out the movement and fix jitter around the position. + Vector2 screenPos = _smoothed.TryGetValue(transferKey, out var lastPos) ? (rawPos - lastPos).Length() < 4f ? lastPos : rawPos : rawPos; + _smoothed[transferKey] = screenPos; + var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes); var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes); diff --git a/LightlessSync/UI/Models/Changelog.cs b/LightlessSync/UI/Models/Changelog.cs deleted file mode 100644 index 23d26c4..0000000 --- a/LightlessSync/UI/Models/Changelog.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace LightlessSync.UI.Models -{ - public class ChangelogFile - { - public string Tagline { get; init; } = string.Empty; - public string Subline { get; init; } = string.Empty; - public List Changelog { get; init; } = new(); - public List? Credits { get; init; } - } - - public class ChangelogEntry - { - public string Name { get; init; } = string.Empty; - public string Date { get; init; } = string.Empty; - public string Tagline { get; init; } = string.Empty; - public bool? IsCurrent { get; init; } - public string? Message { get; init; } - public List? Versions { get; init; } - } - - public class ChangelogVersion - { - public string Number { get; init; } = string.Empty; - public List Items { get; init; } = new(); - } - - public class CreditCategory - { - public string Category { get; init; } = string.Empty; - public List Items { get; init; } = new(); - } - - public class CreditItem - { - public string Name { get; init; } = string.Empty; - public string Role { get; init; } = string.Empty; - } - - public class CreditsFile - { - public List Credits { get; init; } = new(); - } -} \ No newline at end of file diff --git a/LightlessSync/UI/Models/ChangelogEntry.cs b/LightlessSync/UI/Models/ChangelogEntry.cs new file mode 100644 index 0000000..919a6da --- /dev/null +++ b/LightlessSync/UI/Models/ChangelogEntry.cs @@ -0,0 +1,12 @@ +namespace LightlessSync.UI.Models +{ + public class ChangelogEntry + { + public string Name { get; init; } = string.Empty; + public string Date { get; init; } = string.Empty; + public string Tagline { get; init; } = string.Empty; + public bool? IsCurrent { get; init; } + public string? Message { get; init; } + public List? Versions { get; init; } + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/ChangelogFile.cs b/LightlessSync/UI/Models/ChangelogFile.cs new file mode 100644 index 0000000..37997c8 --- /dev/null +++ b/LightlessSync/UI/Models/ChangelogFile.cs @@ -0,0 +1,10 @@ +namespace LightlessSync.UI.Models +{ + public class ChangelogFile + { + public string Tagline { get; init; } = string.Empty; + public string Subline { get; init; } = string.Empty; + public List Changelog { get; init; } = new(); + public List? Credits { get; init; } + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/ChangelogVersion.cs b/LightlessSync/UI/Models/ChangelogVersion.cs new file mode 100644 index 0000000..b70ace6 --- /dev/null +++ b/LightlessSync/UI/Models/ChangelogVersion.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.UI.Models +{ + public class ChangelogVersion + { + public string Number { get; init; } = string.Empty; + public List Items { get; init; } = []; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/CreditCategory.cs b/LightlessSync/UI/Models/CreditCategory.cs new file mode 100644 index 0000000..5b25cca --- /dev/null +++ b/LightlessSync/UI/Models/CreditCategory.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.UI.Models +{ + public class CreditCategory + { + public string Category { get; init; } = string.Empty; + public List Items { get; init; } = []; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/CreditItem.cs b/LightlessSync/UI/Models/CreditItem.cs new file mode 100644 index 0000000..ae0c4be --- /dev/null +++ b/LightlessSync/UI/Models/CreditItem.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.UI.Models +{ + public class CreditItem + { + public string Name { get; init; } = string.Empty; + public string Role { get; init; } = string.Empty; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/CreditsFile.cs b/LightlessSync/UI/Models/CreditsFile.cs new file mode 100644 index 0000000..b6b6a83 --- /dev/null +++ b/LightlessSync/UI/Models/CreditsFile.cs @@ -0,0 +1,7 @@ +namespace LightlessSync.UI.Models +{ + public class CreditsFile + { + public List Credits { get; init; } = []; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/LightlessNotification.cs b/LightlessSync/UI/Models/LightlessNotification.cs index 3c6edea..4ae49a4 100644 --- a/LightlessSync/UI/Models/LightlessNotification.cs +++ b/LightlessSync/UI/Models/LightlessNotification.cs @@ -1,7 +1,7 @@ -using Dalamud.Interface; using LightlessSync.LightlessConfiguration.Models; -using System.Numerics; + namespace LightlessSync.UI.Models; + public class LightlessNotification { public string Id { get; set; } = Guid.NewGuid().ToString(); @@ -20,13 +20,3 @@ public class LightlessNotification public bool IsAnimatingOut { get; set; } = false; public uint? SoundEffectId { get; set; } = null; } -public class LightlessNotificationAction -{ - public string Id { get; set; } = Guid.NewGuid().ToString(); - public string Label { get; set; } = string.Empty; - public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None; - public Vector4 Color { get; set; } = Vector4.One; - public Action OnClick { get; set; } = _ => { }; - public bool IsPrimary { get; set; } = false; - public bool IsDestructive { get; set; } = false; -} \ No newline at end of file diff --git a/LightlessSync/UI/Models/LightlessNotificationAction.cs b/LightlessSync/UI/Models/LightlessNotificationAction.cs new file mode 100644 index 0000000..7c9fd53 --- /dev/null +++ b/LightlessSync/UI/Models/LightlessNotificationAction.cs @@ -0,0 +1,15 @@ +using Dalamud.Interface; +using System.Numerics; + +namespace LightlessSync.UI.Models; + +public class LightlessNotificationAction +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Label { get; set; } = string.Empty; + public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None; + public Vector4 Color { get; set; } = Vector4.One; + public Action OnClick { get; set; } = _ => { }; + public bool IsPrimary { get; set; } = false; + public bool IsDestructive { get; set; } = false; +} \ No newline at end of file diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index dadb9f0..2b2e65c 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -96,10 +96,10 @@ namespace LightlessSync.UI public static Vector4 HexToRgba(string hexColor) { hexColor = hexColor.TrimStart('#'); - int r = int.Parse(hexColor.Substring(0, 2), NumberStyles.HexNumber); - int g = int.Parse(hexColor.Substring(2, 2), NumberStyles.HexNumber); - int b = int.Parse(hexColor.Substring(4, 2), NumberStyles.HexNumber); - int a = hexColor.Length == 8 ? int.Parse(hexColor.Substring(6, 2), NumberStyles.HexNumber) : 255; + int r = int.Parse(hexColor[..2], NumberStyles.HexNumber); + int g = int.Parse(hexColor[2..4], NumberStyles.HexNumber); + int b = int.Parse(hexColor[4..6], NumberStyles.HexNumber); + int a = hexColor.Length == 8 ? int.Parse(hexColor[6..8], NumberStyles.HexNumber) : 255; return new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); } diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index ae9c7a9..243de3d 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -70,7 +70,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase private bool _isOneDrive = false; private bool _isPenumbraDirectory = false; private bool _moodlesExists = false; - private Dictionary _oauthTokenExpiry = new(); + private readonly Dictionary _oauthTokenExpiry = []; private bool _penumbraExists = false; private bool _petNamesExists = false; private int _serverSelectionIndex = -1; @@ -1067,7 +1067,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase { using (ImRaii.Disabled(_discordOAuthUIDs == null)) { - var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UIDAliasPair(t.Key, t.Value)).ToList() ?? [new UIDAliasPair(item.UID ?? null, null)]; + var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UidAliasPair(t.Key, t.Value)).ToList() ?? [new UidAliasPair(item.UID ?? null, null)]; var uidComboName = "UID###" + item.CharacterName + item.WorldId + serverUri + indexOffset + aliasPairs.Count; DrawCombo(uidComboName, aliasPairs, (v) => @@ -1253,6 +1253,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase UidFont.Dispose(); GameFont.Dispose(); MediumFont.Dispose(); + _discordOAuthGetCts.Dispose(); } private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) @@ -1325,6 +1326,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return result; } + public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling); - private record UIDAliasPair(string? UID, string? Alias); + private sealed record UidAliasPair(string? UID, string? Alias); } \ No newline at end of file diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 54a45f0..5fb2480 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -742,7 +742,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml"); if (changelogStream != null) { - using var reader = new StreamReader(changelogStream, Encoding.UTF8, true, 128); + using var reader = new StreamReader(changelogStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128); var yaml = reader.ReadToEnd(); _changelog = deserializer.Deserialize(yaml) ?? new(); } @@ -751,7 +751,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase using var creditsStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.credits.yaml"); if (creditsStream != null) { - using var reader = new StreamReader(creditsStream, Encoding.UTF8, true, 128); + using var reader = new StreamReader(creditsStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128); var yaml = reader.ReadToEnd(); _credits = deserializer.Deserialize(yaml) ?? new(); } diff --git a/LightlessSync/Utils/Crypto.cs b/LightlessSync/Utils/Crypto.cs index 11f08fd..0e8879f 100644 --- a/LightlessSync/Utils/Crypto.cs +++ b/LightlessSync/Utils/Crypto.cs @@ -46,7 +46,7 @@ public static class Crypto return hash; return _hashListPlayersSHA256[playerToHash] = - BitConverter.ToString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString()))).Replace("-", "", StringComparison.Ordinal); + Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString()))); } public static string GetHash256(this string stringToHash) @@ -60,7 +60,7 @@ public static class Crypto return hash; return _hashListSHA256[stringToCompute] = - BitConverter.ToString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))).Replace("-", "", StringComparison.Ordinal); + Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))); } #pragma warning restore SYSLIB0021 // Type or member is obsolete } \ No newline at end of file diff --git a/LightlessSync/Utils/SeStringUtils.cs b/LightlessSync/Utils/SeStringUtils.cs index 7507515..6cb1686 100644 --- a/LightlessSync/Utils/SeStringUtils.cs +++ b/LightlessSync/Utils/SeStringUtils.cs @@ -57,10 +57,9 @@ public static class SeStringUtils continue; var hasColor = fragment.Color.HasValue; - Vector4 color = default; if (hasColor) { - color = fragment.Color!.Value; + Vector4 color = fragment.Color!.Value; builder.PushColorRgba(color); } @@ -233,7 +232,7 @@ public static class SeStringUtils protected abstract byte ChunkType { get; } } - private class ColorPayload : AbstractColorPayload + private sealed class ColorPayload : AbstractColorPayload { protected override byte ChunkType => 0x13; @@ -247,12 +246,12 @@ public static class SeStringUtils public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } } - private class ColorEndPayload : AbstractColorEndPayload + private sealed class ColorEndPayload : AbstractColorEndPayload { protected override byte ChunkType => 0x13; } - private class GlowPayload : AbstractColorPayload + private sealed class GlowPayload : AbstractColorPayload { protected override byte ChunkType => 0x14; @@ -266,7 +265,7 @@ public static class SeStringUtils public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } } - private class GlowEndPayload : AbstractColorEndPayload + private sealed class GlowEndPayload : AbstractColorEndPayload { protected override byte ChunkType => 0x14; } -- 2.49.1 From bebf6c745b02a298192aebb37d4e794270de695f Mon Sep 17 00:00:00 2001 From: cake Date: Mon, 17 Nov 2025 05:32:12 +0100 Subject: [PATCH 09/21] Fixed not clickable notifications --- LightlessSync/FileCache/FileCacheManager.cs | 8 ++++---- LightlessSync/UI/LightlessNotificationUI.cs | 9 ++++++--- LightlessSync/Utils/Crypto.cs | 13 ++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 40feede..c0a1bf3 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -578,13 +578,13 @@ public sealed class FileCacheManager : IHostedService { if (!File.Exists(_csvPath)) { - File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry }); + File.WriteAllLines(_csvPath, [BuildVersionHeader(), entity.CsvEntry]); _csvHeaderEnsured = true; } else { EnsureCsvHeaderLockedCached(); - File.AppendAllLines(_csvPath, new[] { entity.CsvEntry }); + File.AppendAllLines(_csvPath, [entity.CsvEntry]); } } var result = GetFileCacheByPath(fileInfo.FullName); @@ -721,7 +721,7 @@ public sealed class FileCacheManager : IHostedService BackupUnsupportedCache("invalid-version"); parseEntries = false; rewriteRequired = true; - entries = Array.Empty(); + entries = []; } else if (parsedVersion != FileCacheVersion) { @@ -729,7 +729,7 @@ public sealed class FileCacheManager : IHostedService BackupUnsupportedCache($"v{parsedVersion}"); parseEntries = false; rewriteRequired = true; - entries = Array.Empty(); + entries = []; } else { diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 64fe3a2..bdbe8df 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -43,7 +43,6 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoCollapse | - ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.AlwaysAutoResize; @@ -382,9 +381,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase private void HandleClickToDismiss(LightlessNotification notification) { - if (ImGui.IsWindowHovered() && + var pos = ImGui.GetWindowPos(); + var size = ImGui.GetWindowSize(); + bool hovered = ImGui.IsMouseHoveringRect(pos, new Vector2(pos.X + size.X, pos.Y + size.Y)); + + if ((hovered || ImGui.IsWindowHovered()) && _configService.Current.DismissNotificationOnClick && - !notification.Actions.Any() && + notification.Actions.Count == 0 && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { notification.IsDismissed = true; diff --git a/LightlessSync/Utils/Crypto.cs b/LightlessSync/Utils/Crypto.cs index 0e8879f..25215c0 100644 --- a/LightlessSync/Utils/Crypto.cs +++ b/LightlessSync/Utils/Crypto.cs @@ -12,12 +12,14 @@ public static class Crypto private static readonly Dictionary<(string, ushort), string> _hashListPlayersSHA256 = []; private static readonly Dictionary _hashListSHA256 = new(StringComparer.Ordinal); private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new(); - + public static string GetFileHash(this string filePath) { - using SHA1 sha1 = SHA1.Create(); - using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - return Convert.ToHexString(sha1.ComputeHash(stream)); + using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var sha1 = SHA1.Create(); + + var hash = sha1.ComputeHash(stream); + return Convert.ToHexString(hash); } public static async Task GetFileHashAsync(string filePath, CancellationToken cancellationToken = default) @@ -29,7 +31,8 @@ public static class Crypto var buffer = new byte[8192]; int bytesRead; - while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) + + while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) { sha1.TransformBlock(buffer, 0, bytesRead, outputBuffer: null, 0); } -- 2.49.1 From bfaa58380848d04300c342c6f16088a21c5edd05 Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 18 Nov 2025 00:13:40 +0100 Subject: [PATCH 10/21] New hashing added instead of sha-1 --- LightlessSync/FileCache/FileCacheManager.cs | 14 +- LightlessSync/LightlessSync.csproj | 1 + LightlessSync/Services/BroadcastService.cs | 2 +- LightlessSync/Services/ContextMenuService.cs | 2 +- LightlessSync/UI/DtrEntry.cs | 2 +- LightlessSync/Utils/Crypto.cs | 186 ++++++++++++++++++- LightlessSync/packages.lock.json | 6 + 7 files changed, 196 insertions(+), 17 deletions(-) diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index c0a1bf3..d970896 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -230,11 +230,11 @@ public sealed class FileCacheManager : IHostedService brokenEntities.Add(fileCache); return; } - + var algo = Crypto.DetectAlgo(fileCache.Hash); string computedHash; try { - computedHash = await Crypto.GetFileHashAsync(fileCache.ResolvedFilepath, token).ConfigureAwait(false); + computedHash = await Crypto.ComputeFileHashAsync(fileCache.ResolvedFilepath, algo, token).ConfigureAwait(false); } catch (Exception ex) { @@ -246,8 +246,8 @@ public sealed class FileCacheManager : IHostedService if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal)) { _logger.LogInformation( - "Hash mismatch: {file} (got {computedHash}, expected {expected})", - fileCache.ResolvedFilepath, computedHash, fileCache.Hash); + "Hash mismatch: {file} (got {computedHash}, expected {expected} : hash {hash})", + fileCache.ResolvedFilepath, computedHash, fileCache.Hash, algo); brokenEntities.Add(fileCache); } @@ -422,12 +422,13 @@ public sealed class FileCacheManager : IHostedService _logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath); var oldHash = fileCache.Hash; var prefixedPath = fileCache.PrefixedFilePath; + var algo = Crypto.DetectAlgo(fileCache.ResolvedFilepath); if (computeProperties) { var fi = new FileInfo(fileCache.ResolvedFilepath); fileCache.Size = fi.Length; fileCache.CompressedSize = null; - fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); + fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, algo); fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); } RemoveHashedFile(oldHash, prefixedPath); @@ -570,7 +571,8 @@ public sealed class FileCacheManager : IHostedService private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) { - hash ??= Crypto.GetFileHash(fileInfo.FullName); + var algo = Crypto.DetectAlgo(Path.GetFileNameWithoutExtension(fileInfo.Name)); + hash ??= Crypto.ComputeFileHash(fileInfo.FullName, algo); var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length); entity = ReplacePathPrefixes(entity); AddHashedFile(entity); diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index abe4a58..d8a21ab 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -27,6 +27,7 @@ + diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index dfcd975..08ce324 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -68,7 +68,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber try { var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false); - return cid.ToString().GetHash256(); + return cid.ToString().GetBlake3Hash(); } catch (Exception ex) { diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 4d50074..6bd6ec0 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -143,7 +143,7 @@ internal class ContextMenuService : IHostedService return; } - var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetBlake3Hash(); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 680aa29..8b77f16 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -347,7 +347,7 @@ public sealed class DtrEntry : IDisposable, IHostedService try { var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult(); - var hashedCid = cid.ToString().GetHash256(); + var hashedCid = cid.ToString().GetBlake3Hash(); _localHashedCid = hashedCid; _localHashedCidFetchedAt = now; return hashedCid; diff --git a/LightlessSync/Utils/Crypto.cs b/LightlessSync/Utils/Crypto.cs index 25215c0..e4ee053 100644 --- a/LightlessSync/Utils/Crypto.cs +++ b/LightlessSync/Utils/Crypto.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using Blake3; +using System.Security.Cryptography; using System.Text; namespace LightlessSync.Utils; @@ -9,20 +10,93 @@ public static class Crypto private const int _bufferSize = 65536; #pragma warning disable SYSLIB0021 // Type or member is obsolete + // SHA256 hash caches private static readonly Dictionary<(string, ushort), string> _hashListPlayersSHA256 = []; private static readonly Dictionary _hashListSHA256 = new(StringComparer.Ordinal); private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new(); - - public static string GetFileHash(this string filePath) + + // BLAKE3 hash caches + private static readonly Dictionary<(string, ushort), string> _hashListPlayersBlake3 = []; + private static readonly Dictionary _hashListBlake3 = new(StringComparer.Ordinal); + + /// + /// Supports Blake3 or SHA1 for file transfers, no SHA256 supported on it + /// + public enum HashAlgo + { + Blake3, + Sha1 + } + + /// + /// Detects which algo is being used for the file + /// + /// Hashed string + /// HashAlgo + public static HashAlgo DetectAlgo(string hashHex) + { + if (hashHex.Length == 40) + return HashAlgo.Sha1; + + return HashAlgo.Blake3; + } + + #region File Hashing + + /// + /// Compute file hash with given algorithm, supports BLAKE3 and Sha1 for file hashing + /// + /// Filepath for the hashing + /// BLAKE3 or Sha1 + /// Hashed file hash + /// Not a valid HashAlgo or Filepath + public static string ComputeFileHash(string filePath, HashAlgo algo) + { + return algo switch + { + HashAlgo.Blake3 => ComputeFileHashBlake3(filePath), + HashAlgo.Sha1 => ComputeFileHashSha1(filePath), + _ => throw new ArgumentOutOfRangeException(nameof(algo), algo, null) + }; + } + + /// + /// Compute file hash asynchronously with given algorithm, supports BLAKE3 and SHA1 for file hashing + /// + /// Filepath for the hashing + /// BLAKE3 or Sha1 + /// Hashed file hash + /// Not a valid HashAlgo or Filepath + public static async Task ComputeFileHashAsync(string filePath, HashAlgo algo, CancellationToken cancellationToken = default) + { + return algo switch + { + HashAlgo.Blake3 => await ComputeFileHashBlake3Async(filePath, cancellationToken).ConfigureAwait(false), + HashAlgo.Sha1 => await ComputeFileHashSha1Async(filePath, cancellationToken).ConfigureAwait(false), + _ => throw new ArgumentOutOfRangeException(nameof(algo), algo, message: null) + }; + } + + /// + /// Computes an file hash with SHA1 + /// + /// Filepath that has to be computed + /// Hashed file in hex string + private static string ComputeFileHashSha1(string filePath) { using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using var sha1 = SHA1.Create(); - var hash = sha1.ComputeHash(stream); return Convert.ToHexString(hash); } - public static async Task GetFileHashAsync(string filePath, CancellationToken cancellationToken = default) + /// + /// Computes an file hash with SHA1 asynchronously + /// + /// Filepath that has to be computed + /// Cancellation token + /// Hashed file in hex string hashed in SHA1 + private static async Task ComputeFileHashSha1Async(string filePath, CancellationToken cancellationToken) { var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous); await using (stream.ConfigureAwait(false)) @@ -31,18 +105,107 @@ public static class Crypto var buffer = new byte[8192]; int bytesRead; - while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) { sha1.TransformBlock(buffer, 0, bytesRead, outputBuffer: null, 0); } sha1.TransformFinalBlock([], 0, 0); - return Convert.ToHexString(sha1.Hash!); } } + /// + /// Computes an file hash with Blake3 + /// + /// Filepath that has to be computed + /// Hashed file in hex string hashed in Blake3 + private static string ComputeFileHashBlake3(string filePath) + { + using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var hasher = Hasher.New(); + + var buffer = new byte[_bufferSize]; + int bytesRead; + while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) + { + hasher.Update(buffer.AsSpan(0, bytesRead)); + } + + var hash = hasher.Finalize(); + return hash.ToString(); + } + + + /// + /// Computes an file hash with Blake3 asynchronously + /// + /// Filepath that has to be computed + /// Hashed file in hex string hashed in Blake3 + private static async Task ComputeFileHashBlake3Async(string filePath, CancellationToken cancellationToken) + { + var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous); + await using (stream.ConfigureAwait(false)) + { + using var hasher = Hasher.New(); + + var buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) + { + hasher.Update(buffer.AsSpan(0, bytesRead)); + } + + var hash = hasher.Finalize(); + return hash.ToString(); + } + } + #endregion + + + #region String hashing + + public static string GetBlake3Hash(this (string, ushort) playerToHash) + { + if (_hashListPlayersBlake3.TryGetValue(playerToHash, out var hash)) + return hash; + + var toHash = playerToHash.Item1 + playerToHash.Item2.ToString(); + + hash = ComputeBlake3Hex(toHash); + _hashListPlayersBlake3[playerToHash] = hash; + return hash; + } + + /// + /// Computes or gets an Blake3 hash(ed) string. + /// + /// String that needs to be hashsed + /// Hashed string + public static string GetBlake3Hash(this string stringToHash) + { + return GetOrComputeBlake3(stringToHash); + } + + private static string GetOrComputeBlake3(string stringToCompute) + { + if (_hashListBlake3.TryGetValue(stringToCompute, out var hash)) + return hash; + + hash = ComputeBlake3Hex(stringToCompute); + _hashListBlake3[stringToCompute] = hash; + return hash; + } + + private static string ComputeBlake3Hex(string input) + { + var bytes = Encoding.UTF8.GetBytes(input); + + var hash = Hasher.Hash(bytes); + + return Convert.ToHexString(hash.AsSpan()); + } + public static string GetHash256(this (string, ushort) playerToHash) { if (_hashListPlayersSHA256.TryGetValue(playerToHash, out var hash)) @@ -52,6 +215,11 @@ public static class Crypto Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString()))); } + /// + /// Computes or gets an SHA256 hash(ed) string. + /// + /// String that needs to be hashsed + /// Hashed string public static string GetHash256(this string stringToHash) { return GetOrComputeHashSHA256(stringToHash); @@ -64,6 +232,8 @@ public static class Crypto return _hashListSHA256[stringToCompute] = Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))); - } + } + + #endregion #pragma warning restore SYSLIB0021 // Type or member is obsolete } \ No newline at end of file diff --git a/LightlessSync/packages.lock.json b/LightlessSync/packages.lock.json index e2bb034..c8b5c98 100644 --- a/LightlessSync/packages.lock.json +++ b/LightlessSync/packages.lock.json @@ -2,6 +2,12 @@ "version": 1, "dependencies": { "net10.0-windows7.0": { + "Blake3": { + "type": "Direct", + "requested": "[2.0.0, )", + "resolved": "2.0.0", + "contentHash": "v447kojeuNYSY5dvtVGG2bv1+M3vOWJXcrYWwXho/2uUpuwK6qPeu5WSMlqLm4VRJu96kysVO11La0zN3dLAuQ==" + }, "DalamudPackager": { "type": "Direct", "requested": "[13.1.0, )", -- 2.49.1 From a68e9e996bb14394c0d541132ade8f8a310fbaee Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 18 Nov 2025 00:38:43 +0100 Subject: [PATCH 11/21] Back to warning fixing! --- .../WebAPI/Files/FileDownloadManager.cs | 3 +- LightlessSync/WebAPI/SignalR/ApiController.cs | 60 +++++++++++++------ 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index b8f81f2..140b23c 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -294,8 +294,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase bytesRead = await readTask.ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + Logger.LogWarning(ex, "Request got cancelled : {url}", requestUrl); throw; } diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index d2fddc5..b5e64a0 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -29,7 +29,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private readonly DalamudUtilService _dalamudUtil; private readonly HubFactory _hubFactory; private readonly PairManager _pairManager; - private readonly PairRequestService _pairRequestService; private readonly ServerConfigurationManager _serverManager; private readonly TokenProvider _tokenProvider; private readonly LightlessConfigService _lightlessConfigService; @@ -44,13 +43,12 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private CensusUpdateMessage? _lastCensus; public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, - PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, + PairManager pairManager, ServerConfigurationManager serverManager, LightlessMediator mediator, TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil; _pairManager = pairManager; - _pairRequestService = pairRequestService; _serverManager = serverManager; _tokenProvider = tokenProvider; _lightlessConfigService = lightlessConfigService; @@ -133,7 +131,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogInformation("Not recreating Connection, paused"); _connectionDto = null; await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -147,7 +148,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.", NotificationType.Error)); await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -156,7 +160,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogWarning("No secret key set for current character"); _connectionDto = null; await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } } @@ -170,7 +177,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.", NotificationType.Error)); await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -179,7 +189,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogWarning("No UID/OAuth set for current character"); _connectionDto = null; await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -188,7 +201,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogWarning("OAuth2 login token could not be updated"); _connectionDto = null; await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } } @@ -199,7 +215,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational, $"Starting Connection to {_serverManager.CurrentServer.ServerName}"))); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } _connectionCancellationTokenSource?.Dispose(); _connectionCancellationTokenSource = new CancellationTokenSource(); var token = _connectionCancellationTokenSource.Token; @@ -598,7 +617,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL $"Stopping existing connection to {_serverManager.CurrentServer.ServerName}"))); _initialized = false; - _healthCheckTokenSource?.Cancel(); + if (_healthCheckTokenSource != null) + { + await _healthCheckTokenSource.CancelAsync().ConfigureAwait(false); + } Mediator.Publish(new DisconnectedMessage()); _lightlessHub = null; _connectionDto = null; @@ -609,42 +631,42 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL public Task UserGetLightfinderProfile(string hashedCid) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task UpdateChatPresence(ChatPresenceUpdateDto presence) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task Client_ChatReceive(ChatMessageDto message) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task> GetZoneChatChannels() { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task> GetGroupChatChannels() { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task SendChatMessage(ChatSendRequestDto request) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task ReportChatMessage(ChatReportSubmitDto request) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public Task ResolveChatParticipant(ChatParticipantResolveRequestDto request) { - throw new NotImplementedException(); + throw new NotSupportedException(); } } #pragma warning restore MA0040 \ No newline at end of file -- 2.49.1 From 6341a663f2af09094e234f308b5a7edcfd89b326 Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 18 Nov 2025 04:00:53 +0100 Subject: [PATCH 12/21] Refactored some functions --- LightlessSync/FileCache/FileCacheManager.cs | 139 +++++++++++++--- LightlessSync/Services/BroadcastService.cs | 2 - LightlessSync/Services/CharacterAnalyzer.cs | 63 +++++--- LightlessSync/Services/DalamudUtilService.cs | 162 +++++++++++-------- 4 files changed, 252 insertions(+), 114 deletions(-) diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index d970896..1b1471c 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -18,6 +18,7 @@ public sealed class FileCacheManager : IHostedService public const string PenumbraPrefix = "{penumbra}"; private const int FileCacheVersion = 1; private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:"; + private readonly SemaphoreSlim _fileWriteSemaphore = new(1, 1); private readonly LightlessConfigService _configService; private readonly LightlessMediator _lightlessMediator; private readonly string _csvPath; @@ -169,27 +170,53 @@ public sealed class FileCacheManager : IHostedService return CreateFileCacheEntity(fi, prefixedPath); } - public List GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList(); + public List GetAllFileCaches() => [.. _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null))]; public List GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true) { - List output = []; - if (_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + var output = new List(); + + if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + return output; + + foreach (var fileCache in fileCacheEntities.Values + .Where(c => !ignoreCacheEntries || !c.IsCacheEntry)) { - foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList()) + if (!validate) { - if (!validate) - { - output.Add(fileCache); - } - else - { - var validated = GetValidatedFileCache(fileCache); - if (validated != null) - { - output.Add(validated); - } - } + output.Add(fileCache); + continue; + } + + var validated = GetValidatedFileCache(fileCache); + if (validated != null) + output.Add(validated); + } + + return output; + } + + public async Task> GetAllFileCachesByHashAsync(string hash, bool ignoreCacheEntries = false, bool validate = true,CancellationToken token = default) + { + var output = new List(); + + if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + return output; + + foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry)) + { + token.ThrowIfCancellationRequested(); + + if (!validate) + { + output.Add(fileCache); + } + else + { + var validated = await GetValidatedFileCacheAsync(fileCache, token).ConfigureAwait(false); + + if (validated != null) + output.Add(validated); } } @@ -479,6 +506,44 @@ public sealed class FileCacheManager : IHostedService } } + public async Task WriteOutFullCsvAsync(CancellationToken cancellationToken = default) + { + await _fileWriteSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var sb = new StringBuilder(); + sb.AppendLine(BuildVersionHeader()); + + foreach (var entry in _fileCaches.Values + .SelectMany(k => k.Values) + .OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase)) + { + sb.AppendLine(entry.CsvEntry); + } + + if (File.Exists(_csvPath)) + { + File.Copy(_csvPath, CsvBakPath, overwrite: true); + } + + try + { + await File.WriteAllTextAsync(_csvPath, sb.ToString(), cancellationToken).ConfigureAwait(false); + + File.Delete(CsvBakPath); + } + catch + { + await File.WriteAllTextAsync(CsvBakPath, sb.ToString(), cancellationToken).ConfigureAwait(false); + } + } + finally + { + _fileWriteSemaphore.Release(); + } + } + private void EnsureCsvHeaderLocked() { if (!File.Exists(_csvPath)) @@ -601,6 +666,13 @@ public sealed class FileCacheManager : IHostedService return resultingFileCache; } + private async Task GetValidatedFileCacheAsync(FileCacheEntity fileCache, CancellationToken token = default) + { + var resultingFileCache = ReplacePathPrefixes(fileCache); + resultingFileCache = await ValidateAsync(resultingFileCache, token).ConfigureAwait(false); + return resultingFileCache; + } + private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache) { if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase)) @@ -623,6 +695,7 @@ public sealed class FileCacheManager : IHostedService RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); return null; } + var file = new FileInfo(fileCache.ResolvedFilepath); if (!file.Exists) { @@ -630,7 +703,8 @@ public sealed class FileCacheManager : IHostedService return null; } - if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) + var lastWriteTicks = file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); + if (!string.Equals(lastWriteTicks, fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) { UpdateHashedFile(fileCache); } @@ -638,6 +712,33 @@ public sealed class FileCacheManager : IHostedService return fileCache; } + private async Task ValidateAsync(FileCacheEntity fileCache, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath)) + { + _logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath); + RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + return null; + } + + return await Task.Run(() => + { + var file = new FileInfo(fileCache.ResolvedFilepath); + if (!file.Exists) + { + RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + return null; + } + + if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) + { + UpdateHashedFile(fileCache); + } + + return fileCache; + }, token).ConfigureAwait(false); + } + public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting FileCacheManager"); @@ -811,7 +912,7 @@ public sealed class FileCacheManager : IHostedService if (rewriteRequired) { - WriteOutFullCsv(); + await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false); } } @@ -822,7 +923,7 @@ public sealed class FileCacheManager : IHostedService public async Task StopAsync(CancellationToken cancellationToken) { - WriteOutFullCsv(); + await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false); await Task.CompletedTask.ConfigureAwait(false); } } \ No newline at end of file diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index 08ce324..ff59716 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -396,8 +396,6 @@ public class BroadcastService : IHostedService, IMediatorSubscriber return result; } - - public async void ToggleBroadcast() { diff --git a/LightlessSync/Services/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index 8b87c99..379af5a 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -54,34 +54,47 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable var cancelToken = _analysisCts.Token; var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList(); - if (allFiles.Exists(c => !c.IsComputed || recalculate)) + + var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList(); + + if (remaining.Count == 0) + return; + + TotalFiles = remaining.Count; + CurrentFile = 0; + + Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count); + + Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer))); + + try { - var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList(); - TotalFiles = remaining.Count; - CurrentFile = 1; - Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count); - - Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer))); - try + foreach (var file in remaining) { - foreach (var file in remaining) - { - Logger.LogDebug("Computing file {file}", file.FilePaths[0]); - await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); - CurrentFile++; - } + cancelToken.ThrowIfCancellationRequested(); - _fileCacheManager.WriteOutFullCsv(); + var path = file.FilePaths.FirstOrDefault() ?? ""; + Logger.LogDebug("Computing file {file}", path); + await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); + + CurrentFile++; } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to analyze files"); - } - finally - { - Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer))); - } + + await _fileCacheManager.WriteOutFullCsvAsync(cancelToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.LogInformation("File analysis cancelled"); + throw; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to analyze files"); + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer))); } RecalculateSummary(); @@ -113,7 +126,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { token.ThrowIfCancellationRequested(); - var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: true, validate: false).ToList(); + 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; @@ -230,7 +243,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); var normalSize = new FileInfo(FilePaths[0]).Length; - var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: true, validate: false); + var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false); foreach (var entry in entries) { entry.Size = normalSize; diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index f476076..e418f92 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -675,76 +675,75 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _lastGlobalBlockReason = string.Empty; } - if (_clientState.IsGPosing && !IsInGpose) - { - _logger.LogDebug("Gpose start"); - IsInGpose = true; - Mediator.Publish(new GposeStartMessage()); - } - else if (!_clientState.IsGPosing && IsInGpose) - { - _logger.LogDebug("Gpose end"); - IsInGpose = false; - Mediator.Publish(new GposeEndMessage()); - } + // Checks on conditions + var shouldBeInGpose = _clientState.IsGPosing; + var shouldBeInCombat = _condition[ConditionFlag.InCombat] && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat; + var shouldBePerforming = _condition[ConditionFlag.Performing] && _playerPerformanceConfigService.Current.PauseWhilePerforming; + var shouldBeInInstance = _condition[ConditionFlag.BoundByDuty] && _playerPerformanceConfigService.Current.PauseInInstanceDuty; + var shouldBeInCutscene = _condition[ConditionFlag.WatchingCutscene]; - if ((_condition[ConditionFlag.InCombat]) && !IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat) - { - _logger.LogDebug("Combat start"); - IsInCombat = true; - Mediator.Publish(new CombatStartMessage()); - Mediator.Publish(new HaltScanMessage(nameof(IsInCombat))); - } - else if ((!_condition[ConditionFlag.InCombat]) && IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat) - { - _logger.LogDebug("Combat end"); - IsInCombat = false; - Mediator.Publish(new CombatEndMessage()); - Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat))); - } - if (_condition[ConditionFlag.Performing] && !IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming) - { - _logger.LogDebug("Performance start"); - IsInCombat = true; - Mediator.Publish(new PerformanceStartMessage()); - Mediator.Publish(new HaltScanMessage(nameof(IsPerforming))); - } - else if (!_condition[ConditionFlag.Performing] && IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming) - { - _logger.LogDebug("Performance end"); - IsInCombat = false; - Mediator.Publish(new PerformanceEndMessage()); - Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming))); - } - if ((_condition[ConditionFlag.BoundByDuty]) && !IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty) - { - _logger.LogDebug("Instance start"); - IsInInstance = true; - Mediator.Publish(new InstanceOrDutyStartMessage()); - Mediator.Publish(new HaltScanMessage(nameof(IsInInstance))); - } - else if (((!_condition[ConditionFlag.BoundByDuty]) && IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty) || ((_condition[ConditionFlag.BoundByDuty]) && IsInInstance && !_playerPerformanceConfigService.Current.PauseInInstanceDuty)) - { - _logger.LogDebug("Instance end"); - IsInInstance = false; - Mediator.Publish(new InstanceOrDutyEndMessage()); - Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance))); - } + // Gpose + HandleStateTransition(() => IsInGpose, v => IsInGpose = v, shouldBeInGpose, "Gpose", + onEnter: () => + { + Mediator.Publish(new GposeStartMessage()); + }, + onExit: () => + { + Mediator.Publish(new GposeEndMessage()); + }); - if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene) - { - _logger.LogDebug("Cutscene start"); - IsInCutscene = true; - Mediator.Publish(new CutsceneStartMessage()); - Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene))); - } - else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene) - { - _logger.LogDebug("Cutscene end"); - IsInCutscene = false; - Mediator.Publish(new CutsceneEndMessage()); - Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene))); - } + // Combat + HandleStateTransition(() => IsInCombat, v => IsInCombat = v, shouldBeInCombat, "Combat", + onEnter: () => + { + Mediator.Publish(new CombatStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCombat))); + }, + onExit: () => + { + Mediator.Publish(new CombatEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat))); + }); + + // Performance + HandleStateTransition(() => IsPerforming, v => IsPerforming = v, shouldBePerforming, "Performance", + onEnter: () => + { + Mediator.Publish(new PerformanceStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsPerforming))); + }, + onExit: () => + { + Mediator.Publish(new PerformanceEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming))); + }); + + // Instance / Duty + HandleStateTransition(() => IsInInstance, v => IsInInstance = v, shouldBeInInstance, "Instance", + onEnter: () => + { + Mediator.Publish(new InstanceOrDutyStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInInstance))); + }, + onExit: () => + { + Mediator.Publish(new InstanceOrDutyEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance))); + }); + + // Cutscene + HandleStateTransition(() => IsInCutscene,v => IsInCutscene = v, shouldBeInCutscene, "Cutscene", + onEnter: () => + { + Mediator.Publish(new CutsceneStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene))); + }, + onExit: () => + { + Mediator.Publish(new CutsceneEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene))); + }); if (IsInCutscene) { @@ -821,4 +820,31 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _delayedFrameworkUpdateCheck = DateTime.UtcNow; }); } + + /// + /// Handler for the transition of different states of game + /// + /// Get state of condition + /// Set state of condition + /// Correction of the state of the condition + /// Condition name + /// Function for on entering the state + /// Function for on leaving the state + private void HandleStateTransition(Func getState, Action setState, bool shouldBeActive, string stateName, System.Action onEnter, System.Action onExit) + { + var isActive = getState(); + + if (shouldBeActive && !isActive) + { + _logger.LogDebug("{stateName} start", stateName); + setState(true); + onEnter(); + } + else if (!shouldBeActive && isActive) + { + _logger.LogDebug("{stateName} end", stateName); + setState(false); + onExit(); + } + } } \ No newline at end of file -- 2.49.1 From eaf1259fd5665663947406898bddc79e47bc28ff Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 21 Nov 2025 04:00:20 +0100 Subject: [PATCH 13/21] Added new shellfinder ui --- LightlessSync/Services/BroadcastService.cs | 2 +- LightlessSync/Services/XivDataAnalyzer.cs | 13 +- LightlessSync/UI/SyncshellFinderUI.cs | 460 ++++++++++++++++----- 3 files changed, 364 insertions(+), 111 deletions(-) diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index ff59716..bc32c9c 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -68,7 +68,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber try { var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false); - return cid.ToString().GetBlake3Hash(); + return cid.ToString().GetHash256(); } catch (Exception ex) { diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index db721a2..9d32883 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -46,7 +46,7 @@ public sealed class XivDataAnalyzer if (handle->FileName.Length > 1024) continue; var skeletonName = handle->FileName.ToString(); if (string.IsNullOrEmpty(skeletonName)) continue; - outputIndices[skeletonName] = new(); + outputIndices[skeletonName] = []; for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++) { var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String; @@ -70,7 +70,7 @@ public sealed class XivDataAnalyzer var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash); if (cacheEntity == null) return null; - using BinaryReader reader = new BinaryReader(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); + using BinaryReader reader = new(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); // most of this shit is from vfxeditor, surely nothing will change in the pap format :copium: reader.ReadInt32(); // ignore @@ -177,17 +177,18 @@ public sealed class XivDataAnalyzer } long tris = 0; - for (int i = 0; i < file.LodCount; i++) + foreach (var lod in file.Lods) { try { - var meshIdx = file.Lods[i].MeshIndex; - var meshCnt = file.Lods[i].MeshCount; + var meshIdx = lod.MeshIndex; + var meshCnt = lod.MeshCount; + tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; } catch (Exception ex) { - _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", i, filePath); + _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", lod.MeshIndex, filePath); continue; } diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 3181513..7034bca 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -3,6 +3,7 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; @@ -29,11 +30,15 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private readonly List _nearbySyncshells = []; private List _currentSyncshells = []; private int _selectedNearbyIndex = -1; + private int _syncshellPageIndex = 0; private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; private DefaultPermissionsDto _ownPermissions = null!; + private const bool UseTestSyncshells = true; + + private bool _compactView = false; public SyncshellFinderUI( ILogger logger, @@ -72,9 +77,21 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase protected override void DrawInternal() { + ImGui.BeginGroup(); _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); - UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + ImGui.SameLine(); + string checkboxLabel = "Compact view"; + float availWidth = ImGui.GetContentRegionAvail().X; + float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); + + float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth; + ImGui.SetCursorPosX(rightX); + ImGui.Checkbox(checkboxLabel, ref _compactView); + ImGui.EndGroup(); + + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); if (_nearbySyncshells.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); @@ -104,106 +121,299 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } - DrawSyncshellTable(); + // Build card data (same as you had) + var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); + var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); + + foreach (var shell in _nearbySyncshells) + { + string broadcasterName; + + if (UseTestSyncshells) + { + // Fake broadcaster for test mode + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) + ? shell.Group.Alias + : shell.Group.GID; + + broadcasterName = $"Tester of {displayName}"; + } + else + { + var broadcast = broadcasts + .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); + + if (broadcast == null) + continue; + + var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); + if (string.IsNullOrEmpty(name)) + continue; + + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); + broadcasterName = !string.IsNullOrEmpty(worldName) + ? $"{name} ({worldName})" + : name; + } + + cardData.Add((shell, broadcasterName)); + } + + if (cardData.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); + return; + } + + if (_compactView) + { + DrawSyncshellGrid(cardData); + } + else + { + DrawSyncshellList(cardData); + } + if (_joinDto != null && _joinInfo != null && _joinInfo.Success) DrawConfirmation(); } - private void DrawSyncshellTable() + private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData) { - if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) + const int shellsPerPage = 3; + var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage); + if (totalPages <= 0) + totalPages = 1; + + _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); + + var firstIndex = _syncshellPageIndex * shellsPerPage; + var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count); + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); + + for (int index = firstIndex; index < lastExclusive; index++) { - ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); - ImGui.TableHeadersRow(); + var (shell, broadcasterName) = listData[index]; - foreach (var shell in _nearbySyncshells) - { - // Check if there is an active broadcast for this syncshell, if not, skipping this syncshell - var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts() - .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); + ImGui.PushID(shell.Group.GID); + float rowHeight = 90f * ImGuiHelpers.GlobalScale; - if (broadcast == null) - continue; // no active broadcasts + ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); - var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); - if (string.IsNullOrEmpty(Name)) - continue; // broadcaster not found in area, skipping + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); + var style = ImGui.GetStyle(); + float startX = ImGui.GetCursorPosX(); + float regionW = ImGui.GetContentRegionAvail().X; + float rightTxtW = ImGui.CalcTextSize(broadcasterName).X; - var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - ImGui.TextUnformatted(displayName); + _uiSharedService.MediumText(displayName, UIColors.Get("PairBlue")); - ImGui.TableNextColumn(); - var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address); - var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name; - ImGui.TextUnformatted(broadcasterName); + float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; + ImGui.SameLine(); + ImGui.SetCursorPosX(rightX); + ImGui.TextUnformatted(broadcasterName); - ImGui.TableNextColumn(); + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); - var label = $"Join##{shell.Group.GID}"; - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); + ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); - var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); - var isRecentlyJoined = _recentlyJoined.Contains(shell.GID); - - if (!isAlreadyMember && !isRecentlyJoined) - { - if (ImGui.Button(label)) - { - _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); + DrawJoinButton(shell); - _ = Task.Run(async () => - { - try - { - var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( - shell.Group, - shell.Password, - shell.GroupUserPreferredPermissions - )).ConfigureAwait(false); + ImGui.EndChild(); + ImGui.PopID(); - if (info != null && info.Success) - { - _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); - _joinInfo = info; - _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + } - _logger.LogInformation($"Fetched join info for {shell.Group.GID}"); - } - else - { - _logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, $"Join failed for {shell.Group.GID}"); - } - }); - } - } - else - { - using (ImRaii.Disabled()) - { - ImGui.Button(label); - } - UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); - } - ImGui.PopStyleColor(3); - } + ImGui.PopStyleVar(2); - ImGui.EndTable(); + DrawPagination(totalPages); + } + + private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData) + { + const int shellsPerPage = 4; + var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage); + if (totalPages <= 0) + totalPages = 1; + + _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); + + var firstIndex = _syncshellPageIndex * shellsPerPage; + var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count); + + var avail = ImGui.GetContentRegionAvail(); + var spacing = ImGui.GetStyle().ItemSpacing; + + var cardWidth = (avail.X - spacing.X) / 2.0f; + var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f; + cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight); + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); + + for (int index = firstIndex; index < lastExclusive; index++) + { + var localIndex = index - firstIndex; + var (shell, broadcasterName) = cardData[index]; + + if (localIndex % 2 != 0) + ImGui.SameLine(); + + ImGui.PushID(shell.Group.GID); + + ImGui.BeginGroup(); + _ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, + new Vector2(cardWidth, cardHeight), + true); + + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) + ? shell.Group.Alias + : shell.Group.GID; + + _uiSharedService.MediumText(displayName + "(200/250)", UIColors.Get("PairBlue")); + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + + ImGui.TextColored(ImGuiColors.DalamudGrey, "Broadcaster"); + ImGui.TextUnformatted(broadcasterName); + + ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); + + var buttonHeight = ImGui.GetFrameHeightWithSpacing(); + var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight; + if (remainingY > 0) + ImGui.Dummy(new Vector2(0, remainingY)); + + DrawJoinButton(shell); + + ImGui.EndChild(); + ImGui.EndGroup(); + + ImGui.PopID(); + } + + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + ImGui.PopStyleVar(2); + + DrawPagination(totalPages); + } + + private void DrawPagination(int totalPages) + { + if (totalPages > 1) + { + UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + + var style = ImGui.GetStyle(); + string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}"; + + float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2; + float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2; + float textWidth = ImGui.CalcTextSize(pageLabel).X; + + float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2; + + float availWidth = ImGui.GetContentRegionAvail().X; + float offsetX = (availWidth - totalWidth) * 0.5f; + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX); + + if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0) + _syncshellPageIndex--; + + ImGui.SameLine(); + ImGui.Text(pageLabel); + + ImGui.SameLine(); + if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1) + _syncshellPageIndex++; } } + private void DrawJoinButton(dynamic shell) + { + const string visibleLabel = "Join"; + var label = $"{visibleLabel}##{shell.Group.GID}"; + + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); + + var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); + var isRecentlyJoined = _recentlyJoined.Contains(shell.GID); + + Vector2 buttonSize; + + if (!_compactView) + { + var style = ImGui.GetStyle(); + var textSize = ImGui.CalcTextSize(visibleLabel); + + var width = textSize.X + style.FramePadding.X * 20f; + buttonSize = new Vector2(width, 0); + + float availX = ImGui.GetContentRegionAvail().X; + float curX = ImGui.GetCursorPosX(); + float newX = curX + (availX - buttonSize.X); + ImGui.SetCursorPosX(newX); + } + else + { + buttonSize = new Vector2(-1, 0); + } + + if (!isAlreadyMember && !isRecentlyJoined) + { + if (ImGui.Button(label, buttonSize)) + { + _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); + + _ = Task.Run(async () => + { + try + { + var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( + shell.Group, + shell.Password, + shell.GroupUserPreferredPermissions + )).ConfigureAwait(false); + + if (info != null && info.Success) + { + _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); + _joinInfo = info; + _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + + _logger.LogInformation($"Fetched join info for {shell.Group.GID}"); + } + else + { + _logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Join failed for {shell.Group.GID}"); + } + }); + } + } + else + { + using (ImRaii.Disabled()) + { + ImGui.Button(label, buttonSize); + } + + UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); + } + + ImGui.PopStyleColor(3); + } private void DrawConfirmation() { if (_joinDto != null && _joinInfo != null) @@ -267,48 +477,89 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; - - _recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); - if (syncshellBroadcasts.Count == 0) + _recentlyJoined.RemoveWhere(gid => + _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); + + List? updatedList = []; + + if (UseTestSyncshells) + { + // ---- TEST DATA PATH ---- + updatedList = BuildTestSyncshells(); + } + else + { + // ---- NORMAL BEHAVIOUR ---- + if (syncshellBroadcasts.Count == 0) + { + ClearSyncshells(); + return; + } + + try + { + var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts) + .ConfigureAwait(false); + updatedList = groups?.ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); + return; + } + } + + if (updatedList == null || updatedList.Count == 0) { ClearSyncshells(); return; } - List? updatedList = []; - try - { - var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); - updatedList = groups?.ToList(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); - return; - } + var previousGid = GetSelectedGid(); - if (updatedList != null) + _nearbySyncshells.Clear(); + _nearbySyncshells.AddRange(updatedList); + + if (previousGid != null) { - var previousGid = GetSelectedGid(); + var newIndex = _nearbySyncshells.FindIndex(s => + string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - _nearbySyncshells.Clear(); - _nearbySyncshells.AddRange(updatedList); - - if (previousGid != null) + if (newIndex >= 0) { - var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - if (newIndex >= 0) - { - _selectedNearbyIndex = newIndex; - return; - } + _selectedNearbyIndex = newIndex; + return; } } ClearSelection(); } + private List BuildTestSyncshells() + { + var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell"); + var testGroup2 = new GroupData("TEST-BETA", "Beta Shell"); + var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell"); + var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell"); + var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell"); + var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell"); + var testGroup7 = new GroupData("TEST-POINT", "Point Shell"); + var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell"); + + return + [ + new(testGroup1, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup2, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup3, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup4, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup5, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup6, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup7, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup8, "", GroupUserPreferredPermissions.NoneSet), + ]; + } + private void ClearSyncshells() { if (_nearbySyncshells.Count == 0) @@ -321,6 +572,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private void ClearSelection() { _selectedNearbyIndex = -1; + _syncshellPageIndex = 0; _joinDto = null; _joinInfo = null; } -- 2.49.1 From d13c57345c89aaacea4b34eda61e1a04cef14533 Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 21 Nov 2025 04:09:03 +0100 Subject: [PATCH 14/21] Disabled test data --- LightlessSync/UI/SyncshellFinderUI.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 7034bca..0df5470 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -36,7 +36,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; private DefaultPermissionsDto _ownPermissions = null!; - private const bool UseTestSyncshells = true; + private const bool _useTestSyncshells = false; private bool _compactView = false; @@ -129,7 +129,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase { string broadcasterName; - if (UseTestSyncshells) + if (_useTestSyncshells) { // Fake broadcaster for test mode var displayName = !string.IsNullOrEmpty(shell.Group.Alias) @@ -483,7 +483,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase List? updatedList = []; - if (UseTestSyncshells) + if (_useTestSyncshells) { // ---- TEST DATA PATH ---- updatedList = BuildTestSyncshells(); -- 2.49.1 From 6dfac79f5a4feac0c587867bf606ae5e2fe18d1d Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 21 Nov 2025 04:11:39 +0100 Subject: [PATCH 15/21] removed useless addon on display name --- LightlessSync/UI/SyncshellFinderUI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 0df5470..f0076d6 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -275,7 +275,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ? shell.Group.Alias : shell.Group.GID; - _uiSharedService.MediumText(displayName + "(200/250)", UIColors.Get("PairBlue")); + _uiSharedService.MediumText(displayName, UIColors.Get("PairBlue")); UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); ImGui.TextColored(ImGuiColors.DalamudGrey, "Broadcaster"); -- 2.49.1 From b45c1a3ababa29bf55d753b8fca4b5d3ece1640d Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 21 Nov 2025 04:32:53 +0100 Subject: [PATCH 16/21] Added refresh on leave of syncshell. --- LightlessSync/Services/Mediator/Messages.cs | 1 + LightlessSync/UI/Components/DrawFolderGroup.cs | 1 + LightlessSync/UI/SyncshellFinderUI.cs | 15 +++++++++------ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 79434c2..343075c 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -107,6 +107,7 @@ public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase; public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; +public record UserLeftSyncshellReloadFinderUi(bool hasLeft, string gid) : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase; public record PairRequestsUpdatedMessage : MessageBase; diff --git a/LightlessSync/UI/Components/DrawFolderGroup.cs b/LightlessSync/UI/Components/DrawFolderGroup.cs index 6de9e28..ac146eb 100644 --- a/LightlessSync/UI/Components/DrawFolderGroup.cs +++ b/LightlessSync/UI/Components/DrawFolderGroup.cs @@ -111,6 +111,7 @@ public class DrawFolderGroup : DrawFolderBase if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed()) { _ = _apiController.GroupLeave(_groupFullInfoDto); + _lightlessMediator.Publish(new UserLeftSyncshellReloadFinderUi(hasLeft: true, _groupFullInfoDto.GID)); ImGui.CloseCurrentPopup(); } UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal) diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index f0076d6..70e1bf8 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -14,6 +14,7 @@ using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; +using System.Collections.Specialized; using System.Numerics; namespace LightlessSync.UI; @@ -67,6 +68,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); } public override async void OnOpen() @@ -121,7 +123,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } - // Build card data (same as you had) var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); @@ -131,7 +132,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase if (_useTestSyncshells) { - // Fake broadcaster for test mode var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; @@ -473,24 +473,22 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.NewLine(); } - private async Task RefreshSyncshellsAsync() + private async Task RefreshSyncshellsAsync(string gid = null) { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; _recentlyJoined.RemoveWhere(gid => - _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); + _currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); List? updatedList = []; if (_useTestSyncshells) { - // ---- TEST DATA PATH ---- updatedList = BuildTestSyncshells(); } else { - // ---- NORMAL BEHAVIOUR ---- if (syncshellBroadcasts.Count == 0) { ClearSyncshells(); @@ -516,6 +514,11 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } + if (gid != null && _recentlyJoined.Contains(gid)) + { + _recentlyJoined.Remove(gid); + } + var previousGid = GetSelectedGid(); _nearbySyncshells.Clear(); -- 2.49.1 From 1586a1d7ccaa93452d8d7e726006179652f5166e Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 21 Nov 2025 14:23:44 +0100 Subject: [PATCH 17/21] Making button red when already joined syncshell --- LightlessSync/UI/SyncshellFinderUI.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 70e1bf8..2187a3f 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -87,7 +87,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase float availWidth = ImGui.GetContentRegionAvail().X; float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); - float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth; + float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 2.0f; ImGui.SetCursorPosX(rightX); ImGui.Checkbox(checkboxLabel, ref _compactView); ImGui.EndGroup(); @@ -404,9 +404,12 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase } else { - using (ImRaii.Disabled()) + using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) { - ImGui.Button(label, buttonSize); + using (ImRaii.Disabled()) + { + ImGui.Button(label, buttonSize); + } } UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); @@ -473,7 +476,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.NewLine(); } - private async Task RefreshSyncshellsAsync(string gid = null) + private async Task RefreshSyncshellsAsync(string? gid = null) { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; @@ -516,7 +519,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase if (gid != null && _recentlyJoined.Contains(gid)) { - _recentlyJoined.Remove(gid); + _recentlyJoined.Clear(); } var previousGid = GetSelectedGid(); -- 2.49.1 From cae7cda187018a8ec44b40a41a8399071702b5e7 Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 21 Nov 2025 14:33:28 +0100 Subject: [PATCH 18/21] Added join on mediator for refresh on finder ui --- LightlessSync/Services/Mediator/Messages.cs | 3 ++- LightlessSync/UI/Components/DrawFolderGroup.cs | 2 +- LightlessSync/UI/JoinSyncshellUI.cs | 1 + LightlessSync/UI/SyncshellFinderUI.cs | 10 ++++------ 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 343075c..7f4698d 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -107,7 +107,8 @@ public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase; public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; -public record UserLeftSyncshellReloadFinderUi(bool hasLeft, string gid) : MessageBase; +public record UserLeftSyncshell(string gid) : MessageBase; +public record UserJoinedSyncshell(string gid) : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase; public record PairRequestsUpdatedMessage : MessageBase; diff --git a/LightlessSync/UI/Components/DrawFolderGroup.cs b/LightlessSync/UI/Components/DrawFolderGroup.cs index ac146eb..4135d54 100644 --- a/LightlessSync/UI/Components/DrawFolderGroup.cs +++ b/LightlessSync/UI/Components/DrawFolderGroup.cs @@ -111,7 +111,7 @@ public class DrawFolderGroup : DrawFolderBase if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed()) { _ = _apiController.GroupLeave(_groupFullInfoDto); - _lightlessMediator.Publish(new UserLeftSyncshellReloadFinderUi(hasLeft: true, _groupFullInfoDto.GID)); + _lightlessMediator.Publish(new UserLeftSyncshell(_groupFullInfoDto.GID)); ImGui.CloseCurrentPopup(); } UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal) diff --git a/LightlessSync/UI/JoinSyncshellUI.cs b/LightlessSync/UI/JoinSyncshellUI.cs index ffa4e23..989fa07 100644 --- a/LightlessSync/UI/JoinSyncshellUI.cs +++ b/LightlessSync/UI/JoinSyncshellUI.cs @@ -173,6 +173,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase joinPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); joinPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_groupJoinInfo.Group, _previousPassword, joinPermissions)); + Mediator.Publish(new UserJoinedSyncshell(_groupJoinInfo.Group.GID)); IsOpen = false; } } diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 2187a3f..b26644f 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -68,7 +68,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); - Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); } public override async void OnOpen() @@ -404,12 +405,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase } else { - using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) + using (ImRaii.Disabled()) { - using (ImRaii.Disabled()) - { - ImGui.Button(label, buttonSize); - } + ImGui.Button(label, buttonSize); } UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); -- 2.49.1 From c94c785e0b4d4ed715da58f42e3f00ee7755bf67 Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 21 Nov 2025 14:39:43 +0100 Subject: [PATCH 19/21] Changed colors to be used in our colorway. moved buttons a bit around --- LightlessSync/UI/SyncshellFinderUI.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index b26644f..f0c934c 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -81,19 +81,19 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase protected override void DrawInternal() { ImGui.BeginGroup(); - _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); + _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple")); ImGui.SameLine(); string checkboxLabel = "Compact view"; float availWidth = ImGui.GetContentRegionAvail().X; float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); - float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 2.0f; + float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f; ImGui.SetCursorPosX(rightX); ImGui.Checkbox(checkboxLabel, ref _compactView); ImGui.EndGroup(); - UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); if (_nearbySyncshells.Count == 0) { @@ -108,7 +108,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGuiHelpers.ScaledDummy(0.5f); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue")); + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple")); if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) { @@ -211,14 +211,14 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase float regionW = ImGui.GetContentRegionAvail().X; float rightTxtW = ImGui.CalcTextSize(broadcasterName).X; - _uiSharedService.MediumText(displayName, UIColors.Get("PairBlue")); + _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; ImGui.SameLine(); ImGui.SetCursorPosX(rightX); ImGui.TextUnformatted(broadcasterName); - UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); @@ -268,16 +268,14 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.PushID(shell.Group.GID); ImGui.BeginGroup(); - _ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, - new Vector2(cardWidth, cardHeight), - true); + _ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true); var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - _uiSharedService.MediumText(displayName, UIColors.Get("PairBlue")); - UiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); ImGui.TextColored(ImGuiColors.DalamudGrey, "Broadcaster"); ImGui.TextUnformatted(broadcasterName); -- 2.49.1 From 0455a23c12eeb7230b568ea3f89dda958dbaae19 Mon Sep 17 00:00:00 2001 From: cake Date: Sat, 29 Nov 2025 17:51:31 +0100 Subject: [PATCH 20/21] Fixed migration for 2.0.0 --- LightlessSync/FileCache/CacheMonitor.cs | 5 +- LightlessSync/FileCache/FileCompactor.cs | 9 +- LightlessSync/Plugin.cs | 4 +- .../Services/BroadcastScannerService.cs | 21 +- LightlessSync/Services/NameplateHandler.cs | 1032 ++++++++--------- LightlessSync/Services/UiFactory.cs | 6 +- LightlessSync/UI/BroadcastUI.cs | 1 + LightlessSync/UI/DrawEntityFactory.cs | 4 + LightlessSync/UI/SettingsUi.cs | 6 +- LightlessSync/UI/SyncshellAdminUI.cs | 263 +---- LightlessSync/UI/SyncshellFinderUI.cs | 3 +- LightlessSync/UI/UISharedService.cs | 2 +- 12 files changed, 597 insertions(+), 759 deletions(-) diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index 8b90013..d32b40d 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -259,6 +259,9 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private CancellationTokenSource _penumbraFswCts = new(); private CancellationTokenSource _lightlessFswCts = new(); + private long totalSize; + private long maxCacheBytes; + public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; } @@ -485,7 +488,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { long size = 0; - if (!isWine) + if (!_dalamudUtil.IsWine) { try { diff --git a/LightlessSync/FileCache/FileCompactor.cs b/LightlessSync/FileCache/FileCompactor.cs index 971aa15..771f558 100644 --- a/LightlessSync/FileCache/FileCompactor.cs +++ b/LightlessSync/FileCache/FileCompactor.cs @@ -17,7 +17,6 @@ public sealed partial class FileCompactor : IDisposable public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const ulong WOF_PROVIDER_FILE = 2UL; public const int _maxRetries = 3; - private readonly bool _isWindows; private readonly ConcurrentDictionary _pendingCompactions; private readonly ILogger _logger; @@ -272,9 +271,6 @@ public sealed partial class FileCompactor : IDisposable ? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000) : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim()); - return (flowControl: false, value: fileInfo.Length); } catch (Exception ex) @@ -1160,6 +1156,11 @@ public sealed partial class FileCompactor : IDisposable } return true; } + catch (Exception ex) + { + _logger.LogTrace(ex, "Probe open failed for {file} (linux={linux})", winePath, linuxPath); + return false; + } } /// diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 5136a6e..6e68f77 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -288,7 +288,7 @@ public sealed class Plugin : IDalamudPlugin clientState, sp.GetRequiredService())); collection.AddSingleton(); - collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService>(), framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddSingleton(s => new BroadcastScannerService(s.GetRequiredService>(), framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); // add scoped services @@ -342,7 +342,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService())); collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), clientState, gameGui, objectTable, gameInteropProvider, s.GetRequiredService(),s.GetRequiredService())); - collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), + collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), s.GetRequiredService(), clientState, s.GetRequiredService())); collection.AddHostedService(p => p.GetRequiredService()); diff --git a/LightlessSync/Services/BroadcastScannerService.cs b/LightlessSync/Services/BroadcastScannerService.cs index b6e82e1..96576d1 100644 --- a/LightlessSync/Services/BroadcastScannerService.cs +++ b/LightlessSync/Services/BroadcastScannerService.cs @@ -21,16 +21,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase private readonly HashSet _lookupQueuedCids = []; private readonly HashSet _syncshellCids = []; - private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4); - private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); + private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4); + private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1); private readonly CancellationTokenSource _cleanupCts = new(); private readonly Task? _cleanupTask; private readonly int _checkEveryFrames = 20; private int _frameCounter = 0; - private const int MaxLookupsPerFrame = 30; - private const int MaxQueueSize = 100; + private const int _maxLookupsPerFrame = 30; + private const int _maxQueueSize = 100; private volatile bool _batchRunning = false; @@ -38,11 +38,11 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); public BroadcastScannerService(ILogger logger, - IObjectTable objectTable, IFramework framework, BroadcastService broadcastService, LightlessMediator mediator, - NameplateHandler nameplateHandler) : base(logger, mediator) + NameplateHandler nameplateHandler, + ActorObjectService actorTracker) : base(logger, mediator) { _logger = logger; _actorTracker = actorTracker; @@ -57,6 +57,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); _nameplateHandler.Init(); + _actorTracker = actorTracker; } private void OnFrameworkUpdate(IFramework framework) => Update(); @@ -79,14 +80,14 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address); var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now; - if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize) + if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize) _lookupQueue.Enqueue(cid); } if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0) { var cidsToLookup = new List(); - while (_lookupQueue.Count > 0 && lookupsThisFrame < MaxLookupsPerFrame) + while (_lookupQueue.Count > 0 && lookupsThisFrame < _maxLookupsPerFrame) { var cid = _lookupQueue.Dequeue(); _lookupQueuedCids.Remove(cid); @@ -113,8 +114,8 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase continue; var ttl = info.IsBroadcasting && info.TTL.HasValue - ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks)) - : RetryDelay; + ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, _maxAllowedTtl.Ticks)) + : _retryDelay; var expiry = now + ttl; diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs index ccf7ae9..f117da9 100644 --- a/LightlessSync/Services/NameplateHandler.cs +++ b/LightlessSync/Services/NameplateHandler.cs @@ -48,7 +48,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber private ImmutableHashSet _activeBroadcastingCids = []; - public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) + public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairUiService pairUiService) { _logger = logger; _addonLifecycle = addonLifecycle; @@ -121,573 +121,573 @@ public unsafe class NameplateHandler : IMediatorSubscriber var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; - if (_mpNameplateAddon != pNameplateAddon) + if (_mpNameplateAddon != pNameplateAddon) + { + for (int i = 0; i < _mTextNodes.Length; ++i) _mTextNodes[i] = null; + System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); + System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); + System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); + System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); + _mpNameplateAddon = pNameplateAddon; + if (_mpNameplateAddon != null) CreateNameplateNodes(); + } + + UpdateNameplateNodes(); + } + + private void CreateNameplateNodes() { - for (int i = 0; i < _mTextNodes.Length; ++i) _mTextNodes[i] = null; + for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) + { + var nameplateObject = GetNameplateObject(i); + if (nameplateObject == null) + continue; + + var rootNode = nameplateObject.Value.RootComponentNode; + if (rootNode == null || rootNode->Component == null) + continue; + + var pNameplateResNode = nameplateObject.Value.NameContainer; + if (pNameplateResNode == null) + continue; + if (pNameplateResNode->ChildNode == null) + continue; + + var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare); + + if (pNewNode != null) + { + var pLastChild = pNameplateResNode->ChildNode; + while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode; + pNewNode->AtkResNode.NextSiblingNode = pLastChild; + pNewNode->AtkResNode.ParentNode = pNameplateResNode; + pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode; + rootNode->Component->UldManager.UpdateDrawNodeList(); + pNewNode->AtkResNode.SetUseDepthBasedPriority(true); + _mTextNodes[i] = pNewNode; + } + } + } + + private void DestroyNameplateNodes() + { + var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); + if (currentHandle.Address == nint.Zero) + { + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available."); + return; + } + + var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address; + if (_mpNameplateAddon == null) + return; + + if (_mpNameplateAddon != pCurrentNameplateAddon) + { + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon); + return; + } + + for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) + { + var pTextNode = _mTextNodes[i]; + var pNameplateNode = GetNameplateComponentNode(i); + if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null)) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); + continue; + } + + if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null) + { + try + { + if (pTextNode->AtkResNode.PrevSiblingNode != null) + pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode; + if (pTextNode->AtkResNode.NextSiblingNode != null) + pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; + pNameplateNode->Component->UldManager.UpdateDrawNodeList(); + pTextNode->AtkResNode.Destroy(free: true); + _mTextNodes[i] = null; + } + catch (Exception e) + { + if (_logger.IsEnabled(LogLevel.Error)) + _logger.LogError("Unknown error while removing text node 0x{textNode} for nameplate {i} on component node 0x{nameplateNode}:\n{e}", (IntPtr)pTextNode, i, (IntPtr)pNameplateNode, e); + } + } + } + System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); - _mpNameplateAddon = pNameplateAddon; - if (_mpNameplateAddon != null) CreateNameplateNodes(); } - UpdateNameplateNodes(); -} - -private void CreateNameplateNodes() -{ - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) + private void HideAllNameplateNodes() { - var nameplateObject = GetNameplateObject(i); - if (nameplateObject == null) - continue; - - var rootNode = nameplateObject.Value.RootComponentNode; - if (rootNode == null || rootNode->Component == null) - continue; - - var pNameplateResNode = nameplateObject.Value.NameContainer; - if (pNameplateResNode == null) - continue; - if (pNameplateResNode->ChildNode == null) - continue; - - var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare); - - if (pNewNode != null) + for (int i = 0; i < _mTextNodes.Length; ++i) { - var pLastChild = pNameplateResNode->ChildNode; - while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode; - pNewNode->AtkResNode.NextSiblingNode = pLastChild; - pNewNode->AtkResNode.ParentNode = pNameplateResNode; - pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode; - rootNode->Component->UldManager.UpdateDrawNodeList(); - pNewNode->AtkResNode.SetUseDepthBasedPriority(true); - _mTextNodes[i] = pNewNode; + HideNameplateTextNode(i); } } -} -private void DestroyNameplateNodes() -{ - var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); - if (currentHandle.Address == nint.Zero) + private void UpdateNameplateNodes() { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available."); - return; - } - - var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null) - return; - - if (_mpNameplateAddon != pCurrentNameplateAddon) - { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon); - return; - } - - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) - { - var pTextNode = _mTextNodes[i]; - var pNameplateNode = GetNameplateComponentNode(i); - if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null)) + var currentHandle = _gameGui.GetAddonByName("NamePlate"); + if (currentHandle.Address == nint.Zero) { if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); - continue; + _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); + return; } - if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null) + var currentAddon = (AddonNamePlate*)currentHandle.Address; + if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) { - try + if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); + return; + } + + var framework = Framework.Instance(); + if (framework == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); + return; + } + + var uiModule = framework->GetUIModule(); + if (uiModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("UI module unavailable during nameplate update, skipping."); + return; + } + + var ui3DModule = uiModule->GetUI3DModule(); + if (ui3DModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); + return; + } + + var vec = ui3DModule->NamePlateObjectInfoPointers; + if (vec.IsEmpty) + return; + + var visibleUserIdsSnapshot = VisibleUserIds; + + var safeCount = System.Math.Min( + ui3DModule->NamePlateObjectInfoCount, + vec.Length + ); + + for (int i = 0; i < safeCount; ++i) + { + var config = _configService.Current; + + var objectInfoPtr = vec[i]; + if (objectInfoPtr == null) + continue; + + var objectInfo = objectInfoPtr.Value; + if (objectInfo == null || objectInfo->GameObject == null) + continue; + + var nameplateIndex = objectInfo->NamePlateIndex; + if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) + continue; + + var pNode = _mTextNodes[nameplateIndex]; + if (pNode == null) + continue; + + var gameObject = objectInfo->GameObject; + if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) { - if (pTextNode->AtkResNode.PrevSiblingNode != null) - pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode; - if (pTextNode->AtkResNode.NextSiblingNode != null) - pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; - pNameplateNode->Component->UldManager.UpdateDrawNodeList(); - pTextNode->AtkResNode.Destroy(free: true); - _mTextNodes[i] = null; + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - catch (Exception e) + + // CID gating + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); + if (cid == null || !_activeBroadcastingCids.Contains(cid)) { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError("Unknown error while removing text node 0x{textNode} for nameplate {i} on component node 0x{nameplateNode}:\n{e}", (IntPtr)pTextNode, i, (IntPtr)pNameplateNode, e); + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - } - } - System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); - System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); - System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); -} + var local = _clientState.LocalPlayer; + if (!config.LightfinderLabelShowOwn && local != null && + objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) + { + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; + } -private void HideAllNameplateNodes() -{ - for (int i = 0; i < _mTextNodes.Length; ++i) - { - HideNameplateTextNode(i); - } -} + var hidePaired = !config.LightfinderLabelShowPaired; -private void UpdateNameplateNodes() -{ - var currentHandle = _gameGui.GetAddonByName("NamePlate"); - if (currentHandle.Address == nint.Zero) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); - return; - } + var goId = (ulong)gameObject->GetGameObjectId(); + if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) + { + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; + } - var currentAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) - { - if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); - return; - } + var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; + var root = nameplateObject.RootComponentNode; + var nameContainer = nameplateObject.NameContainer; + var nameText = nameplateObject.NameText; + var marker = nameplateObject.MarkerIcon; - var framework = Framework.Instance(); - if (framework == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); - return; - } + if (root == null || root->Component == null || nameContainer == null || nameText == null) + { + _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex); + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; + } - var uiModule = framework->GetUIModule(); - if (uiModule == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("UI module unavailable during nameplate update, skipping."); - return; - } + root->Component->UldManager.UpdateDrawNodeList(); - var ui3DModule = uiModule->GetUI3DModule(); - if (ui3DModule == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); - return; - } + bool isVisible = + ((marker != null) && marker->AtkResNode.IsVisible()) || + (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || + config.LightfinderLabelShowHidden; - var vec = ui3DModule->NamePlateObjectInfoPointers; - if (vec.IsEmpty) - return; + pNode->AtkResNode.ToggleVisibility(isVisible); + if (!isVisible) + continue; - var visibleUserIdsSnapshot = VisibleUserIds; + var labelColor = UIColors.Get("Lightfinder"); + var edgeColor = UIColors.Get("LightfinderEdge"); - var safeCount = System.Math.Min( - ui3DModule->NamePlateObjectInfoCount, - vec.Length - ); + var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); + var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f; + var effectiveScale = baseScale * scaleMultiplier; + var labelContent = config.LightfinderLabelUseIcon + ? NormalizeIconGlyph(config.LightfinderLabelIconGlyph) + : DefaultLabelText; - for (int i = 0; i < safeCount; ++i) - { - var config = _configService.Current; - - var objectInfoPtr = vec[i]; - if (objectInfoPtr == null) - continue; - - var objectInfo = objectInfoPtr.Value; - if (objectInfo == null || objectInfo->GameObject == null) - continue; - - var nameplateIndex = objectInfo->NamePlateIndex; - if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) - continue; - - var pNode = _mTextNodes[nameplateIndex]; - if (pNode == null) - continue; - - var gameObject = objectInfo->GameObject; - if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - // CID gating - var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); - if (cid == null || !_activeBroadcastingCids.Contains(cid)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var local = _clientState.LocalPlayer; - if (!config.LightfinderLabelShowOwn && local != null && - objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var hidePaired = !config.LightfinderLabelShowPaired; - - var goId = (ulong)gameObject->GetGameObjectId(); - if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; - var root = nameplateObject.RootComponentNode; - var nameContainer = nameplateObject.NameContainer; - var nameText = nameplateObject.NameText; - var marker = nameplateObject.MarkerIcon; - - if (root == null || root->Component == null || nameContainer == null || nameText == null) - { - _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex); - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - root->Component->UldManager.UpdateDrawNodeList(); - - bool isVisible = - ((marker != null) && marker->AtkResNode.IsVisible()) || - (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || - config.LightfinderLabelShowHidden; - - pNode->AtkResNode.ToggleVisibility(isVisible); - if (!isVisible) - continue; - - var labelColor = UIColors.Get("Lightfinder"); - var edgeColor = UIColors.Get("LightfinderEdge"); - - var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); - var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f; - var effectiveScale = baseScale * scaleMultiplier; - var labelContent = config.LightfinderLabelUseIcon - ? NormalizeIconGlyph(config.LightfinderLabelIconGlyph) - : DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - pNode->AtkResNode.SetScale(effectiveScale, effectiveScale); - var nodeWidth = (int)pNode->AtkResNode.GetWidth(); - if (nodeWidth <= 0) - nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); - var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f; - var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); - pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255); - AlignmentType alignment; - - var textScaleY = nameText->AtkResNode.ScaleY; - if (textScaleY <= 0f) - textScaleY = 1f; - - var blockHeight = System.Math.Abs((int)nameplateObject.TextH); - if (blockHeight > 0) - { - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - else - { - blockHeight = _cachedNameplateTextHeights[nameplateIndex]; - } - - if (blockHeight <= 0) - { - blockHeight = GetScaledTextHeight(nameText); - if (blockHeight <= 0) - blockHeight = nodeHeight; - - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - - var containerHeight = (int)nameContainer->Height; - if (containerHeight > 0) - { - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - else - { - containerHeight = _cachedNameplateContainerHeights[nameplateIndex]; - } - - if (containerHeight <= 0) - { - containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY); - if (containerHeight <= blockHeight) - containerHeight = blockHeight + 1; - - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - - var blockTop = containerHeight - blockHeight; - if (blockTop < 0) - blockTop = 0; - var verticalPadding = (int)System.Math.Round(4 * effectiveScale); - - var positionY = blockTop - verticalPadding - nodeHeight; - - var textWidth = System.Math.Abs((int)nameplateObject.TextW); - if (textWidth <= 0) - { - textWidth = GetScaledTextWidth(nameText); - if (textWidth <= 0) - textWidth = nodeWidth; - } - - if (textWidth > 0) - { - _cachedNameplateTextWidths[nameplateIndex] = textWidth; - } - - var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); - var hasValidOffset = true; - - if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0) - { - _cachedNameplateTextOffsets[nameplateIndex] = textOffset; - } - else - { - hasValidOffset = false; - } - int positionX; - - - if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) - labelContent = DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - - pNode->SetText(labelContent); - - if (!config.LightfinderLabelUseIcon) - { - pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = (int)pNode->AtkResNode.GetWidth(); + pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; + pNode->AtkResNode.SetScale(effectiveScale, effectiveScale); + var nodeWidth = (int)pNode->AtkResNode.GetWidth(); if (nodeWidth <= 0) nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - pNode->AtkResNode.Width = (ushort)nodeWidth; - } - else - { - pNode->TextFlags |= TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = pNode->AtkResNode.GetWidth(); - } + var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); + var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f; + var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); + pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255); + AlignmentType alignment; + var textScaleY = nameText->AtkResNode.ScaleY; + if (textScaleY <= 0f) + textScaleY = 1f; - if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset) - { - var nameplateWidth = (int)nameContainer->Width; - - int leftPos = nameplateWidth / 8; - int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8); - int centrePos = (nameplateWidth - nodeWidth) / 2; - int staticMargin = 24; - int calcMargin = (int)(nameplateWidth * 0.08f); - - switch (config.LabelAlignment) + var blockHeight = System.Math.Abs((int)nameplateObject.TextH); + if (blockHeight > 0) { - case LabelAlignment.Left: - positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos; - alignment = AlignmentType.BottomLeft; - break; - case LabelAlignment.Right: - positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin; - alignment = AlignmentType.BottomRight; - break; - default: - positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin; - alignment = AlignmentType.Bottom; - break; + _cachedNameplateTextHeights[nameplateIndex] = blockHeight; } + else + { + blockHeight = _cachedNameplateTextHeights[nameplateIndex]; + } + + if (blockHeight <= 0) + { + blockHeight = GetScaledTextHeight(nameText); + if (blockHeight <= 0) + blockHeight = nodeHeight; + + _cachedNameplateTextHeights[nameplateIndex] = blockHeight; + } + + var containerHeight = (int)nameContainer->Height; + if (containerHeight > 0) + { + _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; + } + else + { + containerHeight = _cachedNameplateContainerHeights[nameplateIndex]; + } + + if (containerHeight <= 0) + { + containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY); + if (containerHeight <= blockHeight) + containerHeight = blockHeight + 1; + + _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; + } + + var blockTop = containerHeight - blockHeight; + if (blockTop < 0) + blockTop = 0; + var verticalPadding = (int)System.Math.Round(4 * effectiveScale); + + var positionY = blockTop - verticalPadding - nodeHeight; + + var textWidth = System.Math.Abs((int)nameplateObject.TextW); + if (textWidth <= 0) + { + textWidth = GetScaledTextWidth(nameText); + if (textWidth <= 0) + textWidth = nodeWidth; + } + + if (textWidth > 0) + { + _cachedNameplateTextWidths[nameplateIndex] = textWidth; + } + + var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); + var hasValidOffset = true; + + if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0) + { + _cachedNameplateTextOffsets[nameplateIndex] = textOffset; + } + else + { + hasValidOffset = false; + } + int positionX; + + + if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) + labelContent = DefaultLabelText; + + pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; + + pNode->SetText(labelContent); + + if (!config.LightfinderLabelUseIcon) + { + pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize; + pNode->AtkResNode.Width = 0; + nodeWidth = (int)pNode->AtkResNode.GetWidth(); + if (nodeWidth <= 0) + nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); + pNode->AtkResNode.Width = (ushort)nodeWidth; + } + else + { + pNode->TextFlags |= TextFlags.AutoAdjustNodeSize; + pNode->AtkResNode.Width = 0; + nodeWidth = pNode->AtkResNode.GetWidth(); + } + + + if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset) + { + var nameplateWidth = (int)nameContainer->Width; + + int leftPos = nameplateWidth / 8; + int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8); + int centrePos = (nameplateWidth - nodeWidth) / 2; + int staticMargin = 24; + int calcMargin = (int)(nameplateWidth * 0.08f); + + switch (config.LabelAlignment) + { + case LabelAlignment.Left: + positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos; + alignment = AlignmentType.BottomLeft; + break; + case LabelAlignment.Right: + positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin; + alignment = AlignmentType.BottomRight; + break; + default: + positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin; + alignment = AlignmentType.Bottom; + break; + } + } + else + { + positionX = 58 + config.LightfinderLabelOffsetX; + alignment = AlignmentType.Bottom; + } + + positionY += config.LightfinderLabelOffsetY; + + alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); + pNode->AtkResNode.SetUseDepthBasedPriority(enable: true); + + pNode->AtkResNode.Color.A = 255; + + pNode->TextColor.R = (byte)(labelColor.X * 255); + pNode->TextColor.G = (byte)(labelColor.Y * 255); + pNode->TextColor.B = (byte)(labelColor.Z * 255); + pNode->TextColor.A = (byte)(labelColor.W * 255); + + pNode->EdgeColor.R = (byte)(edgeColor.X * 255); + pNode->EdgeColor.G = (byte)(edgeColor.Y * 255); + pNode->EdgeColor.B = (byte)(edgeColor.Z * 255); + pNode->EdgeColor.A = (byte)(edgeColor.W * 255); + + + if (!config.LightfinderLabelUseIcon) + { + pNode->AlignmentType = AlignmentType.Bottom; + } + else + { + pNode->AlignmentType = alignment; + } + pNode->AtkResNode.SetPositionShort( + (short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue), + (short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue) + ); + var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier); + pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue); + pNode->CharSpacing = 1; + pNode->TextFlags = config.LightfinderLabelUseIcon + ? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize + : TextFlags.Edge | TextFlags.Glare; } - else - { - positionX = 58 + config.LightfinderLabelOffsetX; - alignment = AlignmentType.Bottom; - } - - positionY += config.LightfinderLabelOffsetY; - - alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); - pNode->AtkResNode.SetUseDepthBasedPriority(enable: true); - - pNode->AtkResNode.Color.A = 255; - - pNode->TextColor.R = (byte)(labelColor.X * 255); - pNode->TextColor.G = (byte)(labelColor.Y * 255); - pNode->TextColor.B = (byte)(labelColor.Z * 255); - pNode->TextColor.A = (byte)(labelColor.W * 255); - - pNode->EdgeColor.R = (byte)(edgeColor.X * 255); - pNode->EdgeColor.G = (byte)(edgeColor.Y * 255); - pNode->EdgeColor.B = (byte)(edgeColor.Z * 255); - pNode->EdgeColor.A = (byte)(edgeColor.W * 255); - - - if(!config.LightfinderLabelUseIcon) - { - pNode->AlignmentType = AlignmentType.Bottom; - } - else - { - pNode->AlignmentType = alignment; - } - pNode->AtkResNode.SetPositionShort( - (short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue), - (short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue) - ); - var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier); - pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue); - pNode->CharSpacing = 1; - pNode->TextFlags = config.LightfinderLabelUseIcon - ? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize - : TextFlags.Edge | TextFlags.Glare; } -} -private static unsafe int GetScaledTextHeight(AtkTextNode* node) -{ - if (node == null) - return 0; + private static unsafe int GetScaledTextHeight(AtkTextNode* node) + { + if (node == null) + return 0; - var resNode = &node->AtkResNode; - var rawHeight = (int)resNode->GetHeight(); - if (rawHeight <= 0 && node->LineSpacing > 0) - rawHeight = node->LineSpacing; - if (rawHeight <= 0) - rawHeight = AtkNodeHelpers.DefaultTextNodeHeight; + var resNode = &node->AtkResNode; + var rawHeight = (int)resNode->GetHeight(); + if (rawHeight <= 0 && node->LineSpacing > 0) + rawHeight = node->LineSpacing; + if (rawHeight <= 0) + rawHeight = AtkNodeHelpers.DefaultTextNodeHeight; - var scale = resNode->ScaleY; - if (scale <= 0f) - scale = 1f; + var scale = resNode->ScaleY; + if (scale <= 0f) + scale = 1f; - var computed = (int)System.Math.Round(rawHeight * scale); - return System.Math.Max(1, computed); -} + var computed = (int)System.Math.Round(rawHeight * scale); + return System.Math.Max(1, computed); + } -private static unsafe int GetScaledTextWidth(AtkTextNode* node) -{ - if (node == null) - return 0; + private static unsafe int GetScaledTextWidth(AtkTextNode* node) + { + if (node == null) + return 0; - var resNode = &node->AtkResNode; - var rawWidth = (int)resNode->GetWidth(); - if (rawWidth <= 0) - rawWidth = AtkNodeHelpers.DefaultTextNodeWidth; + var resNode = &node->AtkResNode; + var rawWidth = (int)resNode->GetWidth(); + if (rawWidth <= 0) + rawWidth = AtkNodeHelpers.DefaultTextNodeWidth; - var scale = resNode->ScaleX; - if (scale <= 0f) - scale = 1f; + var scale = resNode->ScaleX; + if (scale <= 0f) + scale = 1f; - var computed = (int)System.Math.Round(rawWidth * scale); - return System.Math.Max(1, computed); -} + var computed = (int)System.Math.Round(rawWidth * scale); + return System.Math.Max(1, computed); + } + + internal static string NormalizeIconGlyph(string? rawInput) + { + if (string.IsNullOrWhiteSpace(rawInput)) + return DefaultIconGlyph; + + var trimmed = rawInput.Trim(); + + if (Enum.TryParse(trimmed, true, out var iconEnum)) + return SeIconCharExtensions.ToIconString(iconEnum); + + var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? trimmed[2..] + : trimmed; + + if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue)) + return char.ConvertFromUtf32(hexValue); + + var enumerator = trimmed.EnumerateRunes(); + if (enumerator.MoveNext()) + return enumerator.Current.ToString(); -internal static string NormalizeIconGlyph(string? rawInput) -{ - if (string.IsNullOrWhiteSpace(rawInput)) return DefaultIconGlyph; + } - var trimmed = rawInput.Trim(); - - if (Enum.TryParse(trimmed, true, out var iconEnum)) - return SeIconCharExtensions.ToIconString(iconEnum); - - var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? trimmed[2..] - : trimmed; - - if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue)) - return char.ConvertFromUtf32(hexValue); - - var enumerator = trimmed.EnumerateRunes(); - if (enumerator.MoveNext()) - return enumerator.Current.ToString(); - - return DefaultIconGlyph; -} - -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 void HideNameplateTextNode(int i) -{ - var pNode = _mTextNodes[i]; - if (pNode != null) + internal static string ToIconEditorString(string? rawInput) { - pNode->AtkResNode.ToggleVisibility(false); + var normalized = NormalizeIconGlyph(rawInput); + var runeEnumerator = normalized.EnumerateRunes(); + return runeEnumerator.MoveNext() + ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture) + : DefaultIconGlyph; + } + private void HideNameplateTextNode(int i) + { + var pNode = _mTextNodes[i]; + if (pNode != null) + { + pNode->AtkResNode.ToggleVisibility(false); + } + } + + private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) + { + if (i < AddonNamePlate.NumNamePlateObjects && + _mpNameplateAddon != null && + _mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) + { + return _mpNameplateAddon->NamePlateObjectArray[i]; + } + return null; + } + + private AtkComponentNode* GetNameplateComponentNode(int i) + { + var nameplateObject = GetNameplateObject(i); + return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; + } + + private HashSet VisibleUserIds + => [.. _pairUiService.GetSnapshot().PairsByUid.Values + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId)]; + + public void FlagRefresh() + { + _needsLabelRefresh = true; + } + + public void OnTick(PriorityFrameworkUpdateMessage _) + { + if (_needsLabelRefresh) + { + UpdateNameplateNodes(); + _needsLabelRefresh = false; + } + } + + public void UpdateBroadcastingCids(IEnumerable cids) + { + var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); + if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) + return; + + _activeBroadcastingCids = newSet; + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); + FlagRefresh(); + } + + public void ClearNameplateCaches() + { + System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); + System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); + System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); + System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); } } - -private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) -{ - if (i < AddonNamePlate.NumNamePlateObjects && - _mpNameplateAddon != null && - _mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) - { - return _mpNameplateAddon->NamePlateObjectArray[i]; - } - return null; -} - -private AtkComponentNode* GetNameplateComponentNode(int i) -{ - var nameplateObject = GetNameplateObject(i); - return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; -} - -private HashSet VisibleUserIds => [.. _pairManager.GetOnlineUserPairs() - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId)]; - - -public void FlagRefresh() -{ - _needsLabelRefresh = true; -} - -public void OnTick(PriorityFrameworkUpdateMessage _) -{ - if (_needsLabelRefresh) - { - UpdateNameplateNodes(); - _needsLabelRefresh = false; - } -} - -public void UpdateBroadcastingCids(IEnumerable cids) -{ - var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); - if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) - return; - - _activeBroadcastingCids = newSet; - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); - FlagRefresh(); -} - -public void ClearNameplateCaches() -{ - System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); - System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); - System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); -} -} diff --git a/LightlessSync/Services/UiFactory.cs b/LightlessSync/Services/UiFactory.cs index 435d3c2..72681f7 100644 --- a/LightlessSync/Services/UiFactory.cs +++ b/LightlessSync/Services/UiFactory.cs @@ -22,7 +22,6 @@ public class UiFactory private readonly ServerConfigurationManager _serverConfigManager; private readonly LightlessProfileManager _lightlessProfileManager; private readonly PerformanceCollectorService _performanceCollectorService; - private readonly FileDialogManager _fileDialogManager; private readonly ProfileTagService _profileTagService; public UiFactory( @@ -34,7 +33,6 @@ public class UiFactory ServerConfigurationManager serverConfigManager, LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService, - FileDialogManager fileDialogManager, ProfileTagService profileTagService) { _loggerFactory = loggerFactory; @@ -45,7 +43,6 @@ public class UiFactory _serverConfigManager = serverConfigManager; _lightlessProfileManager = lightlessProfileManager; _performanceCollectorService = performanceCollectorService; - _fileDialogManager = fileDialogManager; _profileTagService = profileTagService; } @@ -59,8 +56,7 @@ public class UiFactory _pairUiService, dto, _performanceCollectorService, - _lightlessProfileManager, - _fileDialogManager); + _lightlessProfileManager); } public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 6878ce3..5540b02 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; +using Dalamud.Utility; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index ab05f2d..1ecf3f5 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -21,6 +21,7 @@ namespace LightlessSync.UI; public class DrawEntityFactory { + private readonly ILogger _logger; private readonly ApiController _apiController; private readonly LightlessMediator _mediator; private readonly SelectPairForTagUi _selectPairForTagUi; @@ -32,6 +33,8 @@ public class DrawEntityFactory private readonly SelectTagForPairUi _selectTagForPairUi; private readonly RenamePairTagUi _renamePairTagUi; private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; + private readonly RenameSyncshellTagUi _renameSyncshellTagUi; + private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; private readonly TagHandler _tagHandler; private readonly IdDisplayHandler _uidDisplayHandler; private readonly PairLedger _pairLedger; @@ -57,6 +60,7 @@ public class DrawEntityFactory PairLedger pairLedger, PairFactory pairFactory) { + _logger = logger; _apiController = apiController; _uidDisplayHandler = uidDisplayHandler; _selectTagForPairUi = selectTagForPairUi; diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 29e1880..c0f0a68 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2721,7 +2721,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(5)); - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f); var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures; if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed)) { @@ -2729,7 +2729,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Save(); } _uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too."); - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f); ImGui.Dummy(new Vector2(5)); @@ -2737,7 +2737,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(5)); - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 4941912..27da617 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -1,20 +1,20 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; +using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; -using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services.Profiles; +using LightlessSync.UI.Services; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using System.Globalization; namespace LightlessSync.UI; @@ -25,38 +25,29 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase private readonly bool _isModerator = false; private readonly bool _isOwner = false; private readonly List _oneTimeInvites = []; - private readonly PairManager _pairManager; private readonly LightlessProfileManager _lightlessProfileManager; - private readonly FileDialogManager _fileDialogManager; private readonly UiSharedService _uiSharedService; + private readonly PairUiService _pairUiService; private List _bannedUsers = []; private LightlessGroupProfileData? _profileData = null; - private bool _adjustedForScollBarsLocalProfile = false; - private bool _adjustedForScollBarsOnlineProfile = false; - private string _descriptionText = string.Empty; private IDalamudTextureWrap? _pfpTextureWrap; private string _profileDescription = string.Empty; - private byte[] _profileImage = []; - private bool _showFileDialogError = false; private int _multiInvites; private string _newPassword; private bool _pwChangeSuccess; private Task? _pruneTestTask; private Task? _pruneTask; private int _pruneDays = 14; - private List _selectedTags = []; public SyncshellAdminUI(ILogger logger, LightlessMediator mediator, ApiController apiController, - UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager) + UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService) { GroupFullInfo = groupFullInfo; _apiController = apiController; _uiSharedService = uiSharedService; - _pairManager = pairManager; _lightlessProfileManager = lightlessProfileManager; - _fileDialogManager = fileDialogManager; - + _pairUiService = pairUiService; _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); _isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); _newPassword = string.Empty; @@ -76,6 +67,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase MinimumSize = new(700, 500), MaximumSize = new(700, 2000), }; + _pairUiService = pairUiService; } public GroupFullInfoDto GroupFullInfo { get; private set; } @@ -85,10 +77,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase if (!_isModerator && !_isOwner) return; _logger.LogTrace("Drawing Syncshell Admin UI for {group}", GroupFullInfo.GroupAliasOrGID); - GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group]; + var snapshot = _pairUiService.GetSnapshot(); + if (snapshot.GroupsByGid.TryGetValue(GroupFullInfo.Group.GID, out var updatedInfo)) + { + GroupFullInfo = updatedInfo; + } _profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group); - GetTagsFromProfile(); using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); using (_uiSharedService.UidFont.Push()) @@ -207,189 +202,51 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ownerTab.Dispose(); } } + private void DrawProfile() { var profileTab = ImRaii.TabItem("Profile"); + if (!profileTab) + return; - if (profileTab) + if (_profileData != null) { - if (_uiSharedService.MediumTreeNode("Current Profile", UIColors.Get("LightlessPurple"))) + if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.Ordinal)) { - ImGui.Dummy(new Vector2(5)); - - if (_profileData == null) - { - UiSharedService.ColorTextWrapped("Failed to load profile data.", ImGuiColors.DalamudRed); - ImGui.TreePop(); - return; - } - - if (!_profileImage.SequenceEqual(_profileData.ProfileImageData.Value)) - { - _profileImage = _profileData.ProfileImageData.Value; - _pfpTextureWrap?.Dispose(); - _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); - } - - if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.OrdinalIgnoreCase)) - { - _profileDescription = _profileData.Description; - _descriptionText = _profileDescription; - } - - if (_pfpTextureWrap != null) - { - ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); - } - - var spacing = ImGui.GetStyle().ItemSpacing.X; - ImGuiHelpers.ScaledRelativeSameLine(256, spacing); - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSize = ImGui.CalcTextSize(_profileData.Description, wrapWidth: 256f); - var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256); - if (descriptionTextSize.Y > childFrame.Y) - { - _adjustedForScollBarsOnlineProfile = true; - } - else - { - _adjustedForScollBarsOnlineProfile = false; - } - childFrame = childFrame with - { - X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(101, childFrame)) - { - UiSharedService.TextWrapped(_profileData.Description); - } - ImGui.EndChildFrame(); - ImGui.TreePop(); - } - var nsfw = _profileData.IsNsfw; - ImGui.BeginDisabled(); - ImGui.Checkbox("Is NSFW", ref nsfw); - ImGui.EndDisabled(); + _profileDescription = _profileData.Description; } - ImGui.Separator(); + UiSharedService.TextWrapped("Preview the Syncshell profile in a standalone window."); - if (_uiSharedService.MediumTreeNode("Profile Settings", UIColors.Get("LightlessPurple"))) + if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile")) { - ImGui.Dummy(new Vector2(5)); - ImGui.TextUnformatted($"Profile Picture:"); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) - { - _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => - { - if (!success) return; - _ = Task.Run(async () => - { - var fileContent = await File.ReadAllBytesAsync(file).ConfigureAwait(false); - MemoryStream ms = new(fileContent); - await using (ms.ConfigureAwait(false)) - { - var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) - { - _showFileDialogError = true; - return; - } - using var image = Image.Load(fileContent); - - if (image.Width > 512 || image.Height > 512 || (fileContent.Length > 2000 * 1024)) - { - _showFileDialogError = true; - return; - } - - _showFileDialogError = false; - await _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, Convert.ToBase64String(fileContent), BannerBase64: null, IsNsfw: null, IsDisabled: null)) - .ConfigureAwait(false); - } - }); - }); - } - UiSharedService.AttachToolTip("Select and upload a new profile picture"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); - if (_showFileDialogError) - { - UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); - } - ImGui.Separator(); - ImGui.TextUnformatted($"Tags:"); - var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); - - var allCategoryIndexes = Enum.GetValues() - .Cast() - .ToList(); - - foreach (int tag in allCategoryIndexes) - { - using (ImRaii.PushId($"tag-{tag}")) DrawTag(tag); - } - ImGui.Separator(); - var widthTextBox = 400; - var posX = ImGui.GetCursorPosX(); - ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); - ImGui.SetCursorPosX(posX); - ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); - ImGui.TextUnformatted("Preview (approximate)"); - using (_uiSharedService.GameFont.Push()) - ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); - - ImGui.SameLine(); - - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f); - if (descriptionTextSizeLocal.Y > childFrameLocal.Y) - { - _adjustedForScollBarsLocalProfile = true; - } - else - { - _adjustedForScollBarsLocalProfile = false; - } - childFrameLocal = childFrameLocal with - { - X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(102, childFrameLocal)) - { - UiSharedService.TextWrapped(_descriptionText); - } - ImGui.EndChildFrame(); - } - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: _descriptionText, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Sets your profile description text"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Clears your profile description text"); - ImGui.Separator(); - ImGui.TextUnformatted($"Profile Options:"); - var isNsfw = _profileData?.IsNsfw ?? false; - if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: isNsfw, IsDisabled: null)); - } - _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); - ImGui.TreePop(); + Mediator.Publish(new GroupProfileOpenStandaloneMessage(GroupFullInfo)); } + UiSharedService.AttachToolTip("Opens the standalone Syncshell profile window for this group."); + + ImGuiHelpers.ScaledDummy(2f); + ImGui.TextDisabled("Profile Flags"); + ImGui.BulletText(_profileData.IsNsfw ? "Marked as NSFW" : "Marked as SFW"); + ImGui.BulletText(_profileData.IsDisabled ? "Profile disabled for viewers" : "Profile active"); + + ImGuiHelpers.ScaledDummy(2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGuiHelpers.ScaledDummy(2f); + + UiSharedService.TextWrapped("Open the syncshell profile editor to update images, description, tags, and visibility settings."); + ImGuiHelpers.ScaledDummy(2f); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserEdit, "Open Syncshell Profile Editor")) + { + Mediator.Publish(new OpenGroupProfileEditorMessage(GroupFullInfo)); + } + UiSharedService.AttachToolTip("Launches the editor window and associated live preview for this syncshell."); } + else + { + UiSharedService.TextWrapped("Profile information is loading..."); + } + profileTab.Dispose(); } @@ -400,7 +257,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase { if (_uiSharedService.MediumTreeNode("User List & Administration", UIColors.Get("LightlessPurple"))) { - if (!_pairManager.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) + var snapshot = _pairUiService.GetSnapshot(); + if (!snapshot.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) { UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow); } @@ -736,33 +594,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } inviteTab.Dispose(); } - private void DrawTag(int tag) - { - var HasTag = _selectedTags.Contains(tag); - var tagName = (ProfileTags)tag; - - if (ImGui.Checkbox(tagName.ToString(), ref HasTag)) - { - if (HasTag) - { - _selectedTags.Add(tag); - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - else - { - _selectedTags.Remove(tag); - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - } - } - - private void GetTagsFromProfile() - { - if (_profileData != null) - { - _selectedTags = [.. _profileData.Tags]; - } - } public override void OnClose() { diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 629c18b..0ebfdef 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -474,7 +474,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private async Task RefreshSyncshellsAsync(string? gid = null) { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); - _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; + var snapshot = _pairUiService.GetSnapshot(); + _currentSyncshells = [.. snapshot.GroupPairs.Keys]; _recentlyJoined.RemoveWhere(gid => _currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 0673682..b3734a3 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -506,7 +506,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Dummy(new Vector2(0, thickness * ImGuiHelpers.GlobalScale)); } - public static void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f) + public void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f) { float scale = ImGuiHelpers.GlobalScale; -- 2.49.1 From 1e401582746638a5f5ff7bd1a048c9fd9536fbb0 Mon Sep 17 00:00:00 2001 From: cake Date: Sat, 29 Nov 2025 17:56:15 +0100 Subject: [PATCH 21/21] Fixed cache issues because conflicts --- LightlessSync/FileCache/CacheMonitor.cs | 100 ++++++++++----------- LightlessSync/LightlessSync.csproj | 2 +- LightlessSync/UI/Services/PairUiService.cs | 3 - LightlessSync/packages.lock.json | 8 +- 4 files changed, 52 insertions(+), 61 deletions(-) diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index d32b40d..83c3b96 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -259,8 +259,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private CancellationTokenSource _penumbraFswCts = new(); private CancellationTokenSource _lightlessFswCts = new(); - private long totalSize; - private long maxCacheBytes; public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; } @@ -406,73 +404,71 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase public void RecalculateFileCacheSize(CancellationToken token) { - var folder = _configService.Current.CacheFolder; - if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) + if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || + !Directory.Exists(_configService.Current.CacheFolder)) { FileCacheSize = 0; return; } FileCacheSize = -1; + bool isWine = _dalamudUtil?.IsWine ?? false; try { var drive = DriveInfo.GetDrives() - .FirstOrDefault(d => folder.StartsWith(d.Name, StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(d => _configService.Current.CacheFolder + .StartsWith(d.Name, StringComparison.OrdinalIgnoreCase)); + if (drive != null) FileCacheDriveFree = drive.AvailableFreeSpace; } catch (Exception ex) { - Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", folder); + Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder); } - List files; - try - { - files = [.. new DirectoryInfo(folder) - .EnumerateFiles("*", SearchOption.TopDirectoryOnly) - .OrderBy(f => f.LastAccessTimeUtc)]; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to enumerate files in {folder}", folder); - FileCacheSize = 0; - return; - } - - var entries = new List<(FileInfo fi, long size)>(files.Count); - long total = 0; + var files = Directory.EnumerateFiles(_configService.Current.CacheFolder) + .Select(f => new FileInfo(f)) + .OrderBy(f => f.LastAccessTime) + .ToList(); + + long totalSize = 0; foreach (var f in files) { token.ThrowIfCancellationRequested(); - long size; - if (_configService.Current.UseCompactor) + try { - try + long size = 0; + + if (!isWine) { - size = _fileCompactor.GetFileSizeOnDisk(f); - if (size < 0) size = f.Length; + try + { + size = _fileCompactor.GetFileSizeOnDisk(f); + } + catch (Exception ex) + { + Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName); + size = f.Length; + } } - catch (Exception ex) + else { - Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using Length", f.FullName); size = f.Length; } - } - else - { - size = f.Length; + + totalSize += size; + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Error getting size for {file}", f.FullName); } - - - entries.Add((f, size)); - total += size; } - FileCacheSize = total; + FileCacheSize = totalSize; if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled")) { @@ -488,7 +484,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { long size = 0; - if (!_dalamudUtil.IsWine) + if (!isWine) { try { @@ -514,38 +510,34 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } FileCacheSize = (totalSize + totalSizeDownscaled); - } + } else { FileCacheSize = totalSize; } - var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); + var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); if (FileCacheSize < maxCacheInBytes) return; - var buffer = (long)(maxCacheBytes * 0.05d); - var target = maxCacheBytes - buffer; + var maxCacheBuffer = maxCacheInBytes * 0.05d; - var i = 0; - while (i < entries.Count && FileCacheSize > target) + while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer && files.Count > 0) { - token.ThrowIfCancellationRequested(); + var oldestFile = files[0]; - var (fi, sz) = entries[i]; try { - File.Delete(fi.FullName); - FileCacheSize -= sz; + long fileSize = oldestFile.Length; + File.Delete(oldestFile.FullName); + FileCacheSize -= fileSize; } catch (Exception ex) { - Logger.LogTrace(ex, "Failed to delete old file {file}", fi.FullName); - } - finally - { - i++; + Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullName); } + + files.RemoveAt(0); } } diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 3ceb697..975e935 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -10,7 +10,7 @@ - net10.0-windows + net9.0-windows7.0 x64 enable latest diff --git a/LightlessSync/UI/Services/PairUiService.cs b/LightlessSync/UI/Services/PairUiService.cs index 5d38aec..290a7fb 100644 --- a/LightlessSync/UI/Services/PairUiService.cs +++ b/LightlessSync/UI/Services/PairUiService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using LightlessSync.API.Dto.Group; using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Pairs; diff --git a/LightlessSync/packages.lock.json b/LightlessSync/packages.lock.json index df69297..a109393 100644 --- a/LightlessSync/packages.lock.json +++ b/LightlessSync/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - "net10.0-windows7.0": { + "net9.0-windows7.0": { "Blake3": { "type": "Direct", "requested": "[2.0.0, )", @@ -191,7 +191,8 @@ "dependencies": { "Microsoft.AspNetCore.Http.Connections.Common": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3" + "Microsoft.Extensions.Options": "9.0.3", + "System.Net.ServerSentEvents": "9.0.3" } }, "Microsoft.AspNetCore.Http.Connections.Common": { @@ -210,7 +211,8 @@ "Microsoft.AspNetCore.SignalR.Common": "9.0.3", "Microsoft.AspNetCore.SignalR.Protocols.Json": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3" + "Microsoft.Extensions.Logging": "9.0.3", + "System.Threading.Channels": "9.0.3" } }, "Microsoft.AspNetCore.SignalR.Common": { -- 2.49.1