Merge abel stuff

This commit is contained in:
cake
2026-01-05 01:41:03 +01:00
17 changed files with 431 additions and 69 deletions

View File

@@ -1,4 +1,5 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Game.ClientState.Objects.SubKinds;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -11,24 +12,35 @@ public unsafe class BlockedCharacterHandler
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new(); private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
private readonly ILogger<BlockedCharacterHandler> _logger; private readonly ILogger<BlockedCharacterHandler> _logger;
private readonly IObjectTable _objectTable;
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider) public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider, IObjectTable objectTable)
{ {
gameInteropProvider.InitializeFromAttributes(this); gameInteropProvider.InitializeFromAttributes(this);
_logger = logger; _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); if (ptr == nint.Zero || objectIndex >= 200)
var castChar = ((BattleChara*)ptr); 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); 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; firstTime = false;
var combined = GetIdsFromPlayerPointer(ptr); var combined = TryGetIdsFromPlayerPointer(ptr, objectIndex);
if (combined == null)
return false;
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked)) if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
return isBlocked; return isBlocked;

View File

@@ -52,6 +52,7 @@ public class LightlessConfig : ILightlessConfiguration
public bool PreferNotesOverNamesForVisible { get; set; } = false; public bool PreferNotesOverNamesForVisible { get; set; } = false;
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical; public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical; public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
public TextureFormatSortMode TextureFormatSortMode { get; set; } = TextureFormatSortMode.None;
public float ProfileDelay { get; set; } = 1.5f; public float ProfileDelay { get; set; } = 1.5f;
public bool ProfilePopoutRight { get; set; } = false; public bool ProfilePopoutRight { get; set; } = false;
public bool ProfilesAllowNsfw { get; set; } = false; public bool ProfilesAllowNsfw { get; set; } = false;

View File

@@ -22,10 +22,12 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
public int TextureDownscaleMaxDimension { get; set; } = 2048; public int TextureDownscaleMaxDimension { get; set; } = 2048;
public bool OnlyDownscaleUncompressedTextures { get; set; } = true; public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
public bool KeepOriginalTextureFiles { get; set; } = false; public bool KeepOriginalTextureFiles { get; set; } = false;
public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true;
public bool EnableModelDecimation { get; set; } = false; 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 double ModelDecimationTargetRatio { get; set; } = 0.8;
public bool KeepOriginalModelFiles { get; set; } = true; public bool KeepOriginalModelFiles { get; set; } = true;
public bool SkipModelDecimationForPreferredPairs { get; set; } = true;
public bool ModelDecimationAllowBody { get; set; } = false; public bool ModelDecimationAllowBody { get; set; } = false;
public bool ModelDecimationAllowFaceHead { get; set; } = false; public bool ModelDecimationAllowFaceHead { get; set; } = false;
public bool ModelDecimationAllowTail { get; set; } = false; public bool ModelDecimationAllowTail { get; set; } = false;

View File

@@ -169,6 +169,11 @@ public class PlayerDataFactory
using var cts = new CancellationTokenSource(_hardBuildTimeout); using var cts = new CancellationTokenSource(_hardBuildTimeout);
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false); var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
fragment.FileReplacements =
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet();
var allowedExtensions = CacheMonitor.AllowedFileExtensions;
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !allowedExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow); _characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
PruneCharacterCacheIfNeeded(); PruneCharacterCacheIfNeeded();
@@ -213,6 +218,11 @@ public class PlayerDataFactory
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct) await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct)
.ConfigureAwait(false); .ConfigureAwait(false);
// get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind);
var resolvedTransientPaths = transientPaths.Count == 0
? new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly()
: await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false)) if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false))
@@ -622,8 +632,15 @@ public class PlayerDataFactory
{ {
var forwardPaths = forwardResolve.ToArray(); var forwardPaths = forwardResolve.ToArray();
var reversePaths = reverseResolve.ToArray(); var reversePaths = reverseResolve.ToArray();
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal); if (forwardPaths.Length == 0 && reversePaths.Length == 0)
{
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
}
var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty<string>() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
var reversePathsLower = reversePaths.Length == 0 ? Array.Empty<string>() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
if (handler.ObjectKind != ObjectKind.Player) if (handler.ObjectKind != ObjectKind.Player)
{ {
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() => var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
@@ -654,12 +671,19 @@ public class PlayerDataFactory
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
list.Add(forwardPaths[i].ToLowerInvariant()); list.Add(forwardPaths[i].ToLowerInvariant());
else else
{
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
} }
}
for (int i = 0; i < reversePaths.Length; 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)) if (resolvedPaths.TryGetValue(filePath, out var list))
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant())); list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
else else
@@ -683,7 +707,12 @@ public class PlayerDataFactory
for (int i = 0; i < reversePaths.Length; 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)) if (resolvedPaths.TryGetValue(filePath, out var list))
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant())); list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
else else

