From 4b13dfe8d409ca20eb3359d367375cdfd20025ec Mon Sep 17 00:00:00 2001 From: azyges Date: Sat, 3 Jan 2026 03:19:10 +0900 Subject: [PATCH 1/4] skip decimation for direct pairs and make it a toggle in settings --- .../Configurations/PlayerPerformanceConfig.cs | 2 + .../PlayerData/Factories/PlayerDataFactory.cs | 47 +++++++++++++----- .../PlayerData/Pairs/PairHandlerAdapter.cs | 48 ++++++++++++++----- .../Pairs/PairHandlerAdapterFactory.cs | 5 ++ .../Services/PlayerPerformanceService.cs | 13 ++++- LightlessSync/UI/SettingsUi.cs | 16 +++++++ .../WebAPI/Files/FileDownloadManager.cs | 38 ++++++++------- 7 files changed, 126 insertions(+), 43 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs index b905c05..5cdfd4e 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs @@ -22,10 +22,12 @@ public class PlayerPerformanceConfig : ILightlessConfiguration public int TextureDownscaleMaxDimension { get; set; } = 2048; public bool OnlyDownscaleUncompressedTextures { get; set; } = true; public bool KeepOriginalTextureFiles { get; set; } = false; + public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true; public bool EnableModelDecimation { get; set; } = false; public int ModelDecimationTriangleThreshold { get; set; } = 50_000; public double ModelDecimationTargetRatio { get; set; } = 0.8; public bool KeepOriginalModelFiles { get; set; } = true; + public bool SkipModelDecimationForPreferredPairs { get; set; } = true; public bool ModelDecimationAllowBody { get; set; } = false; public bool ModelDecimationAllowFaceHead { get; set; } = false; public bool ModelDecimationAllowTail { get; set; } = false; diff --git a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs index 9ecfcc3..7b7434e 100644 --- a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs +++ b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs @@ -146,7 +146,8 @@ public class PlayerDataFactory fragment.FileReplacements = new HashSet(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance) .Where(p => p.HasFileReplacement).ToHashSet(); - fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); + var allowedExtensions = CacheMonitor.AllowedFileExtensions; + fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !allowedExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); ct.ThrowIfCancellationRequested(); @@ -194,7 +195,9 @@ public class PlayerDataFactory // get all remaining paths and resolve them var transientPaths = ManageSemiTransientData(objectKind); - var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet(StringComparer.Ordinal)).ConfigureAwait(false); + var resolvedTransientPaths = transientPaths.Count == 0 + ? new Dictionary(StringComparer.OrdinalIgnoreCase).AsReadOnly() + : await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet(StringComparer.Ordinal)).ConfigureAwait(false); if (logDebug) { @@ -377,7 +380,15 @@ public class PlayerDataFactory { var forwardPaths = forwardResolve.ToArray(); var reversePaths = reverseResolve.ToArray(); - Dictionary> resolvedPaths = new(StringComparer.Ordinal); + if (forwardPaths.Length == 0 && reversePaths.Length == 0) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase).AsReadOnly(); + } + + var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray(); + var reversePathsLower = reversePaths.Length == 0 ? Array.Empty() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray(); + + Dictionary> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal); if (handler.ObjectKind != ObjectKind.Player) { var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() => @@ -415,24 +426,29 @@ public class PlayerDataFactory if (resolvedPaths.TryGetValue(filePath, out var list)) { - list.Add(forwardPaths[i].ToLowerInvariant()); + list.Add(forwardPathsLower[i]); } else { - resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; + resolvedPaths[filePath] = [forwardPathsLower[i]]; } } for (int i = 0; i < reversePaths.Length; i++) { - var filePath = reversePaths[i].ToLowerInvariant(); + var filePath = reversePathsLower[i]; + var reverseResolvedLower = new string[reverseResolved[i].Length]; + for (var j = 0; j < reverseResolvedLower.Length; j++) + { + reverseResolvedLower[j] = reverseResolved[i][j].ToLowerInvariant(); + } if (resolvedPaths.TryGetValue(filePath, out var list)) { - list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant())); + list.AddRange(reverseResolvedLower); } else { - resolvedPaths[filePath] = new List(reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList()); + resolvedPaths[filePath] = new List(reverseResolvedLower); } } @@ -446,24 +462,29 @@ public class PlayerDataFactory var filePath = forward[i].ToLowerInvariant(); if (resolvedPaths.TryGetValue(filePath, out var list)) { - list.Add(forwardPaths[i].ToLowerInvariant()); + list.Add(forwardPathsLower[i]); } else { - resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; + resolvedPaths[filePath] = [forwardPathsLower[i]]; } } for (int i = 0; i < reversePaths.Length; i++) { - var filePath = reversePaths[i].ToLowerInvariant(); + var filePath = reversePathsLower[i]; + var reverseResolvedLower = new string[reverse[i].Length]; + for (var j = 0; j < reverseResolvedLower.Length; j++) + { + reverseResolvedLower[j] = reverse[i][j].ToLowerInvariant(); + } if (resolvedPaths.TryGetValue(filePath, out var list)) { - list.AddRange(reverse[i].Select(c => c.ToLowerInvariant())); + list.AddRange(reverseResolvedLower); } else { - resolvedPaths[filePath] = new List(reverse[i].Select(c => c.ToLowerInvariant()).ToList()); + resolvedPaths[filePath] = new List(reverseResolvedLower); } } diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 03c4f94..6d859ac 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -6,6 +6,7 @@ using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.FileCache; using LightlessSync.Interop.Ipc; +using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; @@ -38,6 +39,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private readonly ActorObjectService _actorObjectService; private readonly FileDownloadManager _downloadManager; private readonly FileCacheManager _fileDbManager; + private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly IpcManager _ipcManager; private readonly IHostApplicationLifetime _lifetime; @@ -197,6 +199,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa ActorObjectService actorObjectService, IHostApplicationLifetime lifetime, FileCacheManager fileDbManager, + PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceService playerPerformanceService, PairProcessingLimiter pairProcessingLimiter, ServerConfigurationManager serverConfigManager, @@ -217,6 +220,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _actorObjectService = actorObjectService; _lifetime = lifetime; _fileDbManager = fileDbManager; + _playerPerformanceConfigService = playerPerformanceConfigService; _playerPerformanceService = playerPerformanceService; _pairProcessingLimiter = pairProcessingLimiter; _serverConfigManager = serverConfigManager; @@ -522,11 +526,31 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return GetCurrentPairs().Any(predicate); } - private bool ShouldSkipDownscale() + private bool IsPreferredDirectPair() { return GetCurrentPairs().Any(p => p.IsDirectlyPaired && p.SelfToOtherPermissions.IsSticky()); } + private bool ShouldSkipDownscale() + { + if (!_playerPerformanceConfigService.Current.SkipTextureDownscaleForPreferredPairs) + { + return false; + } + + return IsPreferredDirectPair(); + } + + private bool ShouldSkipDecimation() + { + if (!_playerPerformanceConfigService.Current.SkipModelDecimationForPreferredPairs) + { + return false; + } + + return IsPreferredDirectPair(); + } + private bool IsPaused() { var pairs = GetCurrentPairs(); @@ -1843,6 +1867,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa try { bool skipDownscaleForPair = ShouldSkipDownscale(); + bool skipDecimationForPair = ShouldSkipDecimation(); var user = GetPrimaryUserData(); Dictionary<(string GamePath, string? Hash), string> moddedPaths; List missingReplacements = []; @@ -1881,7 +1906,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa } var handlerForDownload = _charaHandler; - _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); + _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair, skipDecimationForPair).ConfigureAwait(false)); await _pairDownloadTask.ConfigureAwait(false); @@ -1904,7 +1929,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false); } + } + if (!skipDecimationForPair) + { var downloadedModelHashes = toDownloadReplacements .Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))) .Select(static replacement => replacement.Hash) @@ -2388,6 +2416,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new(); bool hasMigrationChanges = false; bool skipDownscaleForPair = ShouldSkipDownscale(); + bool skipDecimationForPair = ShouldSkipDecimation(); try { @@ -2419,16 +2448,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa foreach (var gamePath in item.GamePaths) { var preferredPath = fileCache.ResolvedFilepath; - if (!skipDownscaleForPair) + if (!skipDownscaleForPair && gamePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) { - if (gamePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) - { - preferredPath = _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); - } - else if (gamePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) - { - preferredPath = _modelDecimationService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); - } + preferredPath = _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); + } + else if (!skipDecimationForPair && gamePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) + { + preferredPath = _modelDecimationService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); } outputDict[(gamePath, item.Hash)] = preferredPath; } diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs index 088b115..b4c2c71 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs @@ -1,5 +1,6 @@ using LightlessSync.FileCache; using LightlessSync.Interop.Ipc; +using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Factories; using LightlessSync.Services; using LightlessSync.Services.ActorTracking; @@ -27,6 +28,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory private readonly IServiceProvider _serviceProvider; private readonly IHostApplicationLifetime _lifetime; private readonly FileCacheManager _fileCacheManager; + private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceService _playerPerformanceService; private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ServerConfigurationManager _serverConfigManager; @@ -49,6 +51,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory IFramework framework, IHostApplicationLifetime lifetime, FileCacheManager fileCacheManager, + PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceService playerPerformanceService, PairProcessingLimiter pairProcessingLimiter, ServerConfigurationManager serverConfigManager, @@ -69,6 +72,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory _framework = framework; _lifetime = lifetime; _fileCacheManager = fileCacheManager; + _playerPerformanceConfigService = playerPerformanceConfigService; _playerPerformanceService = playerPerformanceService; _pairProcessingLimiter = pairProcessingLimiter; _serverConfigManager = serverConfigManager; @@ -98,6 +102,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory actorObjectService, _lifetime, _fileCacheManager, + _playerPerformanceConfigService, _playerPerformanceService, _pairProcessingLimiter, _serverConfigManager, diff --git a/LightlessSync/Services/PlayerPerformanceService.cs b/LightlessSync/Services/PlayerPerformanceService.cs index 553e87b..5fa0049 100644 --- a/LightlessSync/Services/PlayerPerformanceService.cs +++ b/LightlessSync/Services/PlayerPerformanceService.cs @@ -129,6 +129,8 @@ public class PlayerPerformanceService .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); + var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions; + foreach (var hash in moddedModelHashes) { var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); @@ -138,7 +140,12 @@ public class PlayerPerformanceService var fileEntry = _fileCacheManager.GetFileCacheByHash(hash); if (fileEntry != null) { - var preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath); + var preferredPath = fileEntry.ResolvedFilepath; + if (!skipDecimation) + { + preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath); + } + if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase)) { var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false); @@ -192,7 +199,9 @@ public class PlayerPerformanceService public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List toDownloadFiles) { var config = _playerPerformanceConfigService.Current; - bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions; + bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs + && pairHandler.IsDirectlyPaired + && pairHandler.HasStickyPermissions; long vramUsage = 0; long effectiveVramUsage = 0; diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index cf64df1..ea0d0e1 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -3590,6 +3590,14 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SameLine(); _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow"))); + var skipPreferredDownscale = textureConfig.SkipTextureDownscaleForPreferredPairs; + if (ImGui.Checkbox("Skip downscale for preferred/direct pairs", ref skipPreferredDownscale)) + { + textureConfig.SkipTextureDownscaleForPreferredPairs = skipPreferredDownscale; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("When enabled, textures for direct pairs with preferred permissions are left untouched."); + if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale) { UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed")); @@ -3649,6 +3657,14 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SameLine(); _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective triangle usage information will not work.", UIColors.Get("LightlessYellow"))); + var skipPreferredDecimation = performanceConfig.SkipModelDecimationForPreferredPairs; + if (ImGui.Checkbox("Skip decimation for preferred/direct pairs", ref skipPreferredDecimation)) + { + performanceConfig.SkipModelDecimationForPreferredPairs = skipPreferredDecimation; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("When enabled, models for direct pairs with preferred permissions are left untouched."); + var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 10_000, 100_000)) diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index e8b0af5..e558e74 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -89,12 +89,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase CurrentOwnerToken = null; } - public async Task DownloadFiles(GameObjectHandler? gameObject, List fileReplacementDto, CancellationToken ct, bool skipDownscale = false) + public async Task DownloadFiles(GameObjectHandler? gameObject, List fileReplacementDto, CancellationToken ct, bool skipDownscale = false, bool skipDecimation = false) { Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles))); try { - await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale).ConfigureAwait(false); + await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale, skipDecimation).ConfigureAwait(false); } catch { @@ -498,7 +498,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase IReadOnlyDictionary rawSizeLookup, string downloadLabel, CancellationToken ct, - bool skipDownscale) + bool skipDownscale, + bool skipDecimation) { SetStatus(downloadStatusKey, DownloadStatus.Decompressing); MarkTransferredFiles(downloadStatusKey, 1); @@ -552,7 +553,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false); - PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale); + PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation); } catch (EndOfStreamException) { @@ -638,7 +639,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase yield return items.GetRange(i, Math.Min(chunkSize, items.Count - i)); } - private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List fileReplacement, CancellationToken ct, bool skipDownscale) + private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List fileReplacement, CancellationToken ct, bool skipDownscale, bool skipDecimation) { var objectName = gameObjectHandler?.Name ?? "Unknown"; @@ -742,13 +743,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase Task batchTask = batchChunks.Length == 0 ? Task.CompletedTask : Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct }, - async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, rawSizeLookup, token, skipDownscale).ConfigureAwait(false)); + async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, rawSizeLookup, token, skipDownscale, skipDecimation).ConfigureAwait(false)); // direct downloads Task directTask = directDownloads.Count == 0 ? Task.CompletedTask : Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct }, - async (d, token) => await ProcessDirectAsync(d, replacementLookup, rawSizeLookup, token, skipDownscale).ConfigureAwait(false)); + async (d, token) => await ProcessDirectAsync(d, replacementLookup, rawSizeLookup, token, skipDownscale, skipDecimation).ConfigureAwait(false)); await Task.WhenAll(batchTask, directTask).ConfigureAwait(false); @@ -761,7 +762,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase Dictionary replacementLookup, IReadOnlyDictionary rawSizeLookup, CancellationToken ct, - bool skipDownscale) + bool skipDownscale, + bool skipDecimation) { var statusKey = chunk.StatusKey; @@ -795,7 +797,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase return; } - await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, fi.Name, ct, skipDownscale).ConfigureAwait(false); + await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, fi.Name, ct, skipDownscale, skipDecimation).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -817,7 +819,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase Dictionary replacementLookup, IReadOnlyDictionary rawSizeLookup, CancellationToken ct, - bool skipDownscale) + bool skipDownscale, + bool skipDecimation) { var progress = CreateInlineProgress(bytes => { @@ -827,7 +830,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl)) { - await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale).ConfigureAwait(false); + await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale, skipDecimation).ConfigureAwait(false); return; } @@ -875,7 +878,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false); - PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale); + PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale, skipDecimation); MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1); SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed); @@ -902,7 +905,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase try { - await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale).ConfigureAwait(false); + await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale, skipDecimation).ConfigureAwait(false); if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads) { @@ -932,7 +935,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase IReadOnlyDictionary rawSizeLookup, IProgress progress, CancellationToken ct, - bool skipDownscale) + bool skipDownscale, + bool skipDecimation) { if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl)) throw new InvalidOperationException("Direct download fallback requested without a direct download URL."); @@ -957,7 +961,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase if (!File.Exists(blockFile)) throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile); - await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale) + await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale, skipDecimation) .ConfigureAwait(false); } finally @@ -986,7 +990,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? []; } - private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale) + private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale, bool skipDecimation) { var fi = new FileInfo(filePath); @@ -1014,7 +1018,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase () => _textureMetadataHelper.DetermineMapKind(gamePath, filePath)); } - if (!skipDownscale && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath)) + if (!skipDecimation && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath)) { _modelDecimationService.ScheduleDecimation(fileHash, filePath, gamePath); } From a824d94ffe55e420f93c4dc61b0405f31ded123f Mon Sep 17 00:00:00 2001 From: azyges Date: Sat, 3 Jan 2026 10:20:07 +0900 Subject: [PATCH 2/4] slight adjustments and fixes --- .../Configurations/LightlessConfig.cs | 1 + .../PlayerData/Pairs/PairHandlerAdapter.cs | 7 +- LightlessSync/UI/DataAnalysisUi.cs | 135 ++++++++++++++++-- LightlessSync/UI/DtrEntry.cs | 13 +- .../UI/Models/TextureFormatSortMode.cs | 8 ++ 5 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 LightlessSync/UI/Models/TextureFormatSortMode.cs diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 737f9ee..c475e9e 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -51,6 +51,7 @@ public class LightlessConfig : ILightlessConfiguration public bool PreferNotesOverNamesForVisible { get; set; } = false; public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical; public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical; + public TextureFormatSortMode TextureFormatSortMode { get; set; } = TextureFormatSortMode.None; public float ProfileDelay { get; set; } = 1.5f; public bool ProfilePopoutRight { get; set; } = false; public bool ProfilesAllowNsfw { get; set; } = false; diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 6d859ac..5e5beeb 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -644,9 +644,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa var dataApplied = !string.IsNullOrEmpty(dataHash) && string.Equals(dataHash, _lastSuccessfulDataHash ?? string.Empty, StringComparison.Ordinal); var needsApply = !dataApplied; - var hasModReplacements = sanitized.FileReplacements.Values.Any(list => list.Count > 0); - var needsModReapply = needsApply && hasModReplacements; - var shouldForceMods = shouldForce || needsModReapply; + var modFilesChanged = PlayerModFilesChanged(sanitized, _cachedData); + var shouldForceMods = shouldForce || modFilesChanged; forceApplyCustomization = forced || needsApply; var suppressForcedModRedraw = !forced && hasMissingCachedFiles && dataApplied; @@ -2192,7 +2191,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) { IsVisible = false; - _forceApplyMods = true; + _forceApplyMods = true; _cachedData = charaData; _pairStateCache.Store(Ident, charaData); _forceFullReapply = true; diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index de6f697..e0bfcb1 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -11,6 +11,7 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.TextureCompression; +using LightlessSync.UI.Models; using LightlessSync.Utils; using Microsoft.Extensions.Logging; using OtterTex; @@ -42,6 +43,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private readonly Progress _conversionProgress = new(); private readonly IpcManager _ipcManager; private readonly UiSharedService _uiSharedService; + private readonly LightlessConfigService _configService; private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly TransientResourceManager _transientResourceManager; private readonly TransientConfigService _transientConfigService; @@ -106,10 +108,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private TextureUsageCategory? _textureCategoryFilter = null; private TextureMapKind? _textureMapFilter = null; private TextureCompressionTarget? _textureTargetFilter = null; + private TextureFormatSortMode _textureFormatSortMode = TextureFormatSortMode.None; public DataAnalysisUi(ILogger logger, LightlessMediator mediator, CharacterAnalyzer characterAnalyzer, IpcManager ipcManager, PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService, + LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager, TransientConfigService transientConfigService, TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper) @@ -118,6 +122,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _characterAnalyzer = characterAnalyzer; _ipcManager = ipcManager; _uiSharedService = uiSharedService; + _configService = configService; _playerPerformanceConfig = playerPerformanceConfig; _transientResourceManager = transientResourceManager; _transientConfigService = transientConfigService; @@ -971,6 +976,13 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase #if DEBUG ResetDebugCompressionModalState(); #endif + var savedFormatSort = _configService.Current.TextureFormatSortMode; + if (!Enum.IsDefined(typeof(TextureFormatSortMode), savedFormatSort)) + { + savedFormatSort = TextureFormatSortMode.None; + } + + SetTextureFormatSortMode(savedFormatSort, persist: false); } protected override void Dispose(bool disposing) @@ -2198,26 +2210,56 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending); ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending); ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableHeadersRow(); + DrawTextureTableHeaderRow(); var targets = _textureCompressionService.SelectableTargets; IEnumerable orderedRows = rows; var sortSpecs = ImGui.TableGetSortSpecs(); + var sizeSortColumn = -1; + var sizeSortDirection = ImGuiSortDirection.Ascending; if (sortSpecs.SpecsCount > 0) { var spec = sortSpecs.Specs[0]; - orderedRows = spec.ColumnIndex switch + if (spec.ColumnIndex is 7 or 8) { - 7 => spec.SortDirection == ImGuiSortDirection.Ascending - ? rows.OrderBy(r => r.OriginalSize) - : rows.OrderByDescending(r => r.OriginalSize), - 8 => spec.SortDirection == ImGuiSortDirection.Ascending - ? rows.OrderBy(r => r.CompressedSize) - : rows.OrderByDescending(r => r.CompressedSize), - _ => rows - }; + sizeSortColumn = spec.ColumnIndex; + sizeSortDirection = spec.SortDirection; + } + } + var hasSizeSort = sizeSortColumn != -1; + var indexedRows = rows.Select((row, idx) => (row, idx)); + + if (_textureFormatSortMode != TextureFormatSortMode.None) + { + bool compressedFirst = _textureFormatSortMode == TextureFormatSortMode.CompressedFirst; + int GroupKey(TextureRow row) => row.IsAlreadyCompressed == compressedFirst ? 0 : 1; + long SizeKey(TextureRow row) => sizeSortColumn == 7 ? row.OriginalSize : row.CompressedSize; + + var ordered = indexedRows.OrderBy(pair => GroupKey(pair.row)); + if (hasSizeSort) + { + ordered = sizeSortDirection == ImGuiSortDirection.Ascending + ? ordered.ThenBy(pair => SizeKey(pair.row)) + : ordered.ThenByDescending(pair => SizeKey(pair.row)); + } + + orderedRows = ordered + .ThenBy(pair => pair.idx) + .Select(pair => pair.row); + } + else if (hasSizeSort) + { + long SizeKey(TextureRow row) => sizeSortColumn == 7 ? row.OriginalSize : row.CompressedSize; + + orderedRows = sizeSortDirection == ImGuiSortDirection.Ascending + ? indexedRows.OrderBy(pair => SizeKey(pair.row)).ThenBy(pair => pair.idx).Select(pair => pair.row) + : indexedRows.OrderByDescending(pair => SizeKey(pair.row)).ThenBy(pair => pair.idx).Select(pair => pair.row); + } + + if (sortSpecs.SpecsCount > 0) + { sortSpecs.SpecsDirty = false; } @@ -2259,6 +2301,79 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } } + + private void DrawTextureTableHeaderRow() + { + ImGui.TableNextRow(ImGuiTableRowFlags.Headers); + + DrawHeaderCell(0, "##select"); + DrawHeaderCell(1, "Texture"); + DrawHeaderCell(2, "Slot"); + DrawHeaderCell(3, "Map"); + DrawFormatHeaderCell(); + DrawHeaderCell(5, "Recommended"); + DrawHeaderCell(6, "Target"); + DrawHeaderCell(7, "Original"); + DrawHeaderCell(8, "Compressed"); + } + + private static void DrawHeaderCell(int columnIndex, string label) + { + ImGui.TableSetColumnIndex(columnIndex); + ImGui.TableHeader(label); + } + + private void DrawFormatHeaderCell() + { + ImGui.TableSetColumnIndex(4); + ImGui.TableHeader(GetFormatHeaderLabel()); + + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + CycleTextureFormatSortMode(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Click to cycle sort: normal, compressed first, uncompressed first."); + } + } + + private string GetFormatHeaderLabel() + => _textureFormatSortMode switch + { + TextureFormatSortMode.CompressedFirst => "Format (C)##formatHeader", + TextureFormatSortMode.UncompressedFirst => "Format (U)##formatHeader", + _ => "Format##formatHeader" + }; + + private void SetTextureFormatSortMode(TextureFormatSortMode mode, bool persist = true) + { + if (_textureFormatSortMode == mode) + { + return; + } + + _textureFormatSortMode = mode; + if (persist) + { + _configService.Current.TextureFormatSortMode = mode; + _configService.Save(); + } + } + + private void CycleTextureFormatSortMode() + { + var nextMode = _textureFormatSortMode switch + { + TextureFormatSortMode.None => TextureFormatSortMode.CompressedFirst, + TextureFormatSortMode.CompressedFirst => TextureFormatSortMode.UncompressedFirst, + _ => TextureFormatSortMode.None + }; + + SetTextureFormatSortMode(nextMode); + } + private void StartTextureConversion() { if (_conversionTask != null && !_conversionTask.IsCompleted) diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index ae94d5e..5aa69eb 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -103,10 +103,19 @@ public sealed class DtrEntry : IDisposable, IHostedService public async Task StopAsync(CancellationToken cancellationToken) { - await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); + _cancellationTokenSource.Cancel(); + + if (_dalamudUtilService.IsOnFrameworkThread) + { + _logger.LogDebug("Skipping Lightfinder DTR wait on framework thread during shutdown."); + _cancellationTokenSource.Dispose(); + return; + } + try { - await _runTask!.ConfigureAwait(false); + if (_runTask != null) + await _runTask.ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/LightlessSync/UI/Models/TextureFormatSortMode.cs b/LightlessSync/UI/Models/TextureFormatSortMode.cs new file mode 100644 index 0000000..165e10d --- /dev/null +++ b/LightlessSync/UI/Models/TextureFormatSortMode.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.UI.Models; + +public enum TextureFormatSortMode +{ + None = 0, + CompressedFirst = 1, + UncompressedFirst = 2 +} From b6b9c81a5758681f47234eb9ead2f29ad7a8948f Mon Sep 17 00:00:00 2001 From: azyges Date: Sat, 3 Jan 2026 13:53:55 +0900 Subject: [PATCH 3/4] tighten the check --- .../ActorTracking/ActorObjectService.cs | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/LightlessSync/Services/ActorTracking/ActorObjectService.cs b/LightlessSync/Services/ActorTracking/ActorObjectService.cs index cf22e66..e443496 100644 --- a/LightlessSync/Services/ActorTracking/ActorObjectService.cs +++ b/LightlessSync/Services/ActorTracking/ActorObjectService.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.Interop; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using LightlessSync.PlayerData.Handlers; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -16,7 +17,7 @@ using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind; namespace LightlessSync.Services.ActorTracking; -public sealed class ActorObjectService : IHostedService, IDisposable +public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorSubscriber { public readonly record struct ActorDescriptor( string Name, @@ -36,6 +37,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable private readonly IClientState _clientState; private readonly ICondition _condition; private readonly LightlessMediator _mediator; + private readonly object _playerRelatedHandlerLock = new(); + private readonly HashSet _playerRelatedHandlers = []; private readonly ConcurrentDictionary _activePlayers = new(); private readonly ConcurrentDictionary _gposePlayers = new(); @@ -71,6 +74,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable _clientState = clientState; _condition = condition; _mediator = mediator; + + _mediator.Subscribe(this, (msg) => + { + if (!msg.OwnedObject) return; + lock (_playerRelatedHandlerLock) + { + _playerRelatedHandlers.Add(msg.GameObjectHandler); + } + RefreshTrackedActors(force: true); + }); + _mediator.Subscribe(this, (msg) => + { + if (!msg.OwnedObject) return; + lock (_playerRelatedHandlerLock) + { + _playerRelatedHandlers.Remove(msg.GameObjectHandler); + } + RefreshTrackedActors(force: true); + }); } private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; @@ -84,6 +106,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable public IReadOnlyList PlayerDescriptors => Snapshot.PlayerDescriptors; public IReadOnlyList OwnedDescriptors => Snapshot.OwnedDescriptors; public IReadOnlyList GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors; + public LightlessMediator Mediator => _mediator; public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor); public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor) @@ -324,6 +347,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable _actorsByHash.Clear(); _actorsByName.Clear(); _pendingHashResolutions.Clear(); + _mediator.UnsubscribeAll(this); + lock (_playerRelatedHandlerLock) + { + _playerRelatedHandlers.Clear(); + } Volatile.Write(ref _snapshot, ActorSnapshot.Empty); Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty); return Task.CompletedTask; @@ -500,7 +528,9 @@ public sealed class ActorObjectService : IHostedService, IDisposable if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion) { var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId); - if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount) + if (expectedMinionOrMount != nint.Zero + && (nint)gameObject == expectedMinionOrMount + && IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount)) { var resolvedOwner = ownerId != 0 ? ownerId : localEntityId; return (LightlessObjectKind.MinionOrMount, resolvedOwner); @@ -514,16 +544,37 @@ public sealed class ActorObjectService : IHostedService, IDisposable return (null, ownerId); var expectedPet = GetPetAddress(localPlayerAddress, localEntityId); - if (expectedPet != nint.Zero && (nint)gameObject == expectedPet) + if (expectedPet != nint.Zero + && (nint)gameObject == expectedPet + && IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet)) return (LightlessObjectKind.Pet, ownerId); var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId); - if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion) + if (expectedCompanion != nint.Zero + && (nint)gameObject == expectedCompanion + && IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion)) return (LightlessObjectKind.Companion, ownerId); return (null, ownerId); } + private bool IsPlayerRelatedOwnedAddress(nint address, LightlessObjectKind expectedKind) + { + if (address == nint.Zero) + return false; + + lock (_playerRelatedHandlerLock) + { + foreach (var handler in _playerRelatedHandlers) + { + if (handler.Address == address && handler.ObjectKind == expectedKind) + return true; + } + } + + return false; + } + private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId) { if (localPlayerAddress == nint.Zero) @@ -531,20 +582,20 @@ public sealed class ActorObjectService : IHostedService, IDisposable var playerObject = (GameObject*)localPlayerAddress; var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1); + if (ownerEntityId == 0) + return nint.Zero; + if (candidateAddress != nint.Zero) { var candidate = (GameObject*)candidateAddress; var candidateKind = (DalamudObjectKind)candidate->ObjectKind; if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion) { - if (ownerEntityId == 0 || ResolveOwnerId(candidate) == ownerEntityId) + if (ResolveOwnerId(candidate) == ownerEntityId) return candidateAddress; } } - if (ownerEntityId == 0) - return candidateAddress; - foreach (var obj in _objectTable) { if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress) @@ -558,7 +609,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable return obj.Address; } - return candidateAddress; + return nint.Zero; } private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId) @@ -1029,6 +1080,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable public void Dispose() { DisposeHooks(); + _mediator.UnsubscribeAll(this); GC.SuppressFinalize(this); } From 4da2548e0323bb3b3e97a4b90a7b83e105671277 Mon Sep 17 00:00:00 2001 From: azyges Date: Mon, 5 Jan 2026 07:48:14 +0900 Subject: [PATCH 4/4] just misc --- .../Interop/BlockedCharacterHandler.cs | 24 ++++++-- .../Configurations/PlayerPerformanceConfig.cs | 2 +- LightlessSync/Plugin.cs | 4 +- LightlessSync/Services/DalamudUtilService.cs | 2 +- .../Services/ModelDecimation/MdlDecimator.cs | 27 ++++++++- LightlessSync/UI/SettingsUi.cs | 56 ++++++++++++++++--- LightlessSync/UI/ZoneChatUi.cs | 14 ++++- 7 files changed, 107 insertions(+), 22 deletions(-) diff --git a/LightlessSync/Interop/BlockedCharacterHandler.cs b/LightlessSync/Interop/BlockedCharacterHandler.cs index 0ad3c80..675bf3b 100644 --- a/LightlessSync/Interop/BlockedCharacterHandler.cs +++ b/LightlessSync/Interop/BlockedCharacterHandler.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin.Services; +using Dalamud.Game.ClientState.Objects.SubKinds; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.UI.Info; using Microsoft.Extensions.Logging; @@ -11,24 +12,35 @@ public unsafe class BlockedCharacterHandler private readonly Dictionary _blockedCharacterCache = new(); private readonly ILogger _logger; + private readonly IObjectTable _objectTable; - public BlockedCharacterHandler(ILogger logger, IGameInteropProvider gameInteropProvider) + public BlockedCharacterHandler(ILogger logger, IGameInteropProvider gameInteropProvider, IObjectTable objectTable) { gameInteropProvider.InitializeFromAttributes(this); _logger = logger; + _objectTable = objectTable; } - private static CharaData GetIdsFromPlayerPointer(nint ptr) + private CharaData? TryGetIdsFromPlayerPointer(nint ptr, ushort objectIndex) { - if (ptr == nint.Zero) return new(0, 0); - var castChar = ((BattleChara*)ptr); + if (ptr == nint.Zero || objectIndex >= 200) + return null; + + var obj = _objectTable[objectIndex]; + if (obj is not IPlayerCharacter player || player.Address != ptr) + return null; + + var castChar = (BattleChara*)player.Address; return new(castChar->Character.AccountId, castChar->Character.ContentId); } - public bool IsCharacterBlocked(nint ptr, out bool firstTime) + public bool IsCharacterBlocked(nint ptr, ushort objectIndex, out bool firstTime) { firstTime = false; - var combined = GetIdsFromPlayerPointer(ptr); + var combined = TryGetIdsFromPlayerPointer(ptr, objectIndex); + if (combined == null) + return false; + if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked)) return isBlocked; diff --git a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs index 5cdfd4e..462a63f 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs @@ -24,7 +24,7 @@ public class PlayerPerformanceConfig : ILightlessConfiguration public bool KeepOriginalTextureFiles { get; set; } = false; public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true; public bool EnableModelDecimation { get; set; } = false; - public int ModelDecimationTriangleThreshold { get; set; } = 50_000; + public int ModelDecimationTriangleThreshold { get; set; } = 20_000; public double ModelDecimationTargetRatio { get; set; } = 0.8; public bool KeepOriginalModelFiles { get; set; } = true; public bool SkipModelDecimationForPreferredPairs { get; set; } = true; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index fafd1c7..b760070 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -123,6 +123,7 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -180,7 +181,8 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(sp => new BlockedCharacterHandler( sp.GetRequiredService>(), - gameInteropProvider)); + gameInteropProvider, + objectTable)); services.AddSingleton(sp => new IpcProvider( sp.GetRequiredService>(), diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index b278667..28345d2 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -1010,7 +1010,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber if (actor.ObjectIndex >= 200) continue; - if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, out bool firstTime) && firstTime) + if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, actor.ObjectIndex, out bool firstTime) && firstTime) { _logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X")); continue; diff --git a/LightlessSync/Services/ModelDecimation/MdlDecimator.cs b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs index 36feb4a..a7af13f 100644 --- a/LightlessSync/Services/ModelDecimation/MdlDecimator.cs +++ b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs @@ -1,6 +1,7 @@ using Lumina.Data.Parsing; using Lumina.Extensions; using MeshDecimator; +using MeshDecimator.Algorithms; using MeshDecimator.Math; using Microsoft.Extensions.Logging; using Penumbra.GameData.Files.ModelStructs; @@ -94,6 +95,8 @@ internal static class MdlDecimator var newVertexBuffer = new List(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0); var newIndexBuffer = new List(mdl.IndexBufferSize[lodIndex] > 0 ? (int)(mdl.IndexBufferSize[lodIndex] / sizeof(ushort)) : 0); var subMeshCursor = 0; + DecimationAlgorithm? decimationAlgorithm = null; + int? decimationUvChannelCount = null; for (var meshIndex = 0; meshIndex < meshes.Length; meshIndex++) { @@ -119,6 +122,8 @@ internal static class MdlDecimator out vertexStreams, out indices, out decimated, + ref decimationAlgorithm, + ref decimationUvChannelCount, logger)) { updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase); @@ -309,6 +314,8 @@ internal static class MdlDecimator out byte[][] vertexStreams, out int[] indices, out bool decimated, + ref DecimationAlgorithm? decimationAlgorithm, + ref int? decimationUvChannelCount, MsLogger logger) { updatedMesh = mesh; @@ -352,8 +359,7 @@ internal static class MdlDecimator } var meshDecimatorMesh = BuildMesh(decoded, subMeshIndices); - var algorithm = MeshDecimation.CreateAlgorithm(Algorithm.Default); - algorithm.Logger = logger; + var algorithm = GetOrCreateAlgorithm(format, ref decimationAlgorithm, ref decimationUvChannelCount, logger); algorithm.Initialize(meshDecimatorMesh); algorithm.DecimateMesh(targetTriangles); var decimatedMesh = algorithm.ToMesh(); @@ -374,6 +380,23 @@ internal static class MdlDecimator return true; } + private static DecimationAlgorithm GetOrCreateAlgorithm( + VertexFormat format, + ref DecimationAlgorithm? decimationAlgorithm, + ref int? decimationUvChannelCount, + MsLogger logger) + { + var uvChannelCount = format.UvChannelCount; + if (decimationAlgorithm == null || decimationUvChannelCount != uvChannelCount) + { + decimationAlgorithm = MeshDecimation.CreateAlgorithm(Algorithm.Default); + decimationAlgorithm.Logger = logger; + decimationUvChannelCount = uvChannelCount; + } + + return decimationAlgorithm; + } + private static Mesh BuildMesh(DecodedMeshData decoded, int[][] subMeshIndices) { var mesh = new Mesh(decoded.Positions, subMeshIndices); diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index ea0d0e1..aa5db82 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -3635,8 +3635,31 @@ public class SettingsUi : WindowMediatorSubscriberBase new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true), new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances.")); + _uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), - new SeStringUtils.RichTextEntry("this shit is placeholder still owo")); + new SeStringUtils.RichTextEntry("This feature is encouraged to help "), + new SeStringUtils.RichTextEntry("lower-end systems with limited VRAM", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(" and for use in "), + new SeStringUtils.RichTextEntry("performance-critical scenarios", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("Runtime decimation "), + new SeStringUtils.RichTextEntry("MAY", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" cause higher load on the system when processing downloads.")); + + _uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true)); + + ImGui.Dummy(new Vector2(15)); + + _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessGreen"), + new SeStringUtils.RichTextEntry("If a mesh exceeds the "), + new SeStringUtils.RichTextEntry("triangle threshold", UIColors.Get("LightlessGreen"), true), + new SeStringUtils.RichTextEntry(", it will be decimated automatically to the set "), + new SeStringUtils.RichTextEntry("target triangle ratio", UIColors.Get("LightlessGreen"), true), + new SeStringUtils.RichTextEntry(". This will reduce quality of the mesh or may break it's intended structure.")); + var performanceConfig = _playerPerformanceConfigService.Current; var enableDecimation = performanceConfig.EnableModelDecimation; @@ -3667,9 +3690,9 @@ public class SettingsUi : WindowMediatorSubscriberBase var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 10_000, 100_000)) + if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 8_000, 100_000)) { - performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 10_000, 100_000); + performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 8_000, 100_000); _playerPerformanceConfigService.Save(); } ImGui.SameLine(); @@ -3677,7 +3700,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText($"Models below this triangle count are left untouched.{UiSharedService.TooltipSeparator}Default: 50,000"); var targetPercent = (float)(performanceConfig.ModelDecimationTargetRatio * 100.0); - var clampedPercent = Math.Clamp(targetPercent, 70f, 99f); + var clampedPercent = Math.Clamp(targetPercent, 60f, 99f); if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon) { performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0; @@ -3685,17 +3708,30 @@ public class SettingsUi : WindowMediatorSubscriberBase targetPercent = clampedPercent; } ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - if (ImGui.SliderFloat("Target triangle ratio", ref targetPercent, 70f, 99f, "%.0f%%")) + if (ImGui.SliderFloat("Target triangle ratio", ref targetPercent, 60f, 99f, "%.0f%%")) { - performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.7f, 0.99f); + performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.6f, 0.99f); _playerPerformanceConfigService.Save(); } - _uiShared.DrawHelpText($"Target ratio relative to original triangle count (70% keeps 70% of triangles).{UiSharedService.TooltipSeparator}Default: 70%"); + _uiShared.DrawHelpText($"Target ratio relative to original triangle count (80% keeps 80% of triangles).{UiSharedService.TooltipSeparator}Default: 80%"); - ImGui.Dummy(new Vector2(5)); + ImGui.Dummy(new Vector2(15)); ImGui.TextUnformatted("Decimation targets"); _uiShared.DrawHelpText("Hair mods are always excluded from decimation."); + _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessGreen"), + new SeStringUtils.RichTextEntry("Automatic decimation will only target the selected "), + new SeStringUtils.RichTextEntry("decimation targets", UIColors.Get("LightlessGreen"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("It is advised to not decimate any body related meshes which includes: "), + new SeStringUtils.RichTextEntry("facial mods + sculpts, chest, legs, hands and feet", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("Remember, automatic decimation is not perfect and can cause meshes to be ruined, especially hair mods.", UIColors.Get("DimRed"), true)); + var allowBody = performanceConfig.ModelDecimationAllowBody; if (ImGui.Checkbox("Body", ref allowBody)) { @@ -3731,6 +3767,10 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Save(); } + ImGui.Dummy(new Vector2(5)); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessGrey"), 3f); + ImGui.Dummy(new Vector2(5)); DrawTriangleDecimationCounters(); ImGui.Dummy(new Vector2(5)); diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index cb6dae8..a03ceab 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -205,10 +205,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private void ApplyUiVisibilitySettings() { - var config = _chatConfigService.Current; _uiBuilder.DisableUserUiHide = true; - _uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes; - _uiBuilder.DisableGposeUiHide = config.ShowInGpose; + _uiBuilder.DisableCutsceneUiHide = true; } private bool ShouldHide() @@ -220,6 +218,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase return true; } + if (!config.ShowInGpose && _dalamudUtilService.IsInGpose) + { + return true; + } + + if (!config.ShowInCutscenes && _dalamudUtilService.IsInCutscene) + { + return true; + } + if (config.HideInCombat && _dalamudUtilService.IsInCombat) { return true;