View File

@@ -6,6 +6,7 @@ using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
@@ -39,6 +40,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly ActorObjectService _actorObjectService; private readonly ActorObjectService _actorObjectService;
private readonly FileDownloadManager _downloadManager; private readonly FileDownloadManager _downloadManager;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
@@ -204,6 +206,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
ActorObjectService actorObjectService, ActorObjectService actorObjectService,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager, FileCacheManager fileDbManager,
PlayerPerformanceConfigService playerPerformanceConfigService,
PlayerPerformanceService playerPerformanceService, PlayerPerformanceService playerPerformanceService,
PairProcessingLimiter pairProcessingLimiter, PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager, ServerConfigurationManager serverConfigManager,
@@ -226,6 +229,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_actorObjectService = actorObjectService; _actorObjectService = actorObjectService;
_lifetime = lifetime; _lifetime = lifetime;
_fileDbManager = fileDbManager; _fileDbManager = fileDbManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_playerPerformanceService = playerPerformanceService; _playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
@@ -533,11 +537,31 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return GetCurrentPairs().Any(predicate); return GetCurrentPairs().Any(predicate);
} }
private bool ShouldSkipDownscale() private bool IsPreferredDirectPair()
{ {
return GetCurrentPairs().Any(p => p.IsDirectlyPaired && p.SelfToOtherPermissions.IsSticky()); 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() private bool IsPaused()
{ {
var pairs = GetCurrentPairs(); var pairs = GetCurrentPairs();
@@ -631,9 +655,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
var dataApplied = !string.IsNullOrEmpty(dataHash) var dataApplied = !string.IsNullOrEmpty(dataHash)
&& string.Equals(dataHash, _lastSuccessfulDataHash ?? string.Empty, StringComparison.Ordinal); && string.Equals(dataHash, _lastSuccessfulDataHash ?? string.Empty, StringComparison.Ordinal);
var needsApply = !dataApplied; var needsApply = !dataApplied;
var hasModReplacements = sanitized.FileReplacements.Values.Any(list => list.Count > 0); var modFilesChanged = PlayerModFilesChanged(sanitized, _cachedData);
var needsModReapply = needsApply && hasModReplacements; var shouldForceMods = shouldForce || modFilesChanged;
var shouldForceMods = shouldForce || needsModReapply;
forceApplyCustomization = forced || needsApply; forceApplyCustomization = forced || needsApply;
var suppressForcedModRedraw = !forced && hasMissingCachedFiles && dataApplied; var suppressForcedModRedraw = !forced && hasMissingCachedFiles && dataApplied;
@@ -1854,6 +1877,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
try try
{ {
bool skipDownscaleForPair = ShouldSkipDownscale(); bool skipDownscaleForPair = ShouldSkipDownscale();
bool skipDecimationForPair = ShouldSkipDecimation();
var user = GetPrimaryUserData(); var user = GetPrimaryUserData();
Dictionary<(string GamePath, string? Hash), string> moddedPaths; Dictionary<(string GamePath, string? Hash), string> moddedPaths;
List<FileReplacementData> missingReplacements = []; List<FileReplacementData> missingReplacements = [];
@@ -1892,7 +1916,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
var handlerForDownload = _charaHandler; 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); await _pairDownloadTask.ConfigureAwait(false);
@@ -1915,7 +1939,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false); await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false);
} }
}
if (!skipDecimationForPair)
{
var downloadedModelHashes = toDownloadReplacements var downloadedModelHashes = toDownloadReplacements
.Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))) .Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)))
.Select(static replacement => replacement.Hash) .Select(static replacement => replacement.Hash)
@@ -2424,6 +2451,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new(); ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
bool hasMigrationChanges = false; bool hasMigrationChanges = false;
bool skipDownscaleForPair = ShouldSkipDownscale(); bool skipDownscaleForPair = ShouldSkipDownscale();
bool skipDecimationForPair = ShouldSkipDecimation();
try try
{ {

View File

@@ -28,6 +28,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly PlayerPerformanceService _playerPerformanceService; private readonly PlayerPerformanceService _playerPerformanceService;
private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ServerConfigurationManager _serverConfigManager; private readonly ServerConfigurationManager _serverConfigManager;
@@ -52,6 +53,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
IFramework framework, IFramework framework,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime,
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
PlayerPerformanceConfigService playerPerformanceConfigService,
PlayerPerformanceService playerPerformanceService, PlayerPerformanceService playerPerformanceService,
PairProcessingLimiter pairProcessingLimiter, PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager, ServerConfigurationManager serverConfigManager,
@@ -74,6 +76,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_framework = framework; _framework = framework;
_lifetime = lifetime; _lifetime = lifetime;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_playerPerformanceService = playerPerformanceService; _playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
@@ -105,6 +108,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
actorObjectService, actorObjectService,
_lifetime, _lifetime,
_fileCacheManager, _fileCacheManager,
_playerPerformanceConfigService,
_playerPerformanceService, _playerPerformanceService,
_pairProcessingLimiter, _pairProcessingLimiter,
_serverConfigManager, _serverConfigManager,

View File

@@ -123,6 +123,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<HubFactory>(); services.AddSingleton<HubFactory>();
services.AddSingleton<FileUploadManager>(); services.AddSingleton<FileUploadManager>();
services.AddSingleton<FileTransferOrchestrator>(); services.AddSingleton<FileTransferOrchestrator>();
services.AddSingleton<FileDownloadDeduplicator>();
services.AddSingleton<LightlessPlugin>(); services.AddSingleton<LightlessPlugin>();
services.AddSingleton<LightlessProfileManager>(); services.AddSingleton<LightlessProfileManager>();
services.AddSingleton<TextureCompressionService>(); services.AddSingleton<TextureCompressionService>();
@@ -180,7 +181,8 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton(sp => new BlockedCharacterHandler( services.AddSingleton(sp => new BlockedCharacterHandler(
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(), sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
gameInteropProvider)); gameInteropProvider,
objectTable));
services.AddSingleton(sp => new IpcProvider( services.AddSingleton(sp => new IpcProvider(
sp.GetRequiredService<ILogger<IpcProvider>>(), sp.GetRequiredService<ILogger<IpcProvider>>(),

View File

@@ -6,6 +6,7 @@ using FFXIVClientStructs.Interop;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -16,7 +17,7 @@ using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.Services.ActorTracking; namespace LightlessSync.Services.ActorTracking;
public sealed class ActorObjectService : IHostedService, IDisposable public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorSubscriber
{ {
public readonly record struct ActorDescriptor( public readonly record struct ActorDescriptor(
string Name, string Name,
@@ -36,6 +37,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
private readonly object _playerRelatedHandlerLock = new();
private readonly HashSet<GameObjectHandler> _playerRelatedHandlers = [];
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new(); private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new(); private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
@@ -71,6 +74,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_clientState = clientState; _clientState = clientState;
_condition = condition; _condition = condition;
_mediator = mediator; _mediator = mediator;
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
{
if (!msg.OwnedObject) return;
lock (_playerRelatedHandlerLock)
{
_playerRelatedHandlers.Add(msg.GameObjectHandler);
}
RefreshTrackedActors(force: true);
});
_mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
{
if (!msg.OwnedObject) return;
lock (_playerRelatedHandlerLock)
{
_playerRelatedHandlers.Remove(msg.GameObjectHandler);
}
RefreshTrackedActors(force: true);
});
} }
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
@@ -84,6 +106,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors; public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors; public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors; public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
public LightlessMediator Mediator => _mediator;
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor); public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor) public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
@@ -324,6 +347,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_actorsByHash.Clear(); _actorsByHash.Clear();
_actorsByName.Clear(); _actorsByName.Clear();
_pendingHashResolutions.Clear(); _pendingHashResolutions.Clear();
_mediator.UnsubscribeAll(this);
lock (_playerRelatedHandlerLock)
{
_playerRelatedHandlers.Clear();
}
Volatile.Write(ref _snapshot, ActorSnapshot.Empty); Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty); Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
return Task.CompletedTask; return Task.CompletedTask;
@@ -500,7 +528,9 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion) if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
{ {
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId); 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; var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
return (LightlessObjectKind.MinionOrMount, resolvedOwner); return (LightlessObjectKind.MinionOrMount, resolvedOwner);
@@ -514,16 +544,37 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return (null, ownerId); return (null, ownerId);
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId); 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); return (LightlessObjectKind.Pet, ownerId);
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId); 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 (LightlessObjectKind.Companion, ownerId);
return (null, 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) private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
{ {
if (localPlayerAddress == nint.Zero) if (localPlayerAddress == nint.Zero)
@@ -531,20 +582,20 @@ public sealed class ActorObjectService : IHostedService, IDisposable
var playerObject = (GameObject*)localPlayerAddress; var playerObject = (GameObject*)localPlayerAddress;
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1); var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
if (ownerEntityId == 0)
return nint.Zero;
if (candidateAddress != nint.Zero) if (candidateAddress != nint.Zero)
{ {
var candidate = (GameObject*)candidateAddress; var candidate = (GameObject*)candidateAddress;
var candidateKind = (DalamudObjectKind)candidate->ObjectKind; var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion) if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
{ {
if (ownerEntityId == 0 || ResolveOwnerId(candidate) == ownerEntityId) if (ResolveOwnerId(candidate) == ownerEntityId)
return candidateAddress; return candidateAddress;
} }
} }
if (ownerEntityId == 0)
return candidateAddress;
foreach (var obj in _objectTable) foreach (var obj in _objectTable)
{ {
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress) 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 obj.Address;
} }
return candidateAddress; return nint.Zero;
} }
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId) private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
@@ -1029,6 +1080,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
public void Dispose() public void Dispose()
{ {
DisposeHooks(); DisposeHooks();
_mediator.UnsubscribeAll(this);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@@ -1022,7 +1022,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
if (actor.ObjectIndex >= 200) if (actor.ObjectIndex >= 200)
continue; 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")); _logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
continue; continue;

View File

@@ -1,6 +1,7 @@
using Lumina.Data.Parsing; using Lumina.Data.Parsing;
using Lumina.Extensions; using Lumina.Extensions;
using MeshDecimator; using MeshDecimator;
using MeshDecimator.Algorithms;
using MeshDecimator.Math; using MeshDecimator.Math;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Penumbra.GameData.Files.ModelStructs; using Penumbra.GameData.Files.ModelStructs;
@@ -94,6 +95,8 @@ internal static class MdlDecimator
var newVertexBuffer = new List<byte>(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0); var newVertexBuffer = new List<byte>(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0);
var newIndexBuffer = new List<ushort>(mdl.IndexBufferSize[lodIndex] > 0 ? (int)(mdl.IndexBufferSize[lodIndex] / sizeof(ushort)) : 0); var newIndexBuffer = new List<ushort>(mdl.IndexBufferSize[lodIndex] > 0 ? (int)(mdl.IndexBufferSize[lodIndex] / sizeof(ushort)) : 0);
var subMeshCursor = 0; var subMeshCursor = 0;
DecimationAlgorithm? decimationAlgorithm = null;
int? decimationUvChannelCount = null;
for (var meshIndex = 0; meshIndex < meshes.Length; meshIndex++) for (var meshIndex = 0; meshIndex < meshes.Length; meshIndex++)
{ {
@@ -119,6 +122,8 @@ internal static class MdlDecimator
out vertexStreams, out vertexStreams,
out indices, out indices,
out decimated, out decimated,
ref decimationAlgorithm,
ref decimationUvChannelCount,
logger)) logger))
{ {
updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase); updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase);
@@ -309,6 +314,8 @@ internal static class MdlDecimator
out byte[][] vertexStreams, out byte[][] vertexStreams,
out int[] indices, out int[] indices,
out bool decimated, out bool decimated,
ref DecimationAlgorithm? decimationAlgorithm,
ref int? decimationUvChannelCount,
MsLogger logger) MsLogger logger)
{ {
updatedMesh = mesh; updatedMesh = mesh;
@@ -352,8 +359,7 @@ internal static class MdlDecimator
} }
var meshDecimatorMesh = BuildMesh(decoded, subMeshIndices); var meshDecimatorMesh = BuildMesh(decoded, subMeshIndices);
var algorithm = MeshDecimation.CreateAlgorithm(Algorithm.Default); var algorithm = GetOrCreateAlgorithm(format, ref decimationAlgorithm, ref decimationUvChannelCount, logger);
algorithm.Logger = logger;
algorithm.Initialize(meshDecimatorMesh); algorithm.Initialize(meshDecimatorMesh);
algorithm.DecimateMesh(targetTriangles); algorithm.DecimateMesh(targetTriangles);
var decimatedMesh = algorithm.ToMesh(); var decimatedMesh = algorithm.ToMesh();
@@ -374,6 +380,23 @@ internal static class MdlDecimator
return true; 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) private static Mesh BuildMesh(DecodedMeshData decoded, int[][] subMeshIndices)
{ {
var mesh = new Mesh(decoded.Positions, subMeshIndices); var mesh = new Mesh(decoded.Positions, subMeshIndices);

View File

@@ -129,6 +129,8 @@ public class PlayerPerformanceService
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
foreach (var hash in moddedModelHashes) foreach (var hash in moddedModelHashes)
{ {
var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
@@ -138,7 +140,12 @@ public class PlayerPerformanceService
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash); var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
if (fileEntry != null) 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)) if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase))
{ {
var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false); var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false);
@@ -192,7 +199,9 @@ public class PlayerPerformanceService
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles) public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
{ {
var config = _playerPerformanceConfigService.Current; var config = _playerPerformanceConfigService.Current;
bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions; bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs
&& pairHandler.IsDirectlyPaired
&& pairHandler.HasStickyPermissions;
long vramUsage = 0; long vramUsage = 0;
long effectiveVramUsage = 0; long effectiveVramUsage = 0;

View File

@@ -11,6 +11,7 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.TextureCompression; using LightlessSync.Services.TextureCompression;
using LightlessSync.UI.Models;
using LightlessSync.Utils; using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OtterTex; using OtterTex;
@@ -42,6 +43,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private readonly Progress<TextureConversionProgress> _conversionProgress = new(); private readonly Progress<TextureConversionProgress> _conversionProgress = new();
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly LightlessConfigService _configService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly TransientResourceManager _transientResourceManager; private readonly TransientResourceManager _transientResourceManager;
private readonly TransientConfigService _transientConfigService; private readonly TransientConfigService _transientConfigService;
@@ -106,10 +108,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private TextureUsageCategory? _textureCategoryFilter = null; private TextureUsageCategory? _textureCategoryFilter = null;
private TextureMapKind? _textureMapFilter = null; private TextureMapKind? _textureMapFilter = null;
private TextureCompressionTarget? _textureTargetFilter = null; private TextureCompressionTarget? _textureTargetFilter = null;
private TextureFormatSortMode _textureFormatSortMode = TextureFormatSortMode.None;
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, LightlessMediator mediator, public DataAnalysisUi(ILogger<DataAnalysisUi> logger, LightlessMediator mediator,
CharacterAnalyzer characterAnalyzer, IpcManager ipcManager, CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService, PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService,
LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager, PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
TransientConfigService transientConfigService, TextureCompressionService textureCompressionService, TransientConfigService transientConfigService, TextureCompressionService textureCompressionService,
TextureMetadataHelper textureMetadataHelper) TextureMetadataHelper textureMetadataHelper)
@@ -118,6 +122,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_characterAnalyzer = characterAnalyzer; _characterAnalyzer = characterAnalyzer;
_ipcManager = ipcManager; _ipcManager = ipcManager;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_configService = configService;
_playerPerformanceConfig = playerPerformanceConfig; _playerPerformanceConfig = playerPerformanceConfig;
_transientResourceManager = transientResourceManager; _transientResourceManager = transientResourceManager;
_transientConfigService = transientConfigService; _transientConfigService = transientConfigService;
@@ -971,6 +976,13 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
#if DEBUG #if DEBUG
ResetDebugCompressionModalState(); ResetDebugCompressionModalState();
#endif #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) protected override void Dispose(bool disposing)
@@ -2198,26 +2210,56 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending); ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending);
ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending); ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending);
ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow(); DrawTextureTableHeaderRow();
var targets = _textureCompressionService.SelectableTargets; var targets = _textureCompressionService.SelectableTargets;
IEnumerable<TextureRow> orderedRows = rows; IEnumerable<TextureRow> orderedRows = rows;
var sortSpecs = ImGui.TableGetSortSpecs(); var sortSpecs = ImGui.TableGetSortSpecs();
var sizeSortColumn = -1;
var sizeSortDirection = ImGuiSortDirection.Ascending;
if (sortSpecs.SpecsCount > 0) if (sortSpecs.SpecsCount > 0)
{ {
var spec = sortSpecs.Specs[0]; var spec = sortSpecs.Specs[0];
orderedRows = spec.ColumnIndex switch if (spec.ColumnIndex is 7 or 8)
{ {
7 => spec.SortDirection == ImGuiSortDirection.Ascending sizeSortColumn = spec.ColumnIndex;
? rows.OrderBy(r => r.OriginalSize) sizeSortDirection = spec.SortDirection;
: rows.OrderByDescending(r => r.OriginalSize), }
8 => spec.SortDirection == ImGuiSortDirection.Ascending }
? rows.OrderBy(r => r.CompressedSize)
: rows.OrderByDescending(r => r.CompressedSize),
_ => rows
};
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; 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() private void StartTextureConversion()
{ {
if (_conversionTask != null && !_conversionTask.IsCompleted) if (_conversionTask != null && !_conversionTask.IsCompleted)

View File

@@ -103,10 +103,19 @@ public sealed class DtrEntry : IDisposable, IHostedService
public async Task StopAsync(CancellationToken cancellationToken) 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 try
{ {
await _runTask!.ConfigureAwait(false); if (_runTask != null)
await _runTask.ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models;
public enum TextureFormatSortMode
{
None = 0,
CompressedFirst = 1,
UncompressedFirst = 2
}

View File

@@ -3697,6 +3697,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SameLine(); ImGui.SameLine();
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow"))); _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) if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale)
{ {
UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed")); UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed"));
@@ -3734,8 +3742,31 @@ public class SettingsUi : WindowMediatorSubscriberBase
new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true), new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances.")); new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances."));
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), _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 performanceConfig = _playerPerformanceConfigService.Current;
var enableDecimation = performanceConfig.EnableModelDecimation; var enableDecimation = performanceConfig.EnableModelDecimation;
@@ -3756,11 +3787,19 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SameLine(); ImGui.SameLine();
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective triangle usage information will not work.", UIColors.Get("LightlessYellow"))); _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; var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold;
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); 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(); _playerPerformanceConfigService.Save();
} }
ImGui.SameLine(); ImGui.SameLine();
@@ -3768,7 +3807,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText($"Models below this triangle count are left untouched.{UiSharedService.TooltipSeparator}Default: 50,000"); _uiShared.DrawHelpText($"Models below this triangle count are left untouched.{UiSharedService.TooltipSeparator}Default: 50,000");
var targetPercent = (float)(performanceConfig.ModelDecimationTargetRatio * 100.0); 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) if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon)
{ {
performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0; performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0;
@@ -3776,17 +3815,30 @@ public class SettingsUi : WindowMediatorSubscriberBase
targetPercent = clampedPercent; targetPercent = clampedPercent;
} }
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); 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(); _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"); ImGui.TextUnformatted("Decimation targets");
_uiShared.DrawHelpText("Hair mods are always excluded from decimation."); _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; var allowBody = performanceConfig.ModelDecimationAllowBody;
if (ImGui.Checkbox("Body", ref allowBody)) if (ImGui.Checkbox("Body", ref allowBody))
{ {
@@ -3822,6 +3874,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
_playerPerformanceConfigService.Save(); _playerPerformanceConfigService.Save();
} }
ImGui.Dummy(new Vector2(5));
UiSharedService.ColoredSeparator(UIColors.Get("LightlessGrey"), 3f);
ImGui.Dummy(new Vector2(5)); ImGui.Dummy(new Vector2(5));
DrawTriangleDecimationCounters(); DrawTriangleDecimationCounters();
ImGui.Dummy(new Vector2(5)); ImGui.Dummy(new Vector2(5));

View File

@@ -205,10 +205,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private void ApplyUiVisibilitySettings() private void ApplyUiVisibilitySettings()
{ {
var config = _chatConfigService.Current;
_uiBuilder.DisableUserUiHide = true; _uiBuilder.DisableUserUiHide = true;
_uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes; _uiBuilder.DisableCutsceneUiHide = true;
_uiBuilder.DisableGposeUiHide = config.ShowInGpose;
} }
private bool ShouldHide() private bool ShouldHide()
@@ -220,6 +218,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
return true; return true;
} }
if (!config.ShowInGpose && _dalamudUtilService.IsInGpose)
{
return true;
}
if (!config.ShowInCutscenes && _dalamudUtilService.IsInCutscene)
{
return true;
}
if (config.HideInCombat && _dalamudUtilService.IsInCombat) if (config.HideInCombat && _dalamudUtilService.IsInCombat)
{ {
return true; return true;

View File

@@ -93,12 +93,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
CurrentOwnerToken = null; CurrentOwnerToken = null;
} }
public async Task DownloadFiles(GameObjectHandler? gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct, bool skipDownscale = false) public async Task DownloadFiles(GameObjectHandler? gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct, bool skipDownscale = false, bool skipDecimation = false)
{ {
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles))); Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
try try
{ {
await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale).ConfigureAwait(false); await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
} }
catch catch
{ {
@@ -458,7 +458,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
string downloadLabel, string downloadLabel,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
SetStatus(downloadStatusKey, DownloadStatus.Decompressing); SetStatus(downloadStatusKey, DownloadStatus.Decompressing);
MarkTransferredFiles(downloadStatusKey, 1); MarkTransferredFiles(downloadStatusKey, 1);
@@ -617,7 +618,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
yield return items.GetRange(i, Math.Min(chunkSize, items.Count - i)); yield return items.GetRange(i, Math.Min(chunkSize, items.Count - i));
} }
private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, bool skipDownscale) private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, bool skipDownscale, bool skipDecimation)
{ {
var objectName = gameObjectHandler?.Name ?? "Unknown"; var objectName = gameObjectHandler?.Name ?? "Unknown";
@@ -729,13 +730,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Task batchTask = batchChunks.Length == 0 Task batchTask = batchChunks.Length == 0
? Task.CompletedTask ? Task.CompletedTask
: Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct }, : 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 // direct downloads
Task directTask = directDownloads.Count == 0 Task directTask = directDownloads.Count == 0
? Task.CompletedTask ? Task.CompletedTask
: Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct }, : 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); await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
@@ -751,7 +752,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Dictionary<string, (string Extension, string GamePath)> replacementLookup, Dictionary<string, (string Extension, string GamePath)> replacementLookup,
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
var statusKey = chunk.StatusKey; var statusKey = chunk.StatusKey;
@@ -784,7 +786,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
return; 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) catch (OperationCanceledException)
{ {
@@ -806,7 +808,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Dictionary<string, (string Extension, string GamePath)> replacementLookup, Dictionary<string, (string Extension, string GamePath)> replacementLookup,
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
var progress = CreateInlineProgress(bytes => var progress = CreateInlineProgress(bytes =>
{ {
@@ -816,7 +819,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl)) 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; return;
} }
@@ -864,7 +867,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
} }
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false); 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); MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed); SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed);
@@ -891,7 +894,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
try 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) if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
{ {
@@ -921,7 +924,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
IProgress<long> progress, IProgress<long> progress,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl)) if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
throw new InvalidOperationException("Direct download fallback requested without a direct download URL."); throw new InvalidOperationException("Direct download fallback requested without a direct download URL.");
@@ -946,7 +950,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (!File.Exists(blockFile)) if (!File.Exists(blockFile))
throw new FileNotFoundException("Block file missing after direct download fallback.", 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); .ConfigureAwait(false);
} }
finally finally
@@ -973,7 +977,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? []; return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(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); var fi = new FileInfo(filePath);
@@ -1005,7 +1009,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
() => _textureMetadataHelper.DetermineMapKind(gamePath, filePath)); () => _textureMetadataHelper.DetermineMapKind(gamePath, filePath));
} }
if (!skipDownscale && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath)) if (!skipDecimation && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath))
{ {
_modelDecimationService.ScheduleDecimation(fileHash, filePath, gamePath); _modelDecimationService.ScheduleDecimation(fileHash, filePath, gamePath);
} }