diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index 0dc0cb7..8b90013 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -471,8 +471,55 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase FileCacheSize = total; - var maxCacheBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); - if (FileCacheSize < maxCacheBytes) return; + if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled")) + { + var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList(); + + long totalSizeDownscaled = 0; + + foreach (var f in filesDownscaled) + { + token.ThrowIfCancellationRequested(); + + try + { + long size = 0; + + if (!isWine) + { + try + { + size = _fileCompactor.GetFileSizeOnDisk(f); + } + catch (Exception ex) + { + Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName); + size = f.Length; + } + } + else + { + size = f.Length; + } + + totalSizeDownscaled += size; + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Error getting size for {file}", f.FullName); + } + } + + FileCacheSize = (totalSize + totalSizeDownscaled); + } + else + { + FileCacheSize = totalSize; + } + + var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); + if (FileCacheSize < maxCacheInBytes) + return; var buffer = (long)(maxCacheBytes * 0.05d); var target = maxCacheBytes - buffer; diff --git a/LightlessSync/FileCache/FileCompactor.cs b/LightlessSync/FileCache/FileCompactor.cs index c1a0532..971aa15 100644 --- a/LightlessSync/FileCache/FileCompactor.cs +++ b/LightlessSync/FileCache/FileCompactor.cs @@ -4,6 +4,7 @@ using LightlessSync.Services.Compactor; using Microsoft.Extensions.Logging; using Microsoft.Win32.SafeHandles; using System.Collections.Concurrent; +using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Channels; @@ -16,6 +17,7 @@ public sealed partial class FileCompactor : IDisposable public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const ulong WOF_PROVIDER_FILE = 2UL; public const int _maxRetries = 3; + private readonly bool _isWindows; private readonly ConcurrentDictionary _pendingCompactions; private readonly ILogger _logger; @@ -263,24 +265,12 @@ public sealed partial class FileCompactor : IDisposable { try { - bool isWine = _dalamudUtilService?.IsWine ?? false; + var (_, linuxPath) = ResolvePathsForBtrfs(fileInfo.FullName); - string linuxPath = isWine ? ToLinuxPathIfWine(fileInfo.FullName, isWine) - : fileInfo.FullName; - - (bool ok, string so, string se, int code) res; - - res = isWine - ? RunProcessShell($"stat -c %b -- {QuoteSingle(linuxPath)}", timeoutMs: 10000) - : RunProcessDirect("stat", ["-c", "%b", "--", linuxPath], "/", 10000); - - var outTrim = res.so?.Trim() ?? ""; - - if (res.ok && long.TryParse(outTrim, out long blocks) && blocks >= 0) - { - // st_blocks are 512-byte units - return (flowControl: false, value: blocks * 512L); - } + var (ok, output, err, code) = + _isWindows + ? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000) + : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000); if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim()); @@ -305,17 +295,28 @@ public sealed partial class FileCompactor : IDisposable try { var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.IsWine); - var losize = GetCompressedFileSizeW(fileInfo.FullName, out uint hosize); - var size = (long)hosize << 32 | losize; - return (flowControl: false, value: ((size + blockSize - 1) / blockSize) * blockSize); + if (blockSize <= 0) + throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}"); + + uint lo = GetCompressedFileSizeW(fileInfo.FullName, out uint hi); + + if (lo == 0xFFFFFFFF) + { + int err = Marshal.GetLastWin32Error(); + if (err != 0) + throw new Win32Exception(err); + } + + long size = ((long)hi << 32) | lo; + long rounded = ((size + blockSize - 1) / blockSize) * blockSize; + + return (flowControl: false, value: rounded); } catch (Exception ex) { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName); + _logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName); + return (flowControl: true, value: default); } - - return (flowControl: true, value: default); } /// @@ -1149,18 +1150,16 @@ public sealed partial class FileCompactor : IDisposable { try { - var pathToOpen = _isWindows ? winePath : linuxPath; - - if (string.IsNullOrEmpty(pathToOpen) || !File.Exists(pathToOpen)) - return false; - - using var _ = new FileStream(pathToOpen, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + if (_isWindows) + { + using var _ = new FileStream(winePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + else + { + using var _ = new FileStream(linuxPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } return true; } - catch - { - return false; - } } /// diff --git a/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs b/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs new file mode 100644 index 0000000..dbf1c15 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs @@ -0,0 +1,193 @@ +using Dalamud.Plugin; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Interop.Ipc.Framework; + +public enum IpcConnectionState +{ + Unknown = 0, + MissingPlugin = 1, + VersionMismatch = 2, + PluginDisabled = 3, + NotReady = 4, + Available = 5, + Error = 6, +} + +public sealed record IpcServiceDescriptor(string InternalName, string DisplayName, Version MinimumVersion) +{ + public override string ToString() + => $"{DisplayName} (>= {MinimumVersion})"; +} + +public interface IIpcService : IDisposable +{ + IpcServiceDescriptor Descriptor { get; } + IpcConnectionState State { get; } + IDalamudPluginInterface PluginInterface { get; } + bool APIAvailable { get; } + void CheckAPI(); +} + +public interface IIpcInterop : IDisposable +{ + string Name { get; } + void OnConnectionStateChanged(IpcConnectionState state); +} + +public abstract class IpcInteropBase : IIpcInterop +{ + protected IpcInteropBase(ILogger logger) + { + Logger = logger; + } + + protected ILogger Logger { get; } + + protected IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown; + + protected bool IsAvailable => State == IpcConnectionState.Available; + + public abstract string Name { get; } + + public void OnConnectionStateChanged(IpcConnectionState state) + { + if (State == state) + { + return; + } + + var previous = State; + State = state; + HandleStateChange(previous, state); + } + + protected abstract void HandleStateChange(IpcConnectionState previous, IpcConnectionState current); + + public virtual void Dispose() + { + } +} + +public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcService +{ + private readonly List _interops = new(); + + protected IpcServiceBase( + ILogger logger, + LightlessMediator mediator, + IDalamudPluginInterface pluginInterface, + IpcServiceDescriptor descriptor) : base(logger, mediator) + { + PluginInterface = pluginInterface; + Descriptor = descriptor; + } + + protected IDalamudPluginInterface PluginInterface { get; } + + IDalamudPluginInterface IIpcService.PluginInterface => PluginInterface; + + protected IpcServiceDescriptor Descriptor { get; } + + IpcServiceDescriptor IIpcService.Descriptor => Descriptor; + + public IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown; + + public bool APIAvailable => State == IpcConnectionState.Available; + + public virtual void CheckAPI() + { + var newState = EvaluateState(); + UpdateState(newState); + } + + protected virtual IpcConnectionState EvaluateState() + { + try + { + var plugin = PluginInterface.InstalledPlugins + .FirstOrDefault(p => string.Equals(p.InternalName, Descriptor.InternalName, StringComparison.OrdinalIgnoreCase)); + + if (plugin == null) + { + return IpcConnectionState.MissingPlugin; + } + + if (plugin.Version < Descriptor.MinimumVersion) + { + return IpcConnectionState.VersionMismatch; + } + + if (!IsPluginEnabled()) + { + return IpcConnectionState.PluginDisabled; + } + + if (!IsPluginReady()) + { + return IpcConnectionState.NotReady; + } + + return IpcConnectionState.Available; + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to evaluate IPC state for {Service}", Descriptor.DisplayName); + return IpcConnectionState.Error; + } + } + + protected virtual bool IsPluginEnabled() + => true; + + protected virtual bool IsPluginReady() + => true; + + protected TInterop RegisterInterop(TInterop interop) + where TInterop : IIpcInterop + { + _interops.Add(interop); + interop.OnConnectionStateChanged(State); + return interop; + } + + private void UpdateState(IpcConnectionState newState) + { + if (State == newState) + { + return; + } + + var previous = State; + State = newState; + OnConnectionStateChanged(previous, newState); + + foreach (var interop in _interops) + { + interop.OnConnectionStateChanged(newState); + } + } + + protected virtual void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + Logger.LogTrace("{Service} IPC state transitioned from {Previous} to {Current}", Descriptor.DisplayName, previous, current); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) + { + return; + } + + for (var i = _interops.Count - 1; i >= 0; --i) + { + _interops[i].Dispose(); + } + + _interops.Clear(); + } +} diff --git a/LightlessSync/Interop/Ipc/IIpcCaller.cs b/LightlessSync/Interop/Ipc/IIpcCaller.cs deleted file mode 100644 index 8519d1a..0000000 --- a/LightlessSync/Interop/Ipc/IIpcCaller.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LightlessSync.Interop.Ipc; - -public interface IIpcCaller : IDisposable -{ - bool APIAvailable { get; } - void CheckAPI(); -} diff --git a/LightlessSync/Interop/Ipc/IpcCallerBrio.cs b/LightlessSync/Interop/Ipc/IpcCallerBrio.cs index 5728464..83105d9 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerBrio.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerBrio.cs @@ -2,15 +2,19 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using LightlessSync.API.Dto.CharaData; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; +using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Numerics; using System.Text.Json.Nodes; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerBrio : IIpcCaller +public sealed class IpcCallerBrio : IpcServiceBase { + private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtilService; private readonly ICallGateSubscriber<(int, int)> _brioApiVersion; @@ -25,10 +29,8 @@ public sealed class IpcCallerBrio : IIpcCaller private readonly ICallGateSubscriber _brioFreezePhysics; - public bool APIAvailable { get; private set; } - public IpcCallerBrio(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, - DalamudUtilService dalamudUtilService) + DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator, dalamudPluginInterface, BrioDescriptor) { _logger = logger; _dalamudUtilService = dalamudUtilService; @@ -46,19 +48,6 @@ public sealed class IpcCallerBrio : IIpcCaller CheckAPI(); } - public void CheckAPI() - { - try - { - var version = _brioApiVersion.InvokeFunc(); - APIAvailable = (version.Item1 == 2 && version.Item2 >= 0); - } - catch - { - APIAvailable = false; - } - } - public async Task SpawnActorAsync() { if (!APIAvailable) return null; @@ -140,7 +129,30 @@ public sealed class IpcCallerBrio : IIpcCaller return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false); } - public void Dispose() + protected override IpcConnectionState EvaluateState() { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var version = _brioApiVersion.InvokeFunc(); + return version.Item1 == 2 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Brio IPC version"); + return IpcConnectionState.Error; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs b/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs index 60feaba..fd1b88c 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Utility; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; @@ -9,8 +10,10 @@ using System.Text; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerCustomize : IIpcCaller +public sealed class IpcCallerCustomize : IpcServiceBase { + private static readonly IpcServiceDescriptor CustomizeDescriptor = new("CustomizePlus", "Customize+", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion; private readonly ICallGateSubscriber _customizePlusGetActiveProfile; private readonly ICallGateSubscriber _customizePlusGetProfileById; @@ -23,7 +26,7 @@ public sealed class IpcCallerCustomize : IIpcCaller private readonly LightlessMediator _lightlessMediator; public IpcCallerCustomize(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, - DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) + DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) : base(logger, lightlessMediator, dalamudPluginInterface, CustomizeDescriptor) { _customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion"); _customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.GetActiveProfileIdOnCharacter"); @@ -41,8 +44,6 @@ public sealed class IpcCallerCustomize : IIpcCaller CheckAPI(); } - public bool APIAvailable { get; private set; } = false; - public async Task RevertAsync(nint character) { if (!APIAvailable) return; @@ -113,16 +114,25 @@ public sealed class IpcCallerCustomize : IIpcCaller return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); } - public void CheckAPI() + protected override IpcConnectionState EvaluateState() { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + try { var version = _customizePlusApiVersion.InvokeFunc(); - APIAvailable = (version.Item1 == 6 && version.Item2 >= 0); + return version.Item1 == 6 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; } - catch + catch (Exception ex) { - APIAvailable = false; + Logger.LogDebug(ex, "Failed to query Customize+ API version"); + return IpcConnectionState.Error; } } @@ -132,8 +142,14 @@ public sealed class IpcCallerCustomize : IIpcCaller _lightlessMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null)); } - public void Dispose() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (!disposing) + { + return; + } + _customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs b/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs index 8763188..4f9f53d 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using Glamourer.Api.Helpers; using Glamourer.Api.IpcSubscribers; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; @@ -10,8 +11,9 @@ using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller +public sealed class IpcCallerGlamourer : IpcServiceBase { + private static readonly IpcServiceDescriptor GlamourerDescriptor = new("Glamourer", "Glamourer", new Version(1, 3, 0, 10)); private readonly ILogger _logger; private readonly IDalamudPluginInterface _pi; private readonly DalamudUtilService _dalamudUtil; @@ -31,7 +33,7 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC private readonly uint LockCode = 0x6D617265; public IpcCallerGlamourer(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator, - RedrawManager redrawManager) : base(logger, lightlessMediator) + RedrawManager redrawManager) : base(logger, lightlessMediator, pi, GlamourerDescriptor) { _glamourerApiVersions = new ApiVersion(pi); _glamourerGetAllCustomization = new GetStateBase64(pi); @@ -62,47 +64,6 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC _glamourerStateChanged?.Dispose(); } - public bool APIAvailable { get; private set; } - - public void CheckAPI() - { - bool apiAvailable = false; - try - { - bool versionValid = (_pi.InstalledPlugins - .FirstOrDefault(p => string.Equals(p.InternalName, "Glamourer", StringComparison.OrdinalIgnoreCase)) - ?.Version ?? new Version(0, 0, 0, 0)) >= new Version(1, 3, 0, 10); - try - { - var version = _glamourerApiVersions.Invoke(); - if (version is { Major: 1, Minor: >= 1 } && versionValid) - { - apiAvailable = true; - } - } - catch - { - // ignore - } - _shownGlamourerUnavailable = _shownGlamourerUnavailable && !apiAvailable; - - APIAvailable = apiAvailable; - } - catch - { - APIAvailable = apiAvailable; - } - finally - { - if (!apiAvailable && !_shownGlamourerUnavailable) - { - _shownGlamourerUnavailable = true; - _lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.", - NotificationType.Error)); - } - } - } - public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false) { if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return; @@ -210,6 +171,49 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC } } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var version = _glamourerApiVersions.Invoke(); + return version is { Major: 1, Minor: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Glamourer API version"); + return IpcConnectionState.Error; + } + } + + protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + base.OnConnectionStateChanged(previous, current); + + if (current == IpcConnectionState.Available) + { + _shownGlamourerUnavailable = false; + return; + } + + if (_shownGlamourerUnavailable || current == IpcConnectionState.Unknown) + { + return; + } + + _shownGlamourerUnavailable = true; + _lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", + "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.", + NotificationType.Error)); + } + private void GlamourerChanged(nint address) { _lightlessMediator.Publish(new GlamourerChangedMessage(address)); diff --git a/LightlessSync/Interop/Ipc/IpcCallerHeels.cs b/LightlessSync/Interop/Ipc/IpcCallerHeels.cs index 69b359c..23fe192 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerHeels.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerHeels.cs @@ -1,13 +1,16 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerHeels : IIpcCaller +public sealed class IpcCallerHeels : IpcServiceBase { + private static readonly IpcServiceDescriptor HeelsDescriptor = new("SimpleHeels", "Simple Heels", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly LightlessMediator _lightlessMediator; private readonly DalamudUtilService _dalamudUtil; @@ -18,6 +21,7 @@ public sealed class IpcCallerHeels : IIpcCaller private readonly ICallGateSubscriber _heelsUnregisterPlayer; public IpcCallerHeels(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) + : base(logger, lightlessMediator, pi, HeelsDescriptor) { _logger = logger; _lightlessMediator = lightlessMediator; @@ -32,8 +36,26 @@ public sealed class IpcCallerHeels : IIpcCaller CheckAPI(); } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } - public bool APIAvailable { get; private set; } = false; + try + { + return _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query SimpleHeels API version"); + return IpcConnectionState.Error; + } + } private void HeelsOffsetChange(string offset) { @@ -74,20 +96,14 @@ public sealed class IpcCallerHeels : IIpcCaller }).ConfigureAwait(false); } - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 }; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs b/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs index 58588c5..4a2ed5d 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; @@ -8,8 +9,10 @@ using System.Text; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerHonorific : IIpcCaller +public sealed class IpcCallerHonorific : IpcServiceBase { + private static readonly IpcServiceDescriptor HonorificDescriptor = new("Honorific", "Honorific", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion; private readonly ICallGateSubscriber _honorificClearCharacterTitle; private readonly ICallGateSubscriber _honorificDisposing; @@ -22,7 +25,7 @@ public sealed class IpcCallerHonorific : IIpcCaller private readonly DalamudUtilService _dalamudUtil; public IpcCallerHonorific(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, HonorificDescriptor) { _logger = logger; _lightlessMediator = lightlessMediator; @@ -41,23 +44,14 @@ public sealed class IpcCallerHonorific : IIpcCaller CheckAPI(); } - - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 }; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged); _honorificDisposing.Unsubscribe(OnHonorificDisposing); _honorificReady.Unsubscribe(OnHonorificReady); @@ -113,6 +107,27 @@ public sealed class IpcCallerHonorific : IIpcCaller } } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + return _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Honorific API version"); + return IpcConnectionState.Error; + } + } + private void OnHonorificDisposing() { _lightlessMediator.Publish(new HonorificMessage(string.Empty)); diff --git a/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs b/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs index 610ece4..e8b1b76 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs @@ -1,14 +1,17 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerMoodles : IIpcCaller +public sealed class IpcCallerMoodles : IpcServiceBase { + private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber _moodlesApiVersion; private readonly ICallGateSubscriber _moodlesOnChange; private readonly ICallGateSubscriber _moodlesGetStatus; @@ -19,7 +22,7 @@ public sealed class IpcCallerMoodles : IIpcCaller private readonly LightlessMediator _lightlessMediator; public IpcCallerMoodles(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, MoodlesDescriptor) { _logger = logger; _dalamudUtil = dalamudUtil; @@ -41,22 +44,14 @@ public sealed class IpcCallerMoodles : IIpcCaller _lightlessMediator.Publish(new MoodlesMessage(character.Address)); } - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _moodlesApiVersion.InvokeFunc() == 3; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _moodlesOnChange.Unsubscribe(OnMoodlesChange); } @@ -101,4 +96,25 @@ public sealed class IpcCallerMoodles : IIpcCaller _logger.LogWarning(e, "Could not Set Moodles Status"); } } + + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + return _moodlesApiVersion.InvokeFunc() == 3 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Moodles API version"); + return IpcConnectionState.Error; + } + } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs index 2ecc56b..4169e5c 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs @@ -1,4 +1,6 @@ -using Dalamud.Plugin; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Interop.Ipc.Penumbra; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; @@ -8,525 +10,210 @@ using Microsoft.Extensions.Logging; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller +public sealed class IpcCallerPenumbra : IpcServiceBase { - private readonly IDalamudPluginInterface _pi; - private readonly DalamudUtilService _dalamudUtil; - private readonly LightlessMediator _lightlessMediator; - private readonly RedrawManager _redrawManager; - private readonly ActorObjectService _actorObjectService; - private bool _shownPenumbraUnavailable = false; - private string? _penumbraModDirectory; - public string? ModDirectory - { - get => _penumbraModDirectory; - private set - { - if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal)) - { - _penumbraModDirectory = value; - _lightlessMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory)); - } - } - } + private static readonly IpcServiceDescriptor PenumbraDescriptor = new("Penumbra", "Penumbra", new Version(1, 2, 0, 22)); - private readonly ConcurrentDictionary _penumbraRedrawRequests = new(); - private readonly ConcurrentDictionary _trackedActors = new(); + private readonly PenumbraCollections _collections; + private readonly PenumbraResource _resources; + private readonly PenumbraRedraw _redraw; + private readonly PenumbraTexture _textures; - private readonly EventSubscriber _penumbraDispose; - private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; - private readonly EventSubscriber _penumbraInit; - private readonly EventSubscriber _penumbraModSettingChanged; - private readonly EventSubscriber _penumbraObjectIsRedrawn; - - private readonly AddTemporaryMod _penumbraAddTemporaryMod; - private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection; - private readonly ConvertTextureFile _penumbraConvertTextureFile; - private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection; private readonly GetEnabledState _penumbraEnabled; - private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations; - private readonly RedrawObject _penumbraRedraw; - private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection; - private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod; - private readonly GetModDirectory _penumbraResolveModDir; - private readonly ResolvePlayerPathsAsync _penumbraResolvePaths; - private readonly GetGameObjectResourcePaths _penumbraResourcePaths; - //private readonly GetPlayerResourcePaths _penumbraPlayerResourcePaths; - private readonly GetCollections _penumbraGetCollections; - private readonly ConcurrentDictionary _activeTemporaryCollections = new(); - private int _performedInitialCleanup; + private readonly GetModDirectory _penumbraGetModDirectory; + private readonly EventSubscriber _penumbraInit; + private readonly EventSubscriber _penumbraDispose; + private readonly EventSubscriber _penumbraModSettingChanged; - public IpcCallerPenumbra(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator, RedrawManager redrawManager, ActorObjectService actorObjectService) : base(logger, lightlessMediator) + private bool _shownPenumbraUnavailable; + private string? _modDirectory; + + public IpcCallerPenumbra( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + RedrawManager redrawManager, + ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor) { - _pi = pi; - _dalamudUtil = dalamudUtil; - _lightlessMediator = lightlessMediator; - _redrawManager = redrawManager; - _actorObjectService = actorObjectService; - _penumbraInit = Initialized.Subscriber(pi, PenumbraInit); - _penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose); - _penumbraResolveModDir = new GetModDirectory(pi); - _penumbraRedraw = new RedrawObject(pi); - _penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent); - _penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi); - _penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi); - _penumbraAddTemporaryMod = new AddTemporaryMod(pi); - _penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi); - _penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi); - _penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi); - _penumbraGetCollections = new GetCollections(pi); - _penumbraResolvePaths = new ResolvePlayerPathsAsync(pi); - _penumbraEnabled = new GetEnabledState(pi); - _penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) => - { - if (change == ModSettingChange.EnableState) - _lightlessMediator.Publish(new PenumbraModSettingChangedMessage()); - }); - _penumbraConvertTextureFile = new ConvertTextureFile(pi); - _penumbraResourcePaths = new GetGameObjectResourcePaths(pi); - //_penumbraPlayerResourcePaths = new GetPlayerResourcePaths(pi); + _penumbraEnabled = new GetEnabledState(pluginInterface); + _penumbraGetModDirectory = new GetModDirectory(pluginInterface); + _penumbraInit = Initialized.Subscriber(pluginInterface, HandlePenumbraInitialized); + _penumbraDispose = Disposed.Subscriber(pluginInterface, HandlePenumbraDisposed); + _penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged); - _penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); + _collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator)); + _resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService)); + _redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager)); + _textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw)); + + SubscribeMediatorEvents(); CheckAPI(); CheckModDirectory(); - - Mediator.Subscribe(this, (msg) => - { - _penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose); - }); - - Mediator.Subscribe(this, (msg) => _shownPenumbraUnavailable = false); - - Mediator.Subscribe(this, msg => - { - if (msg.Descriptor.Address != nint.Zero) - { - _trackedActors[(IntPtr)msg.Descriptor.Address] = 0; - } - }); - - Mediator.Subscribe(this, msg => - { - if (msg.Descriptor.Address != nint.Zero) - { - _trackedActors.TryRemove((IntPtr)msg.Descriptor.Address, out _); - } - }); - - Mediator.Subscribe(this, msg => - { - if (msg.GameObjectHandler.Address != nint.Zero) - { - _trackedActors[(IntPtr)msg.GameObjectHandler.Address] = 0; - } - }); - - Mediator.Subscribe(this, msg => - { - if (msg.GameObjectHandler.Address != nint.Zero) - { - _trackedActors.TryRemove((IntPtr)msg.GameObjectHandler.Address, out _); - } - }); - - foreach (var descriptor in _actorObjectService.PlayerDescriptors) - { - if (descriptor.Address != nint.Zero) - { - _trackedActors[(IntPtr)descriptor.Address] = 0; - } - } } - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + public string? ModDirectory { - bool penumbraAvailable = false; - try + get => _modDirectory; + private set { - var penumbraVersion = (_pi.InstalledPlugins - .FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase)) - ?.Version ?? new Version(0, 0, 0, 0)); - penumbraAvailable = penumbraVersion >= new Version(1, 2, 0, 22); - try + if (string.Equals(_modDirectory, value, StringComparison.Ordinal)) { - penumbraAvailable &= _penumbraEnabled.Invoke(); + return; } - catch - { - penumbraAvailable = false; - } - _shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable; - APIAvailable = penumbraAvailable; - } - catch - { - APIAvailable = penumbraAvailable; - } - finally - { - if (!penumbraAvailable && !_shownPenumbraUnavailable) - { - _shownPenumbraUnavailable = true; - _lightlessMediator.Publish(new NotificationMessage("Penumbra inactive", - "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.", - NotificationType.Error)); - } - } - if (APIAvailable) - { - ScheduleTemporaryCollectionCleanup(); + _modDirectory = value; + Mediator.Publish(new PenumbraDirectoryChangedMessage(_modDirectory)); } } + public Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex) + => _collections.AssignTemporaryCollectionAsync(logger, collectionId, objectIndex); + + public Task CreateTemporaryCollectionAsync(ILogger logger, string uid) + => _collections.CreateTemporaryCollectionAsync(logger, uid); + + public Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId) + => _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId); + + public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary modPaths) + => _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths); + + public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData) + => _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData); + + public Task>?> GetCharacterData(ILogger logger, GameObjectHandler handler) + => _resources.GetCharacterDataAsync(logger, handler); + + public string GetMetaManipulations() + => _resources.GetMetaManipulations(); + + public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse) + => _resources.ResolvePathsAsync(forward, reverse); + + public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + => _redraw.RedrawAsync(logger, handler, applicationId, token); + + public Task ConvertTextureFiles(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token) + => _textures.ConvertTextureFilesAsync(logger, jobs, progress, token); + + public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token) + => _textures.ConvertTextureFileDirectAsync(job, token); + public void CheckModDirectory() { if (!APIAvailable) { ModDirectory = string.Empty; - } - else - { - ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant(); - } - } - - private void ScheduleTemporaryCollectionCleanup() - { - if (Interlocked.Exchange(ref _performedInitialCleanup, 1) != 0) - return; - - _ = Task.Run(CleanupTemporaryCollectionsAsync); - } - - private async Task CleanupTemporaryCollectionsAsync() - { - if (!APIAvailable) return; + } try { - var collections = await _dalamudUtil.RunOnFrameworkThread(() => _penumbraGetCollections.Invoke()).ConfigureAwait(false); - foreach (var (collectionId, name) in collections) - { - if (!IsLightlessCollectionName(name)) - continue; - - if (_activeTemporaryCollections.ContainsKey(collectionId)) - continue; - - Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId); - var deleteResult = await _dalamudUtil.RunOnFrameworkThread(() => - { - var result = (PenumbraApiEc)_penumbraRemoveTemporaryCollection.Invoke(collectionId); - Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result); - return result; - }).ConfigureAwait(false); - if (deleteResult == PenumbraApiEc.Success) - { - _activeTemporaryCollections.TryRemove(collectionId, out _); - } - else - { - Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult); - } - } + ModDirectory = _penumbraGetModDirectory.Invoke().ToLowerInvariant(); } catch (Exception ex) { - Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections"); + Logger.LogWarning(ex, "Failed to resolve Penumbra mod directory"); } } - private static bool IsLightlessCollectionName(string? name) - => !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal); + protected override bool IsPluginEnabled() + { + try + { + return _penumbraEnabled.Invoke(); + } + catch + { + return false; + } + } + + protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + base.OnConnectionStateChanged(previous, current); + + if (current == IpcConnectionState.Available) + { + _shownPenumbraUnavailable = false; + if (string.IsNullOrEmpty(ModDirectory)) + { + CheckModDirectory(); + } + return; + } + + ModDirectory = string.Empty; + _redraw.CancelPendingRedraws(); + + if (_shownPenumbraUnavailable || current == IpcConnectionState.Unknown) + { + return; + } + + _shownPenumbraUnavailable = true; + Mediator.Publish(new NotificationMessage( + "Penumbra inactive", + "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.", + NotificationType.Error)); + } + + private void SubscribeMediatorEvents() + { + Mediator.Subscribe(this, msg => + { + _redraw.RequestImmediateRedraw(msg.Character.ObjectIndex, RedrawType.AfterGPose); + }); + + Mediator.Subscribe(this, _ => _shownPenumbraUnavailable = false); + + Mediator.Subscribe(this, msg => _resources.TrackActor(msg.Descriptor.Address)); + Mediator.Subscribe(this, msg => _resources.UntrackActor(msg.Descriptor.Address)); + Mediator.Subscribe(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address)); + Mediator.Subscribe(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address)); + } + + private void HandlePenumbraInitialized() + { + Mediator.Publish(new PenumbraInitializedMessage()); + CheckModDirectory(); + _redraw.RequestImmediateRedraw(0, RedrawType.Redraw); + CheckAPI(); + } + + private void HandlePenumbraDisposed() + { + _redraw.CancelPendingRedraws(); + ModDirectory = string.Empty; + Mediator.Publish(new PenumbraDisposedMessage()); + CheckAPI(); + } + + private void HandlePenumbraModSettingChanged(ModSettingChange change, Guid _, string __, bool ___) + { + if (change == ModSettingChange.EnableState) + { + Mediator.Publish(new PenumbraModSettingChangedMessage()); + CheckAPI(); + } + } protected override void Dispose(bool disposing) { base.Dispose(disposing); - _redrawManager.Cancel(); + if (!disposing) + { + return; + } _penumbraModSettingChanged.Dispose(); - _penumbraGameObjectResourcePathResolved.Dispose(); _penumbraDispose.Dispose(); _penumbraInit.Dispose(); - _penumbraObjectIsRedrawn.Dispose(); - } - - public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx) - { - if (!APIAvailable) return; - - await _dalamudUtil.RunOnFrameworkThread(() => - { - var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true); - logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign); - return collName; - }).ConfigureAwait(false); - } - - public async Task ConvertTextureFiles(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token) - { - if (!APIAvailable || jobs.Count == 0) - { - return; - } - - _lightlessMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles))); - - var totalJobs = jobs.Count; - var completedJobs = 0; - - try - { - foreach (var job in jobs) - { - if (token.IsCancellationRequested) - { - break; - } - - progress?.Report(new TextureConversionProgress(completedJobs, totalJobs, job)); - - logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType); - var convertTask = _penumbraConvertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps); - await convertTask.ConfigureAwait(false); - - if (convertTask.IsCompletedSuccessfully && job.DuplicateTargets is { Count: > 0 }) - { - foreach (var duplicate in job.DuplicateTargets) - { - logger.LogInformation("Synchronizing duplicate {Duplicate}", duplicate); - try - { - File.Copy(job.OutputFile, duplicate, overwrite: true); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to copy duplicate {Duplicate}", duplicate); - } - } - } - - completedJobs++; - } - } - finally - { - _lightlessMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles))); - } - - if (completedJobs > 0 && !token.IsCancellationRequested) - { - await _dalamudUtil.RunOnFrameworkThread(async () => - { - var player = await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false); - if (player == null) - { - return; - } - - var gameObject = await _dalamudUtil.CreateGameObjectAsync(player).ConfigureAwait(false); - _penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw); - }).ConfigureAwait(false); - } - } - - public async Task CreateTemporaryCollectionAsync(ILogger logger, string uid) - { - if (!APIAvailable) return Guid.Empty; - - var (collectionId, collectionName) = await _dalamudUtil.RunOnFrameworkThread(() => - { - var collName = "Lightless_" + uid; - _penumbraCreateNamedTemporaryCollection.Invoke(collName, collName, out var collId); - logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId); - return (collId, collName); - - }).ConfigureAwait(false); - if (collectionId != Guid.Empty) - { - _activeTemporaryCollections[collectionId] = collectionName; - } - - return collectionId; - } - - public async Task>?> GetCharacterData(ILogger logger, GameObjectHandler handler) - { - if (!APIAvailable) return null; - - return await _dalamudUtil.RunOnFrameworkThread(() => - { - logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); - var idx = handler.GetGameObject()?.ObjectIndex; - if (idx == null) return null; - return _penumbraResourcePaths.Invoke(idx.Value)[0]; - }).ConfigureAwait(false); - } - - public string GetMetaManipulations() - { - if (!APIAvailable) return string.Empty; - return _penumbraGetMetaManipulations.Invoke(); - } - - public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) - { - if (!APIAvailable || _dalamudUtil.IsZoning) return; - try - { - await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false); - await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) => - { - logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId); - _penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw); - - }, token).ConfigureAwait(false); - } - finally - { - _redrawManager.RedrawSemaphore.Release(); - } - } - - public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId) - { - if (!APIAvailable) return; - await _dalamudUtil.RunOnFrameworkThread(() => - { - logger.LogTrace("[{applicationId}] Removing temp collection for {collId}", applicationId, collId); - var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collId); - logger.LogTrace("[{applicationId}] RemoveTemporaryCollection: {ret2}", applicationId, ret2); - }).ConfigureAwait(false); - if (collId != Guid.Empty) - { - _activeTemporaryCollections.TryRemove(collId, out _); - } - } - - public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse) - { - return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false); - } - - public async Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token) - { - if (!APIAvailable) return; - - token.ThrowIfCancellationRequested(); - - await _penumbraConvertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps) - .ConfigureAwait(false); - - if (job.DuplicateTargets is { Count: > 0 }) - { - foreach (var duplicate in job.DuplicateTargets) - { - try - { - File.Copy(job.OutputFile, duplicate, overwrite: true); - } - catch (Exception ex) - { - Logger.LogDebug(ex, "Failed to copy duplicate {Duplicate} for texture conversion", duplicate); - } - } - } - } - - public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collId, string manipulationData) - { - if (!APIAvailable) return; - - await _dalamudUtil.RunOnFrameworkThread(() => - { - logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData); - var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Meta", collId, [], manipulationData, 0); - logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd); - }).ConfigureAwait(false); - } - - public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary modPaths) - { - if (!APIAvailable) return; - - await _dalamudUtil.RunOnFrameworkThread(() => - { - foreach (var mod in modPaths) - { - logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value); - } - var retRemove = _penumbraRemoveTemporaryMod.Invoke("LightlessChara_Files", collId, 0); - logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove); - var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Files", collId, modPaths, string.Empty, 0); - logger.LogTrace("[{applicationId}] Setting temp files mod for {collId}, Success: {ret}", applicationId, collId, retAdd); - }).ConfigureAwait(false); - } - - private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) - { - bool wasRequested = false; - if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest) - { - _penumbraRedrawRequests[objectAddress] = false; - } - else - { - _lightlessMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested)); - } - } - - private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) - { - if (ptr == IntPtr.Zero) - return; - - if (!_trackedActors.ContainsKey(ptr)) - { - var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr); - if (descriptor.Address != nint.Zero) - { - _trackedActors[ptr] = 0; - } - else - { - return; - } - } - - if (string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) == 0) - return; - - _lightlessMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2)); - } - - private void PenumbraDispose() - { - _redrawManager.Cancel(); - _lightlessMediator.Publish(new PenumbraDisposedMessage()); - } - - private void PenumbraInit() - { - APIAvailable = true; - ModDirectory = _penumbraResolveModDir.Invoke(); - _lightlessMediator.Publish(new PenumbraInitializedMessage()); - ScheduleTemporaryCollectionCleanup(); - _penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs b/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs index 9839f29..5d7fea9 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs @@ -1,14 +1,17 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerPetNames : IIpcCaller +public sealed class IpcCallerPetNames : IpcServiceBase { + private static readonly IpcServiceDescriptor PetRenamerDescriptor = new("PetRenamer", "Pet Renamer", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly LightlessMediator _lightlessMediator; @@ -24,7 +27,7 @@ public sealed class IpcCallerPetNames : IIpcCaller private readonly ICallGateSubscriber _clearPlayerData; public IpcCallerPetNames(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, PetRenamerDescriptor) { _logger = logger; _dalamudUtil = dalamudUtil; @@ -46,25 +49,6 @@ public sealed class IpcCallerPetNames : IIpcCaller CheckAPI(); } - - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() - { - try - { - APIAvailable = _enabled?.InvokeFunc() ?? false; - if (APIAvailable) - { - APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 }; - } - } - catch - { - APIAvailable = false; - } - } - private void OnPetNicknamesReady() { CheckAPI(); @@ -76,6 +60,34 @@ public sealed class IpcCallerPetNames : IIpcCaller _lightlessMediator.Publish(new PetNamesMessage(string.Empty)); } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var enabled = _enabled?.InvokeFunc() ?? false; + if (!enabled) + { + return IpcConnectionState.PluginDisabled; + } + + var version = _apiVersion?.InvokeFunc() ?? (0u, 0u); + return version.Item1 == 4 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Pet Renamer API version"); + return IpcConnectionState.Error; + } + } + public string GetLocalNames() { if (!APIAvailable) return string.Empty; @@ -149,8 +161,14 @@ public sealed class IpcCallerPetNames : IIpcCaller _lightlessMediator.Publish(new PetNamesMessage(data)); } - public void Dispose() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (!disposing) + { + return; + } + _petnamesReady.Unsubscribe(OnPetNicknamesReady); _petnamesDisposing.Unsubscribe(OnPetNicknamesDispose); _playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange); diff --git a/LightlessSync/Interop/Ipc/IpcProvider.cs b/LightlessSync/Interop/Ipc/IpcProvider.cs index 88e0202..77f8043 100644 --- a/LightlessSync/Interop/Ipc/IpcProvider.cs +++ b/LightlessSync/Interop/Ipc/IpcProvider.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.ClientState.Objects.Types; +using System; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using LightlessSync.PlayerData.Handlers; @@ -14,9 +15,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber private readonly ILogger _logger; private readonly IDalamudPluginInterface _pi; private readonly CharaDataManager _charaDataManager; - private ICallGateProvider? _loadFileProvider; - private ICallGateProvider>? _loadFileAsyncProvider; - private ICallGateProvider>? _handledGameAddresses; + private readonly List _ipcRegisters = []; private readonly List _activeGameObjectHandlers = []; public LightlessMediator Mediator { get; init; } @@ -44,12 +43,9 @@ public class IpcProvider : IHostedService, IMediatorSubscriber public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting IpcProviderService"); - _loadFileProvider = _pi.GetIpcProvider("LightlessSync.LoadMcdf"); - _loadFileProvider.RegisterFunc(LoadMcdf); - _loadFileAsyncProvider = _pi.GetIpcProvider>("LightlessSync.LoadMcdfAsync"); - _loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync); - _handledGameAddresses = _pi.GetIpcProvider>("LightlessSync.GetHandledAddresses"); - _handledGameAddresses.RegisterFunc(GetHandledAddresses); + _ipcRegisters.Add(RegisterFunc("LightlessSync.LoadMcdf", LoadMcdf)); + _ipcRegisters.Add(RegisterFunc>("LightlessSync.LoadMcdfAsync", LoadMcdfAsync)); + _ipcRegisters.Add(RegisterFunc("LightlessSync.GetHandledAddresses", GetHandledAddresses)); _logger.LogInformation("Started IpcProviderService"); return Task.CompletedTask; } @@ -57,9 +53,11 @@ public class IpcProvider : IHostedService, IMediatorSubscriber public Task StopAsync(CancellationToken cancellationToken) { _logger.LogDebug("Stopping IpcProvider Service"); - _loadFileProvider?.UnregisterFunc(); - _loadFileAsyncProvider?.UnregisterFunc(); - _handledGameAddresses?.UnregisterFunc(); + foreach (var register in _ipcRegisters) + { + register.Dispose(); + } + _ipcRegisters.Clear(); Mediator.UnsubscribeAll(this); return Task.CompletedTask; } @@ -89,4 +87,40 @@ public class IpcProvider : IHostedService, IMediatorSubscriber { return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList(); } + + private IpcRegister RegisterFunc(string label, Func> handler) + { + var provider = _pi.GetIpcProvider>(label); + provider.RegisterFunc(handler); + return new IpcRegister(provider.UnregisterFunc); + } + + private IpcRegister RegisterFunc(string label, Func handler) + { + var provider = _pi.GetIpcProvider(label); + provider.RegisterFunc(handler); + return new IpcRegister(provider.UnregisterFunc); + } + + private sealed class IpcRegister : IDisposable + { + private readonly Action _unregister; + private bool _disposed; + + public IpcRegister(Action unregister) + { + _unregister = unregister; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _unregister(); + _disposed = true; + } + } } diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs new file mode 100644 index 0000000..4f7b000 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs @@ -0,0 +1,27 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public abstract class PenumbraBase : IpcInteropBase +{ + protected PenumbraBase( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator) : base(logger) + { + PluginInterface = pluginInterface; + DalamudUtil = dalamudUtil; + Mediator = mediator; + } + + protected IDalamudPluginInterface PluginInterface { get; } + + protected DalamudUtilService DalamudUtil { get; } + + protected LightlessMediator Mediator { get; } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs new file mode 100644 index 0000000..e5c28e2 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs @@ -0,0 +1,197 @@ +using System.Collections.Concurrent; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraCollections : PenumbraBase +{ + private readonly CreateTemporaryCollection _createNamedTemporaryCollection; + private readonly AssignTemporaryCollection _assignTemporaryCollection; + private readonly DeleteTemporaryCollection _removeTemporaryCollection; + private readonly AddTemporaryMod _addTemporaryMod; + private readonly RemoveTemporaryMod _removeTemporaryMod; + private readonly GetCollections _getCollections; + private readonly ConcurrentDictionary _activeTemporaryCollections = new(); + + private int _cleanupScheduled; + + public PenumbraCollections( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _createNamedTemporaryCollection = new CreateTemporaryCollection(pluginInterface); + _assignTemporaryCollection = new AssignTemporaryCollection(pluginInterface); + _removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface); + _addTemporaryMod = new AddTemporaryMod(pluginInterface); + _removeTemporaryMod = new RemoveTemporaryMod(pluginInterface); + _getCollections = new GetCollections(pluginInterface); + } + + public override string Name => "Penumbra.Collections"; + + public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + var result = _assignTemporaryCollection.Invoke(collectionId, objectIndex, forceAssignment: true); + logger.LogTrace("Assigning Temp Collection {CollectionId} to index {ObjectIndex}, Success: {Result}", collectionId, objectIndex, result); + return result; + }).ConfigureAwait(false); + } + + public async Task CreateTemporaryCollectionAsync(ILogger logger, string uid) + { + if (!IsAvailable) + { + return Guid.Empty; + } + + var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() => + { + var name = $"Lightless_{uid}"; + _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId); + logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId); + return (tempCollectionId, name); + }).ConfigureAwait(false); + + if (collectionId != Guid.Empty) + { + _activeTemporaryCollections[collectionId] = collectionName; + } + + return collectionId; + } + + public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("[{ApplicationId}] Removing temp collection for {CollectionId}", applicationId, collectionId); + var result = _removeTemporaryCollection.Invoke(collectionId); + logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result); + }).ConfigureAwait(false); + + _activeTemporaryCollections.TryRemove(collectionId, out _); + } + + public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary modPaths) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + foreach (var mod in modPaths) + { + logger.LogTrace("[{ApplicationId}] Change: {From} => {To}", applicationId, mod.Key, mod.Value); + } + + var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0); + logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult); + + var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary(modPaths), string.Empty, 0); + logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult); + }).ConfigureAwait(false); + } + + public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("[{ApplicationId}] Manip: {Data}", applicationId, manipulationData); + var result = _addTemporaryMod.Invoke("LightlessChara_Meta", collectionId, [], manipulationData, 0); + logger.LogTrace("[{ApplicationId}] Setting temp meta mod for {CollectionId}, Success: {Result}", applicationId, collectionId, result); + }).ConfigureAwait(false); + } + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + if (current == IpcConnectionState.Available) + { + ScheduleCleanup(); + } + else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available) + { + Interlocked.Exchange(ref _cleanupScheduled, 0); + } + } + + private void ScheduleCleanup() + { + if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0) + { + return; + } + + _ = Task.Run(CleanupTemporaryCollectionsAsync); + } + + private async Task CleanupTemporaryCollectionsAsync() + { + if (!IsAvailable) + { + return; + } + + try + { + var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false); + foreach (var (collectionId, name) in collections) + { + if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId)) + { + continue; + } + + Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId); + var deleteResult = await DalamudUtil.RunOnFrameworkThread(() => + { + var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId); + Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result); + return result; + }).ConfigureAwait(false); + + if (deleteResult == PenumbraApiEc.Success) + { + _activeTemporaryCollections.TryRemove(collectionId, out _); + } + else + { + Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult); + } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections"); + } + } + + private static bool IsLightlessCollectionName(string? name) + => !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal); +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs new file mode 100644 index 0000000..7b3abd1 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs @@ -0,0 +1,81 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.PlayerData.Handlers; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraRedraw : PenumbraBase +{ + private readonly RedrawManager _redrawManager; + private readonly RedrawObject _penumbraRedraw; + private readonly EventSubscriber _penumbraObjectIsRedrawn; + + public PenumbraRedraw( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + RedrawManager redrawManager) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _redrawManager = redrawManager; + + _penumbraRedraw = new RedrawObject(pluginInterface); + _penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pluginInterface, HandlePenumbraRedrawEvent); + } + + public override string Name => "Penumbra.Redraw"; + + public void CancelPendingRedraws() + => _redrawManager.Cancel(); + + public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType) + { + if (!IsAvailable) + { + return; + } + + _penumbraRedraw.Invoke(objectIndex, redrawType); + } + + public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + { + if (!IsAvailable || DalamudUtil.IsZoning) + { + return; + } + + try + { + await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false); + await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, chara => + { + logger.LogDebug("[{ApplicationId}] Calling on IPC: PenumbraRedraw", applicationId); + _penumbraRedraw.Invoke(chara.ObjectIndex, RedrawType.Redraw); + }, token).ConfigureAwait(false); + } + finally + { + _redrawManager.RedrawSemaphore.Release(); + } + } + + private void HandlePenumbraRedrawEvent(IntPtr objectAddress, int objectTableIndex) + => Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, false)); + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + } + + public override void Dispose() + { + base.Dispose(); + _penumbraObjectIsRedrawn.Dispose(); + } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs new file mode 100644 index 0000000..75d1d86 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs @@ -0,0 +1,141 @@ +using System.Collections.Concurrent; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.PlayerData.Handlers; +using LightlessSync.Services; +using LightlessSync.Services.ActorTracking; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraResource : PenumbraBase +{ + private readonly ActorObjectService _actorObjectService; + private readonly GetGameObjectResourcePaths _gameObjectResourcePaths; + private readonly ResolvePlayerPathsAsync _resolvePlayerPaths; + private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations; + private readonly EventSubscriber _gameObjectResourcePathResolved; + private readonly ConcurrentDictionary _trackedActors = new(); + + public PenumbraResource( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + ActorObjectService actorObjectService) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _actorObjectService = actorObjectService; + _gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface); + _resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface); + _getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface); + _gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded); + + foreach (var descriptor in _actorObjectService.PlayerDescriptors) + { + TrackActor(descriptor.Address); + } + } + + public override string Name => "Penumbra.Resources"; + + public async Task>?> GetCharacterDataAsync(ILogger logger, GameObjectHandler handler) + { + if (!IsAvailable) + { + return null; + } + + return await DalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); + var idx = handler.GetGameObject()?.ObjectIndex; + if (idx == null) + { + return null; + } + + return _gameObjectResourcePaths.Invoke(idx.Value)[0]; + }).ConfigureAwait(false); + } + + public string GetMetaManipulations() + => IsAvailable ? _getPlayerMetaManipulations.Invoke() : string.Empty; + + public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forwardPaths, string[] reversePaths) + { + if (!IsAvailable) + { + return (Array.Empty(), Array.Empty()); + } + + return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false); + } + + public void TrackActor(nint address) + { + if (address != nint.Zero) + { + _trackedActors[(IntPtr)address] = 0; + } + } + + public void UntrackActor(nint address) + { + if (address != nint.Zero) + { + _trackedActors.TryRemove((IntPtr)address, out _); + } + } + + private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath) + { + if (ptr == nint.Zero) + { + return; + } + + if (!_trackedActors.ContainsKey(ptr)) + { + var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr); + if (descriptor.Address != nint.Zero) + { + _trackedActors[ptr] = 0; + } + else + { + return; + } + } + + if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0) + { + return; + } + + Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath)); + } + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + if (current != IpcConnectionState.Available) + { + _trackedActors.Clear(); + } + else + { + foreach (var descriptor in _actorObjectService.PlayerDescriptors) + { + TrackActor(descriptor.Address); + } + } + } + + public override void Dispose() + { + base.Dispose(); + _gameObjectResourcePathResolved.Dispose(); + } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs new file mode 100644 index 0000000..e12fd7b --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs @@ -0,0 +1,121 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraTexture : PenumbraBase +{ + private readonly PenumbraRedraw _redrawFeature; + private readonly ConvertTextureFile _convertTextureFile; + + public PenumbraTexture( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + PenumbraRedraw redrawFeature) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _redrawFeature = redrawFeature; + _convertTextureFile = new ConvertTextureFile(pluginInterface); + } + + public override string Name => "Penumbra.Textures"; + + public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token) + { + if (!IsAvailable || jobs.Count == 0) + { + return; + } + + Mediator.Publish(new HaltScanMessage(nameof(ConvertTextureFilesAsync))); + + var totalJobs = jobs.Count; + var completedJobs = 0; + + try + { + foreach (var job in jobs) + { + if (token.IsCancellationRequested) + { + break; + } + + progress?.Report(new TextureConversionProgress(completedJobs, totalJobs, job)); + await ConvertSingleJobAsync(logger, job, token).ConfigureAwait(false); + completedJobs++; + } + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync))); + } + + if (completedJobs > 0 && !token.IsCancellationRequested) + { + await DalamudUtil.RunOnFrameworkThread(async () => + { + var player = await DalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false); + if (player == null) + { + return; + } + + var gameObject = await DalamudUtil.CreateGameObjectAsync(player).ConfigureAwait(false); + if (gameObject == null) + { + return; + } + + _redrawFeature.RequestImmediateRedraw(gameObject.ObjectIndex, RedrawType.Redraw); + }).ConfigureAwait(false); + } + } + + public async Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token) + { + if (!IsAvailable) + { + return; + } + + await ConvertSingleJobAsync(Logger, job, token).ConfigureAwait(false); + } + + private async Task ConvertSingleJobAsync(ILogger logger, TextureConversionJob job, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType); + var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps); + await convertTask.ConfigureAwait(false); + + if (!convertTask.IsCompletedSuccessfully || job.DuplicateTargets is not { Count: > 0 }) + { + return; + } + + foreach (var duplicate in job.DuplicateTargets) + { + try + { + logger.LogInformation("Synchronizing duplicate {Duplicate}", duplicate); + File.Copy(job.OutputFile, duplicate, overwrite: true); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to copy duplicate {Duplicate}", duplicate); + } + } + } + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + } +} diff --git a/LightlessSync/PlayerData/Factories/PairFactory.cs b/LightlessSync/PlayerData/Factories/PairFactory.cs index fd63f51..a7ffd6e 100644 --- a/LightlessSync/PlayerData/Factories/PairFactory.cs +++ b/LightlessSync/PlayerData/Factories/PairFactory.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto.User; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; diff --git a/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs b/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs index a11893b..cd62f98 100644 --- a/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs +++ b/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs @@ -2,6 +2,9 @@ using LightlessSync.API.Data; namespace LightlessSync.PlayerData.Pairs; +/// +/// performance metrics for each pair handler +/// public interface IPairPerformanceSubject { string Ident { get; } diff --git a/LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs b/LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs deleted file mode 100644 index a5c5eff..0000000 --- a/LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LightlessSync.PlayerData.Pairs; - -public record OptionalPluginWarning -{ - public bool ShownHeelsWarning { get; set; } = false; - public bool ShownCustomizePlusWarning { get; set; } = false; - public bool ShownHonorificWarning { get; set; } = false; - public bool ShownMoodlesWarning { get; set; } = false; - public bool ShowPetNicknamesWarning { get; set; } = false; -} \ No newline at end of file diff --git a/LightlessSync/PlayerData/Pairs/Pair.cs b/LightlessSync/PlayerData/Pairs/Pair.cs index 7709b06..a861dae 100644 --- a/LightlessSync/PlayerData/Pairs/Pair.cs +++ b/LightlessSync/PlayerData/Pairs/Pair.cs @@ -1,12 +1,9 @@ -using System; -using System.Linq; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Text.SeStringHandling; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.User; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using Microsoft.Extensions.Logging; @@ -14,6 +11,9 @@ using LightlessSync.WebAPI; namespace LightlessSync.PlayerData.Pairs; +/// +/// ui wrapper around a pair connection +/// public class Pair { private readonly PairLedger _pairLedger; diff --git a/LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs b/LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs new file mode 100644 index 0000000..7bdfc23 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs @@ -0,0 +1,136 @@ +using LightlessSync.API.Dto.Group; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// handles group related pair events +/// +public sealed partial class PairCoordinator +{ + public void HandleGroupChangePermissions(GroupPermissionDto dto) + { + var result = _pairManager.UpdateGroupPermissions(dto); + if (!result.Success) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update permissions for group {GroupId}: {Error}", dto.Group.GID, result.Error); + } + return; + } + + PublishPairDataChanged(groupChanged: true); + } + + public void HandleGroupFullInfo(GroupFullInfoDto dto) + { + var result = _pairManager.AddGroup(dto); + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to add group {GroupId}: {Error}", dto.Group.GID, result.Error); + return; + } + + PublishPairDataChanged(groupChanged: true); + } + + public void HandleGroupPairJoined(GroupPairFullInfoDto dto) + { + var result = _pairManager.AddOrUpdateGroupPair(dto); + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to add group pair {Uid}/{Group}: {Error}", dto.User.UID, dto.Group.GID, result.Error); + return; + } + + PublishPairDataChanged(groupChanged: true); + } + + public void HandleGroupPairLeft(GroupPairDto dto) + { + var deregistration = _pairManager.RemoveGroupPair(dto); + if (deregistration.Success && deregistration.Value is { } registration && registration.CharacterIdent is not null) + { + _ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true); + } + else if (!deregistration.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("RemoveGroupPair failed for {Uid}: {Error}", dto.User.UID, deregistration.Error); + } + + if (deregistration.Success) + { + PublishPairDataChanged(groupChanged: true); + } + } + + public void HandleGroupRemoved(GroupDto dto) + { + var removalResult = _pairManager.RemoveGroup(dto.Group.GID); + if (removalResult.Success) + { + foreach (var registration in removalResult.Value) + { + if (registration.CharacterIdent is not null) + { + _ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true); + } + } + } + else if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to remove group {Group}: {Error}", dto.Group.GID, removalResult.Error); + } + + if (removalResult.Success) + { + PublishPairDataChanged(groupChanged: true); + } + } + + public void HandleGroupInfoUpdate(GroupInfoDto dto) + { + var result = _pairManager.UpdateGroupInfo(dto); + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update group info for {Group}: {Error}", dto.Group.GID, result.Error); + return; + } + + PublishPairDataChanged(groupChanged: true); + } + + public void HandleGroupPairPermissions(GroupPairUserPermissionDto dto) + { + var result = _pairManager.UpdateGroupPairPermissions(dto); + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update group pair permissions for {Group}: {Error}", dto.Group.GID, result.Error); + return; + } + + PublishPairDataChanged(groupChanged: true); + } + + public void HandleGroupPairStatus(GroupPairUserInfoDto dto, bool isSelf) + { + PairOperationResult result; + if (isSelf) + { + result = _pairManager.UpdateGroupStatus(dto); + } + else + { + result = _pairManager.UpdateGroupPairStatus(dto); + } + + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update group status for {Group}:{Uid}: {Error}", dto.GID, dto.UID, result.Error); + return; + } + + PublishPairDataChanged(groupChanged: true); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs b/LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs new file mode 100644 index 0000000..0891035 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs @@ -0,0 +1,302 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.User; +using LightlessSync.Services.Events; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// handles user pair events +/// +public sealed partial class PairCoordinator +{ + public void HandleUserAddPair(UserPairDto dto, bool addToLastAddedUser = true) + { + var result = _pairManager.AddOrUpdateIndividual(dto, addToLastAddedUser); + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to add/update pair {Uid}: {Error}", dto.User.UID, result.Error); + return; + } + + PublishPairDataChanged(); + } + + public void HandleUserAddPair(UserFullPairDto dto) + { + var result = _pairManager.AddOrUpdateIndividual(dto); + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to add/update full pair {Uid}: {Error}", dto.User.UID, result.Error); + return; + } + + PublishPairDataChanged(); + } + + public void HandleUserRemovePair(UserDto dto) + { + var removal = _pairManager.RemoveIndividual(dto); + if (removal.Success && removal.Value is { } registration && registration.CharacterIdent is not null) + { + _ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true); + } + else if (!removal.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("RemoveIndividual failed for {Uid}: {Error}", dto.User.UID, removal.Error); + } + + if (removal.Success) + { + _pendingCharacterData.TryRemove(dto.User.UID, out _); + PublishPairDataChanged(); + } + } + + public void HandleUserStatus(UserIndividualPairStatusDto dto) + { + var result = _pairManager.SetIndividualStatus(dto); + if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update individual pair status for {Uid}: {Error}", dto.User.UID, result.Error); + return; + } + + PublishPairDataChanged(); + } + + public void HandleUserOnline(OnlineUserIdentDto dto, bool sendNotification) + { + var wasOnline = false; + PairConnection? previousConnection = null; + if (_pairManager.TryGetPair(dto.User.UID, out var existingConnection)) + { + previousConnection = existingConnection; + wasOnline = existingConnection.IsOnline; + } + + var registrationResult = _pairManager.MarkOnline(dto); + if (!registrationResult.Success) + { + _logger.LogDebug("MarkOnline failed for {Uid}: {Error}", dto.User.UID, registrationResult.Error); + return; + } + + var registration = registrationResult.Value; + if (registration.CharacterIdent is null) + { + _logger.LogDebug("Online registration for {Uid} missing ident.", dto.User.UID); + } + else + { + var handlerResult = _handlerRegistry.RegisterOnlinePair(registration); + if (!handlerResult.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("RegisterOnlinePair failed for {Uid}: {Error}", dto.User.UID, handlerResult.Error); + } + } + + var connectionResult = _pairManager.GetPair(dto.User.UID); + var connection = connectionResult.Success ? connectionResult.Value : previousConnection; + if (connection is not null) + { + _mediator.Publish(new ClearProfileUserDataMessage(connection.User)); + } + else + { + _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); + } + + if (!wasOnline) + { + NotifyUserOnline(connection, sendNotification); + } + + if (registration.CharacterIdent is not null && + _pendingCharacterData.TryRemove(dto.User.UID, out var pendingData)) + { + var pendingRegistration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), registration.CharacterIdent); + var pendingApply = _handlerRegistry.ApplyCharacterData(pendingRegistration, pendingData); + if (!pendingApply.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Applying pending character data for {Uid} failed: {Error}", dto.User.UID, pendingApply.Error); + } + } + + PublishPairDataChanged(); + } + + public void HandleUserOffline(UserData user) + { + var registrationResult = _pairManager.MarkOffline(user); + if (registrationResult.Success) + { + _pendingCharacterData.TryRemove(user.UID, out _); + if (registrationResult.Value.CharacterIdent is not null) + { + _ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value); + } + + _mediator.Publish(new ClearProfileUserDataMessage(user)); + PublishPairDataChanged(); + } + else if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("MarkOffline failed for {Uid}: {Error}", user.UID, registrationResult.Error); + } + } + + public void HandleUserPermissions(UserPermissionsDto dto) + { + var pairResult = _pairManager.GetPair(dto.User.UID); + if (!pairResult.Success) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Permission update received for unknown pair {Uid}", dto.User.UID); + } + return; + } + + var connection = pairResult.Value; + var previous = connection.OtherToSelfPermissions; + + var updateResult = _pairManager.UpdateOtherPermissions(dto); + if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error); + return; + } + + PublishPairDataChanged(); + + if (previous.IsPaused() != dto.Permissions.IsPaused()) + { + _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); + + if (connection.Ident is not null) + { + var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused()); + if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error); + } + } + } + + if (!connection.IsPaused && connection.Ident is not null) + { + ReapplyLastKnownData(dto.User.UID, connection.Ident); + } + } + + public void HandleSelfPermissions(UserPermissionsDto dto) + { + var pairResult = _pairManager.GetPair(dto.User.UID); + if (!pairResult.Success) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Self permission update received for unknown pair {Uid}", dto.User.UID); + } + return; + } + + var connection = pairResult.Value; + var previous = connection.SelfToOtherPermissions; + + var updateResult = _pairManager.UpdateSelfPermissions(dto); + if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update self permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error); + return; + } + + PublishPairDataChanged(); + + if (previous.IsPaused() != dto.Permissions.IsPaused()) + { + _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); + + if (connection.Ident is not null) + { + var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused()); + if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error); + } + } + } + + if (!connection.IsPaused && connection.Ident is not null) + { + ReapplyLastKnownData(dto.User.UID, connection.Ident); + } + } + + public void HandleUploadStatus(UserDto dto) + { + var pairResult = _pairManager.GetPair(dto.User.UID); + if (!pairResult.Success) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Upload status received for unknown pair {Uid}", dto.User.UID); + } + return; + } + + var connection = pairResult.Value; + if (connection.Ident is null) + { + return; + } + + var setResult = _handlerRegistry.SetUploading(new PairUniqueIdentifier(dto.User.UID), connection.Ident, true); + if (!setResult.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Failed to set uploading for {Uid}: {Error}", dto.User.UID, setResult.Error); + } + } + + public void HandleCharacterData(OnlineUserCharaDataDto dto) + { + var pairResult = _pairManager.GetPair(dto.User.UID); + if (!pairResult.Success) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Character data received for unknown pair {Uid}, queued for later.", dto.User.UID); + } + _pendingCharacterData[dto.User.UID] = dto; + return; + } + + var connection = pairResult.Value; + _mediator.Publish(new EventMessage(new Event(connection.User, nameof(PairCoordinator), EventSeverity.Informational, "Received Character Data"))); + if (connection.Ident is null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Character data received for {Uid} without ident, queued for later.", dto.User.UID); + } + _pendingCharacterData[dto.User.UID] = dto; + return; + } + + _pendingCharacterData.TryRemove(dto.User.UID, out _); + var registration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident); + var applyResult = _handlerRegistry.ApplyCharacterData(registration, dto); + if (!applyResult.Success && _logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("ApplyCharacterData queued for {Uid}: {Error}", dto.User.UID, applyResult.Error); + } + } + + public void HandleProfile(UserDto dto) + { + _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairCoordinator.cs b/LightlessSync/PlayerData/Pairs/PairCoordinator.cs index ddc4adb..7774851 100644 --- a/LightlessSync/PlayerData/Pairs/PairCoordinator.cs +++ b/LightlessSync/PlayerData/Pairs/PairCoordinator.cs @@ -1,21 +1,17 @@ -using System; using System.Collections.Concurrent; -using LightlessSync.API.Data; -using LightlessSync.API.Data.Enum; -using LightlessSync.API.Data.Extensions; -using LightlessSync.API.Dto.CharaData; -using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services.Mediator; -using LightlessSync.Services.Events; using LightlessSync.Services.ServerConfiguration; using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; -public sealed class PairCoordinator : MediatorSubscriberBase +/// +/// wires mediator events into the pair system +/// +public sealed partial class PairCoordinator : MediatorSubscriberBase { private readonly ILogger _logger; private readonly LightlessConfigService _configService; @@ -107,45 +103,6 @@ public sealed class PairCoordinator : MediatorSubscriberBase } } - public void HandleGroupChangePermissions(GroupPermissionDto dto) - { - var result = _pairManager.UpdateGroupPermissions(dto); - if (!result.Success) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update permissions for group {GroupId}: {Error}", dto.Group.GID, result.Error); - } - return; - } - - PublishPairDataChanged(groupChanged: true); - } - - public void HandleGroupFullInfo(GroupFullInfoDto dto) - { - var result = _pairManager.AddGroup(dto); - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to add group {GroupId}: {Error}", dto.Group.GID, result.Error); - return; - } - - PublishPairDataChanged(groupChanged: true); - } - - public void HandleGroupPairJoined(GroupPairFullInfoDto dto) - { - var result = _pairManager.AddOrUpdateGroupPair(dto); - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to add group pair {Uid}/{Group}: {Error}", dto.User.UID, dto.Group.GID, result.Error); - return; - } - - PublishPairDataChanged(groupChanged: true); - } - private void HandleActiveServerChange(string serverUrl) { if (_logger.IsEnabled(LogLevel.Debug)) @@ -175,379 +132,4 @@ public sealed class PairCoordinator : MediatorSubscriberBase _mediator.Publish(new ClearProfileGroupDataMessage()); PublishPairDataChanged(groupChanged: true); } - - public void HandleGroupPairLeft(GroupPairDto dto) - { - var deregistration = _pairManager.RemoveGroupPair(dto); - if (deregistration.Success && deregistration.Value is { } registration && registration.CharacterIdent is not null) - { - _ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true); - } - else if (!deregistration.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("RemoveGroupPair failed for {Uid}: {Error}", dto.User.UID, deregistration.Error); - } - - if (deregistration.Success) - { - PublishPairDataChanged(groupChanged: true); - } - } - - public void HandleGroupRemoved(GroupDto dto) - { - var removalResult = _pairManager.RemoveGroup(dto.Group.GID); - if (removalResult.Success) - { - foreach (var registration in removalResult.Value) - { - if (registration.CharacterIdent is not null) - { - _ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true); - } - } - } - else if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to remove group {Group}: {Error}", dto.Group.GID, removalResult.Error); - } - - if (removalResult.Success) - { - PublishPairDataChanged(groupChanged: true); - } - } - - public void HandleGroupInfoUpdate(GroupInfoDto dto) - { - var result = _pairManager.UpdateGroupInfo(dto); - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update group info for {Group}: {Error}", dto.Group.GID, result.Error); - return; - } - - PublishPairDataChanged(groupChanged: true); - } - - public void HandleGroupPairPermissions(GroupPairUserPermissionDto dto) - { - var result = _pairManager.UpdateGroupPairPermissions(dto); - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update group pair permissions for {Group}: {Error}", dto.Group.GID, result.Error); - return; - } - - PublishPairDataChanged(groupChanged: true); - } - - public void HandleGroupPairStatus(GroupPairUserInfoDto dto, bool isSelf) - { - PairOperationResult result; - if (isSelf) - { - result = _pairManager.UpdateGroupStatus(dto); - } - else - { - result = _pairManager.UpdateGroupPairStatus(dto); - } - - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update group status for {Group}:{Uid}: {Error}", dto.GID, dto.UID, result.Error); - return; - } - - PublishPairDataChanged(groupChanged: true); - } - - public void HandleUserAddPair(UserPairDto dto, bool addToLastAddedUser = true) - { - var result = _pairManager.AddOrUpdateIndividual(dto, addToLastAddedUser); - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to add/update pair {Uid}: {Error}", dto.User.UID, result.Error); - return; - } - - PublishPairDataChanged(); - } - - public void HandleUserAddPair(UserFullPairDto dto) - { - var result = _pairManager.AddOrUpdateIndividual(dto); - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to add/update full pair {Uid}: {Error}", dto.User.UID, result.Error); - return; - } - - PublishPairDataChanged(); - } - - public void HandleUserRemovePair(UserDto dto) - { - var removal = _pairManager.RemoveIndividual(dto); - if (removal.Success && removal.Value is { } registration && registration.CharacterIdent is not null) - { - _ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true); - } - else if (!removal.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("RemoveIndividual failed for {Uid}: {Error}", dto.User.UID, removal.Error); - } - - if (removal.Success) - { - _pendingCharacterData.TryRemove(dto.User.UID, out _); - PublishPairDataChanged(); - } - } - - public void HandleUserStatus(UserIndividualPairStatusDto dto) - { - var result = _pairManager.SetIndividualStatus(dto); - if (!result.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update individual pair status for {Uid}: {Error}", dto.User.UID, result.Error); - return; - } - - PublishPairDataChanged(); - } - - public void HandleUserOnline(OnlineUserIdentDto dto, bool sendNotification) - { - var wasOnline = false; - PairConnection? previousConnection = null; - if (_pairManager.TryGetPair(dto.User.UID, out var existingConnection)) - { - previousConnection = existingConnection; - wasOnline = existingConnection.IsOnline; - } - - var registrationResult = _pairManager.MarkOnline(dto); - if (!registrationResult.Success) - { - _logger.LogDebug("MarkOnline failed for {Uid}: {Error}", dto.User.UID, registrationResult.Error); - return; - } - - var registration = registrationResult.Value; - if (registration.CharacterIdent is null) - { - _logger.LogDebug("Online registration for {Uid} missing ident.", dto.User.UID); - } - else - { - var handlerResult = _handlerRegistry.RegisterOnlinePair(registration); - if (!handlerResult.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("RegisterOnlinePair failed for {Uid}: {Error}", dto.User.UID, handlerResult.Error); - } - } - - var connectionResult = _pairManager.GetPair(dto.User.UID); - var connection = connectionResult.Success ? connectionResult.Value : previousConnection; - if (connection is not null) - { - _mediator.Publish(new ClearProfileUserDataMessage(connection.User)); - } - else - { - _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); - } - - if (!wasOnline) - { - NotifyUserOnline(connection, sendNotification); - } - - if (registration.CharacterIdent is not null && - _pendingCharacterData.TryRemove(dto.User.UID, out var pendingData)) - { - var pendingRegistration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), registration.CharacterIdent); - var pendingApply = _handlerRegistry.ApplyCharacterData(pendingRegistration, pendingData); - if (!pendingApply.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Applying pending character data for {Uid} failed: {Error}", dto.User.UID, pendingApply.Error); - } - } - - PublishPairDataChanged(); - } - - public void HandleUserOffline(UserData user) - { - var registrationResult = _pairManager.MarkOffline(user); - if (registrationResult.Success) - { - _pendingCharacterData.TryRemove(user.UID, out _); - if (registrationResult.Value.CharacterIdent is not null) - { - _ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value); - } - - _mediator.Publish(new ClearProfileUserDataMessage(user)); - PublishPairDataChanged(); - } - else if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("MarkOffline failed for {Uid}: {Error}", user.UID, registrationResult.Error); - } - } - - public void HandleUserPermissions(UserPermissionsDto dto) - { - var pairResult = _pairManager.GetPair(dto.User.UID); - if (!pairResult.Success) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Permission update received for unknown pair {Uid}", dto.User.UID); - } - return; - } - - var connection = pairResult.Value; - var previous = connection.OtherToSelfPermissions; - - var updateResult = _pairManager.UpdateOtherPermissions(dto); - if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error); - return; - } - - PublishPairDataChanged(); - - if (previous.IsPaused() != dto.Permissions.IsPaused()) - { - _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); - - if (connection.Ident is not null) - { - var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused()); - if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error); - } - } - } - - if (!connection.IsPaused && connection.Ident is not null) - { - ReapplyLastKnownData(dto.User.UID, connection.Ident); - } - } - - public void HandleSelfPermissions(UserPermissionsDto dto) - { - var pairResult = _pairManager.GetPair(dto.User.UID); - if (!pairResult.Success) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Self permission update received for unknown pair {Uid}", dto.User.UID); - } - return; - } - - var connection = pairResult.Value; - var previous = connection.SelfToOtherPermissions; - - var updateResult = _pairManager.UpdateSelfPermissions(dto); - if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update self permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error); - return; - } - - PublishPairDataChanged(); - - if (previous.IsPaused() != dto.Permissions.IsPaused()) - { - _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); - - if (connection.Ident is not null) - { - var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused()); - if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error); - } - } - } - - if (!connection.IsPaused && connection.Ident is not null) - { - ReapplyLastKnownData(dto.User.UID, connection.Ident); - } - } - - public void HandleUploadStatus(UserDto dto) - { - var pairResult = _pairManager.GetPair(dto.User.UID); - if (!pairResult.Success) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Upload status received for unknown pair {Uid}", dto.User.UID); - } - return; - } - - var connection = pairResult.Value; - if (connection.Ident is null) - { - return; - } - - var setResult = _handlerRegistry.SetUploading(new PairUniqueIdentifier(dto.User.UID), connection.Ident, true); - if (!setResult.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Failed to set uploading for {Uid}: {Error}", dto.User.UID, setResult.Error); - } - } - - public void HandleCharacterData(OnlineUserCharaDataDto dto) - { - var pairResult = _pairManager.GetPair(dto.User.UID); - if (!pairResult.Success) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Character data received for unknown pair {Uid}, queued for later.", dto.User.UID); - } - _pendingCharacterData[dto.User.UID] = dto; - return; - } - - var connection = pairResult.Value; - _mediator.Publish(new EventMessage(new Event(connection.User, nameof(PairCoordinator), EventSeverity.Informational, "Received Character Data"))); - if (connection.Ident is null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Character data received for {Uid} without ident, queued for later.", dto.User.UID); - } - _pendingCharacterData[dto.User.UID] = dto; - return; - } - - _pendingCharacterData.TryRemove(dto.User.UID, out _); - var registration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident); - var applyResult = _handlerRegistry.ApplyCharacterData(registration, dto); - if (!applyResult.Success && _logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("ApplyCharacterData queued for {Uid}: {Error}", dto.User.UID, applyResult.Error); - } - } - - public void HandleProfile(UserDto dto) - { - _mediator.Publish(new ClearProfileUserDataMessage(dto.User)); - } } diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 2114c35..ad77bcb 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1,11 +1,5 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; @@ -28,6 +22,9 @@ using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; namespace LightlessSync.PlayerData.Pairs; +/// +/// orchestrates the lifecycle of a paired character +/// public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject { string Ident { get; } @@ -80,6 +77,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private CancellationTokenSource? _downloadCancellationTokenSource = new(); private bool _forceApplyMods = false; private bool _forceFullReapply; + private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths; + private bool _needsCollectionRebuild; private bool _isVisible; private Guid _penumbraCollection; private readonly object _collectionGate = new(); @@ -352,12 +351,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null) { Guid toRelease = Guid.Empty; + bool hadCollection = false; lock (_collectionGate) { if (_penumbraCollection != Guid.Empty) { toRelease = _penumbraCollection; _penumbraCollection = Guid.Empty; + hadCollection = true; } } @@ -365,6 +366,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (cached.HasValue && cached.Value != Guid.Empty) { toRelease = cached.Value; + hadCollection = true; + } + + if (hadCollection) + { + _needsCollectionRebuild = true; + _forceFullReapply = true; } if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable) @@ -603,6 +611,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return data; } + private bool HasValidCachedModdedPaths() + { + if (_lastAppliedModdedPaths is null || _lastAppliedModdedPaths.Count == 0) + { + return false; + } + + foreach (var entry in _lastAppliedModdedPaths) + { + if (string.IsNullOrEmpty(entry.Value) || !File.Exists(entry.Value)) + { + Logger.LogDebug("Cached file path {path} missing for {handler}, forcing recalculation", entry.Value ?? "empty", GetLogIdentifier()); + return false; + } + } + + return true; + } + private bool CanApplyNow() { return !_dalamudUtil.IsInCombat @@ -847,6 +874,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { PlayerName = null; _cachedData = null; + _lastAppliedModdedPaths = null; + _needsCollectionRebuild = false; Logger.LogDebug("Disposing {name} complete", name); } } @@ -1015,73 +1044,103 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles)); var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip)); + var needsCollectionRebuild = _needsCollectionRebuild; + var reuseCachedModdedPaths = !updateModdedPaths && needsCollectionRebuild && _lastAppliedModdedPaths is not null; + updateModdedPaths = updateModdedPaths || needsCollectionRebuild; + updateManip = updateManip || needsCollectionRebuild; + Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths = null; + if (reuseCachedModdedPaths) + { + if (HasValidCachedModdedPaths()) + { + cachedModdedPaths = _lastAppliedModdedPaths; + } + else + { + Logger.LogDebug("{handler}: Cached files missing, recalculating mappings", GetLogIdentifier()); + _lastAppliedModdedPaths = null; + } + } _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); var downloadToken = _downloadCancellationTokenSource.Token; - _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false); + _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken).ConfigureAwait(false); } private Task? _pairDownloadTask; private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, - bool updateModdedPaths, bool updateManip, CancellationToken downloadToken) + bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) { await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); - Dictionary<(string GamePath, string? Hash), string> moddedPaths = []; bool skipDownscaleForPair = ShouldSkipDownscale(); var user = GetPrimaryUserData(); + Dictionary<(string GamePath, string? Hash), string> moddedPaths; if (updateModdedPaths) { - int attempts = 0; - List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); - - while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) + if (cachedModdedPaths is not null) { - if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) + moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer); + } + else + { + int attempts = 0; + List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + + while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) { - Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData); + if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) + { + Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData); + await _pairDownloadTask.ConfigureAwait(false); + } + + Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); + + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational, + $"Starting download for {toDownloadReplacements.Count} files"))); + var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); + + if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) + { + _downloadManager.ClearDownload(); + return; + } + + var handlerForDownload = _charaHandler; + _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); + await _pairDownloadTask.ConfigureAwait(false); + + if (downloadToken.IsCancellationRequested) + { + Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); + return; + } + + toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + + if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) + { + break; + } + + await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false); } - Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); - - Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational, - $"Starting download for {toDownloadReplacements.Count} files"))); - var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); - - if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) + if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) { - _downloadManager.ClearDownload(); return; } - - var handlerForDownload = _charaHandler; - _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); - - await _pairDownloadTask.ConfigureAwait(false); - - if (downloadToken.IsCancellationRequested) - { - Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); - return; - } - - toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); - - if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) - { - break; - } - - await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false); - } - - if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) - { - return; } } + else + { + moddedPaths = cachedModdedPaths is not null + ? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer) + : []; + } downloadToken.ThrowIfCancellationRequested(); @@ -1165,6 +1224,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection, moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false); + _lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer); LastAppliedDataBytes = -1; foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) { @@ -1190,6 +1250,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _cachedData = charaData; _pairStateCache.Store(Ident, charaData); _forceFullReapply = false; + _needsCollectionRebuild = false; if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) { _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List()); diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs index 6c43119..97e3733 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs @@ -1,22 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; -using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.User; using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; +/// +/// creates, tracks, and removes pair handlers +/// public sealed class PairHandlerRegistry : IDisposable { private readonly object _gate = new(); - private readonly Dictionary _identToHandler = new(StringComparer.Ordinal); - private readonly Dictionary> _handlerToPairs = new(); - private readonly Dictionary _waitingRequests = new(StringComparer.Ordinal); + private readonly Dictionary _entriesByIdent = new(StringComparer.Ordinal); + private readonly Dictionary _entriesByHandler = new(); private readonly IPairHandlerAdapterFactory _handlerFactory; private readonly PairManager _pairManager; @@ -24,7 +19,6 @@ public sealed class PairHandlerRegistry : IDisposable private readonly ILogger _logger; private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5); - private readonly TimeSpan _waitForHandlerGracePeriod = TimeSpan.FromMinutes(2); public PairHandlerRegistry( IPairHandlerAdapterFactory handlerFactory, @@ -42,7 +36,7 @@ public sealed class PairHandlerRegistry : IDisposable { lock (_gate) { - return _handlerToPairs.Keys.Count(handler => handler.IsVisible); + return _entriesByHandler.Keys.Count(handler => handler.IsVisible); } } @@ -50,7 +44,7 @@ public sealed class PairHandlerRegistry : IDisposable { lock (_gate) { - return _identToHandler.TryGetValue(ident, out var handler) && handler.IsVisible; + return _entriesByIdent.TryGetValue(ident, out var entry) && entry.Handler.IsVisible; } } @@ -64,16 +58,10 @@ public sealed class PairHandlerRegistry : IDisposable IPairHandlerAdapter handler; lock (_gate) { - handler = GetOrAddHandler(registration.CharacterIdent); + var entry = GetOrCreateEntry(registration.CharacterIdent); + handler = entry.Handler; handler.ScheduledForDeletion = false; - - if (!_handlerToPairs.TryGetValue(handler, out var set)) - { - set = new HashSet(); - _handlerToPairs[handler] = set; - } - - set.Add(registration.PairIdent); + entry.AddPair(registration.PairIdent); } ApplyPauseStateForHandler(handler); @@ -109,25 +97,23 @@ public sealed class PairHandlerRegistry : IDisposable lock (_gate) { - if (!_identToHandler.TryGetValue(registration.CharacterIdent, out handler)) + if (!_entriesByIdent.TryGetValue(registration.CharacterIdent, out var entry)) { return PairOperationResult.Fail($"Ident {registration.CharacterIdent} not registered."); } - if (_handlerToPairs.TryGetValue(handler, out var set)) + handler = entry.Handler; + entry.RemovePair(registration.PairIdent); + if (entry.PairCount == 0) { - set.Remove(registration.PairIdent); - if (set.Count == 0) + if (forceDisposal) { - if (forceDisposal) - { - shouldDisposeImmediately = true; - } - else - { - shouldScheduleRemoval = true; - handler.ScheduledForDeletion = true; - } + shouldDisposeImmediately = true; + } + else + { + shouldScheduleRemoval = true; + handler.ScheduledForDeletion = true; } } } @@ -154,13 +140,7 @@ public sealed class PairHandlerRegistry : IDisposable return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}."); } - IPairHandlerAdapter? handler; - lock (_gate) - { - _identToHandler.TryGetValue(registration.CharacterIdent, out handler); - } - - if (handler is null) + if (!TryGetHandler(registration.CharacterIdent, out var handler) || handler is null) { var registerResult = RegisterOnlinePair(registration); if (!registerResult.Success) @@ -168,30 +148,19 @@ public sealed class PairHandlerRegistry : IDisposable return PairOperationResult.Fail(registerResult.Error); } - lock (_gate) + if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null) { - _identToHandler.TryGetValue(registration.CharacterIdent, out handler); + return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}."); } } - if (handler is null) - { - return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}."); - } - handler.ApplyData(dto.CharaData); return PairOperationResult.Ok(); } public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false) { - IPairHandlerAdapter? handler; - lock (_gate) - { - _identToHandler.TryGetValue(ident, out handler); - } - - if (handler is null) + if (!TryGetHandler(ident, out var handler) || handler is null) { return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found."); } @@ -202,13 +171,7 @@ public sealed class PairHandlerRegistry : IDisposable public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading) { - IPairHandlerAdapter? handler; - lock (_gate) - { - _identToHandler.TryGetValue(ident, out handler); - } - - if (handler is null) + if (!TryGetHandler(ident, out var handler) || handler is null) { return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found."); } @@ -219,44 +182,31 @@ public sealed class PairHandlerRegistry : IDisposable public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused) { - IPairHandlerAdapter? handler; - lock (_gate) - { - _identToHandler.TryGetValue(ident, out handler); - } - - if (handler is null) + if (!TryGetHandler(ident, out var handler) || handler is null) { return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found."); } _ = paused; // value reflected in pair manager already - // Recalculate pause state against all registered pairs to ensure consistency across contexts. ApplyPauseStateForHandler(handler); return PairOperationResult.Ok(); } public PairOperationResult> GetPairConnections(string ident) { - IPairHandlerAdapter? handler; - HashSet? identifiers = null; - + PairHandlerEntry? entry; lock (_gate) { - _identToHandler.TryGetValue(ident, out handler); - if (handler is not null) - { - _handlerToPairs.TryGetValue(handler, out identifiers); - } + _entriesByIdent.TryGetValue(ident, out entry); } - if (handler is null || identifiers is null) + if (entry is null) { return PairOperationResult>.Fail($"No handler registered for {ident}."); } var list = new List<(PairUniqueIdentifier, PairConnection)>(); - foreach (var pairIdent in identifiers) + foreach (var pairIdent in entry.SnapshotPairs()) { var result = _pairManager.GetPair(pairIdent.UserId); if (result.Success) @@ -279,8 +229,8 @@ public sealed class PairHandlerRegistry : IDisposable { lock (_gate) { - var success = _identToHandler.TryGetValue(ident, out var resolved); - handler = resolved; + var success = _entriesByIdent.TryGetValue(ident, out var entry); + handler = entry?.Handler; return success; } } @@ -289,7 +239,7 @@ public sealed class PairHandlerRegistry : IDisposable { lock (_gate) { - return _identToHandler.Values.Distinct().ToList(); + return _entriesByHandler.Keys.ToList(); } } @@ -297,9 +247,9 @@ public sealed class PairHandlerRegistry : IDisposable { lock (_gate) { - if (_handlerToPairs.TryGetValue(handler, out var pairs)) + if (_entriesByHandler.TryGetValue(handler, out var entry)) { - return pairs.ToList(); + return entry.SnapshotPairs(); } } @@ -330,17 +280,9 @@ public sealed class PairHandlerRegistry : IDisposable List handlers; lock (_gate) { - handlers = _identToHandler.Values.Distinct().ToList(); - _identToHandler.Clear(); - _handlerToPairs.Clear(); - - foreach (var pending in _waitingRequests.Values) - { - pending.Cancel(); - pending.Dispose(); - } - - _waitingRequests.Clear(); + handlers = _entriesByHandler.Keys.ToList(); + _entriesByIdent.Clear(); + _entriesByHandler.Clear(); } foreach (var handler in handlers) @@ -364,14 +306,9 @@ public sealed class PairHandlerRegistry : IDisposable List handlers; lock (_gate) { - handlers = _identToHandler.Values.Distinct().ToList(); - _identToHandler.Clear(); - _handlerToPairs.Clear(); - foreach (var kv in _waitingRequests.Values) - { - kv.Cancel(); - } - _waitingRequests.Clear(); + handlers = _entriesByHandler.Keys.ToList(); + _entriesByIdent.Clear(); + _entriesByHandler.Clear(); } foreach (var handler in handlers) @@ -380,46 +317,23 @@ public sealed class PairHandlerRegistry : IDisposable } } - private IPairHandlerAdapter GetOrAddHandler(string ident) + private PairHandlerEntry GetOrCreateEntry(string ident) { - if (_identToHandler.TryGetValue(ident, out var handler)) + if (_entriesByIdent.TryGetValue(ident, out var entry)) { - return handler; + return entry; } - handler = _handlerFactory.Create(ident); - _identToHandler[ident] = handler; - _handlerToPairs[handler] = new HashSet(); - return handler; - } - - private void EnsureInitialized(IPairHandlerAdapter handler) - { - if (handler.Initialized) - { - return; - } - - try - { - handler.Initialize(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to initialize handler for {Ident}", handler.Ident); - } + var handler = _handlerFactory.Create(ident); + entry = new PairHandlerEntry(ident, handler); + _entriesByIdent[ident] = entry; + _entriesByHandler[handler] = entry; + return entry; } private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler) { - try - { - await Task.Delay(_deletionGracePeriod).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - return; - } + await Task.Delay(_deletionGracePeriod).ConfigureAwait(false); if (TryFinalizeHandlerRemoval(handler)) { @@ -431,63 +345,15 @@ public sealed class PairHandlerRegistry : IDisposable { lock (_gate) { - if (!_handlerToPairs.TryGetValue(handler, out var set) || set.Count > 0) + if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs) { handler.ScheduledForDeletion = false; return false; } - _handlerToPairs.Remove(handler); - _identToHandler.Remove(handler.Ident); - - if (_waitingRequests.TryGetValue(handler.Ident, out var cts)) - { - cts.Cancel(); - cts.Dispose(); - _waitingRequests.Remove(handler.Ident); - } - + _entriesByHandler.Remove(handler); + _entriesByIdent.Remove(entry.Ident); return true; } } - - private async Task WaitThenApplyDataAsync(PairRegistration registration, OnlineUserCharaDataDto dto, CancellationTokenSource cts) - { - var token = cts.Token; - try - { - while (!token.IsCancellationRequested) - { - IPairHandlerAdapter? handler; - lock (_gate) - { - _identToHandler.TryGetValue(registration.CharacterIdent!, out handler); - } - - if (handler is not null && handler.Initialized) - { - handler.ApplyData(dto.CharaData); - break; - } - - await Task.Delay(TimeSpan.FromMilliseconds(500), token).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - // expected - } - finally - { - lock (_gate) - { - if (_waitingRequests.TryGetValue(registration.CharacterIdent!, out var existing) && existing == cts) - { - _waitingRequests.Remove(registration.CharacterIdent!); - } - } - - cts.Dispose(); - } - } } diff --git a/LightlessSync/PlayerData/Pairs/PairLedger.cs b/LightlessSync/PlayerData/Pairs/PairLedger.cs index 1e0e359..66decfb 100644 --- a/LightlessSync/PlayerData/Pairs/PairLedger.cs +++ b/LightlessSync/PlayerData/Pairs/PairLedger.cs @@ -1,17 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; -using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; using LightlessSync.UI.Models; using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; +/// +/// keeps pair info for ui and reapplication +/// public sealed class PairLedger : DisposableMediatorSubscriberBase { private readonly PairManager _pairManager; diff --git a/LightlessSync/PlayerData/Pairs/PairManager.cs b/LightlessSync/PlayerData/Pairs/PairManager.cs index adbe5b8..fc6844a 100644 --- a/LightlessSync/PlayerData/Pairs/PairManager.cs +++ b/LightlessSync/PlayerData/Pairs/PairManager.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto.Group; @@ -9,6 +6,9 @@ using LightlessSync.API.Dto.User; namespace LightlessSync.PlayerData.Pairs; +/// +/// in memory state for pairs, groups, and syncshells +/// public sealed class PairManager { private readonly object _gate = new(); diff --git a/LightlessSync/PlayerData/Pairs/PairState.cs b/LightlessSync/PlayerData/Pairs/PairModels.cs similarity index 69% rename from LightlessSync/PlayerData/Pairs/PairState.cs rename to LightlessSync/PlayerData/Pairs/PairModels.cs index 0e2a508..9f34ab2 100644 --- a/LightlessSync/PlayerData/Pairs/PairState.cs +++ b/LightlessSync/PlayerData/Pairs/PairModels.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; @@ -7,42 +5,27 @@ using LightlessSync.API.Dto.Group; namespace LightlessSync.PlayerData.Pairs; -public readonly struct PairOperationResult +/// +/// core models for the pair system +/// +public sealed class PairState { - private PairOperationResult(bool success, string? error) - { - Success = success; - Error = error; - } + public CharacterData? CharacterData { get; set; } + public Guid? TemporaryCollectionId { get; set; } - public bool Success { get; } - public string? Error { get; } - - public static PairOperationResult Ok() => new(true, null); - - public static PairOperationResult Fail(string error) => new(false, error); + public bool IsEmpty => CharacterData is null && (TemporaryCollectionId is null || TemporaryCollectionId == Guid.Empty); } -public readonly struct PairOperationResult -{ - private PairOperationResult(bool success, T value, string? error) - { - Success = success; - Value = value; - Error = error; - } - - public bool Success { get; } - public T Value { get; } - public string? Error { get; } - - public static PairOperationResult Ok(T value) => new(true, value, null); - - public static PairOperationResult Fail(string error) => new(false, default!, error); -} +public readonly record struct PairUniqueIdentifier(string UserId); +/// +/// link between a pair id and character ident +/// public sealed record PairRegistration(PairUniqueIdentifier PairIdent, string? CharacterIdent); +/// +/// per group membership info for a pair +/// public sealed class GroupPairRelationship { public GroupPairRelationship(string groupId, GroupPairUserInfo? info) @@ -60,6 +43,9 @@ public sealed class GroupPairRelationship } } +/// +/// runtime view of a single pair connection +/// public sealed class PairConnection { public PairConnection(UserData user) @@ -121,6 +107,9 @@ public sealed class PairConnection } } +/// +/// syncshell metadata plus member connections +/// public sealed class Syncshell { public Syncshell(GroupFullInfoDto dto) @@ -138,12 +127,94 @@ public sealed class Syncshell } } -public sealed class PairState +/// +/// simple success/failure result +/// +public readonly struct PairOperationResult { - public CharacterData? CharacterData { get; set; } - public Guid? TemporaryCollectionId { get; set; } + private PairOperationResult(bool success, string? error) + { + Success = success; + Error = error; + } - public bool IsEmpty => CharacterData is null && (TemporaryCollectionId is null || TemporaryCollectionId == Guid.Empty); + public bool Success { get; } + public string? Error { get; } + + public static PairOperationResult Ok() => new(true, null); + + public static PairOperationResult Fail(string error) => new(false, error); } -public readonly record struct PairUniqueIdentifier(string UserId); +/// +/// typed success/failure result +/// +public readonly struct PairOperationResult +{ + private PairOperationResult(bool success, T value, string? error) + { + Success = success; + Value = value; + Error = error; + } + + public bool Success { get; } + public T Value { get; } + public string? Error { get; } + + public static PairOperationResult Ok(T value) => new(true, value, null); + + public static PairOperationResult Fail(string error) => new(false, default!, error); +} + +/// +/// state of which optional plugin warnings were shown +/// +public record OptionalPluginWarning +{ + public bool ShownHeelsWarning { get; set; } = false; + public bool ShownCustomizePlusWarning { get; set; } = false; + public bool ShownHonorificWarning { get; set; } = false; + public bool ShownMoodlesWarning { get; set; } = false; + public bool ShowPetNicknamesWarning { get; set; } = false; +} + +/// +/// tracks the handler registered pairs for an ident +/// +internal sealed class PairHandlerEntry +{ + private readonly HashSet _pairs = new(); + + public PairHandlerEntry(string ident, IPairHandlerAdapter handler) + { + Ident = ident; + Handler = handler; + } + + public string Ident { get; } + public IPairHandlerAdapter Handler { get; } + + public bool HasPairs => _pairs.Count > 0; + public int PairCount => _pairs.Count; + + public void AddPair(PairUniqueIdentifier pair) + { + _pairs.Add(pair); + } + + public bool RemovePair(PairUniqueIdentifier pair) + { + return _pairs.Remove(pair); + } + + public IReadOnlyCollection SnapshotPairs() + { + if (_pairs.Count == 0) + { + return Array.Empty(); + } + + return _pairs.ToArray(); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairStateCache.cs b/LightlessSync/PlayerData/Pairs/PairStateCache.cs index 67e8c8c..3d7a377 100644 --- a/LightlessSync/PlayerData/Pairs/PairStateCache.cs +++ b/LightlessSync/PlayerData/Pairs/PairStateCache.cs @@ -1,11 +1,12 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using LightlessSync.API.Data; using LightlessSync.Utils; namespace LightlessSync.PlayerData.Pairs; +/// +/// cache for character/pair data and penumbra collections +/// public sealed class PairStateCache { private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); diff --git a/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs b/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs index 1840813..a1c7587 100644 --- a/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs +++ b/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.API.Data; using LightlessSync.API.Data.Comparer; using LightlessSync.Services; @@ -14,6 +9,9 @@ using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; +/// +/// pushes character data to visible pairs +/// public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase { private readonly ApiController _apiController; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 542d9a1..5136a6e 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -228,7 +228,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerBrio(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcManager(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), @@ -340,8 +340,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), pluginInterface, textureProvider, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), namePlateGui, clientState, - s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), clientState, gameGui, objectTable, gameInteropProvider, + s.GetRequiredService(),s.GetRequiredService())); collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), clientState, s.GetRequiredService())); diff --git a/LightlessSync/Services/Chat/ChatModels.cs b/LightlessSync/Services/Chat/ChatModels.cs index f83a7e9..e9058e7 100644 --- a/LightlessSync/Services/Chat/ChatModels.cs +++ b/LightlessSync/Services/Chat/ChatModels.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using LightlessSync.API.Dto.Chat; namespace LightlessSync.Services.Chat; diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs index 1aee611..4499cf8 100644 --- a/LightlessSync/Services/Chat/ZoneChatService.cs +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -1,12 +1,5 @@ -using LightlessSync; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.API.Dto; using LightlessSync.API.Dto.Chat; -using LightlessSync.Services; using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; using LightlessSync.WebAPI; diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index ab15b86..4ca8a2f 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -1,114 +1,254 @@ using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.Gui.NamePlate; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.NativeWrapper; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; -using LightlessSync.UI; using LightlessSync.UI.Services; using Microsoft.Extensions.Logging; +using System.Numerics; +using static LightlessSync.UI.DtrEntry; +using LSeStringBuilder = Lumina.Text.SeStringBuilder; namespace LightlessSync.Services; -public class NameplateService : DisposableMediatorSubscriberBase +/// +/// NameplateService is used for coloring our nameplates based on the settings of the user. +/// +public unsafe class NameplateService : DisposableMediatorSubscriberBase { + private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex); + + // Glyceri, Thanks :bow: + [Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))] + private readonly Hook? _nameplateHook = null; + private readonly ILogger _logger; private readonly LightlessConfigService _configService; private readonly IClientState _clientState; - private readonly INamePlateGui _namePlateGui; + private readonly IGameGui _gameGui; + private readonly IObjectTable _objectTable; private readonly PairUiService _pairUiService; public NameplateService(ILogger logger, LightlessConfigService configService, - INamePlateGui namePlateGui, IClientState clientState, - PairUiService pairUiService, - LightlessMediator lightlessMediator) : base(logger, lightlessMediator) + IGameGui gameGui, + IObjectTable objectTable, + IGameInteropProvider interop, + LightlessMediator lightlessMediator, + PairUiService pairUiService) : base(logger, lightlessMediator) { _logger = logger; _configService = configService; - _namePlateGui = namePlateGui; _clientState = clientState; + _gameGui = gameGui; + _objectTable = objectTable; _pairUiService = pairUiService; - _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; - _namePlateGui.RequestRedraw(); - Mediator.Subscribe(this, (_) => _namePlateGui.RequestRedraw()); + interop.InitializeFromAttributes(this); + _nameplateHook?.Enable(); + Refresh(); + + Mediator.Subscribe(this, (_) => Refresh()); } - private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) + /// + /// Detour for the game's internal nameplate update function. + /// This will be called whenever the client updates any nameplate. + /// + /// We hook into it to apply our own nameplate coloring logic via , + /// + private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex) { - if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) + try + { + SetNameplate(namePlateInfo, battleChara); + } + catch (Exception e) + { + _logger.LogError(e, "Error in NameplateService UpdateNameplateDetour"); + } + + return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex); + } + + /// + /// Determine if the player should be colored based on conditions (isFriend, IsInParty) + /// + /// Player character that will be checked + /// All visible users in the current object table + /// PLayer should or shouldnt be colored based on the result. True means colored + private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet visibleUserIds) + { + if (!visibleUserIds.Contains(playerCharacter.GameObjectId)) + return false; + + var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember); + var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend); + + bool partyColorAllowed = _configService.Current.overridePartyColor && isInParty; + bool friendColorAllowed = _configService.Current.overrideFriendColor && isFriend; + + if ((isInParty && !partyColorAllowed) || (isFriend && !friendColorAllowed)) + return false; + + return true; + } + + /// + /// Setting up the nameplate of the user to be colored + /// + /// Information given from the Signature to be updated + /// Character from FF + private void SetNameplate(RaptureAtkModule.NamePlateInfo* namePlateInfo, BattleChara* battleChara) + { + if (!_configService.Current.IsNameplateColorsEnabled || _clientState.IsPvPExcludingDen) + return; + if (namePlateInfo == null || battleChara == null) + return; + + var obj = _objectTable.FirstOrDefault(o => o.Address == (nint)battleChara); + if (obj is not IPlayerCharacter player) return; var snapshot = _pairUiService.GetSnapshot(); var visibleUsersIds = snapshot.PairsByUid.Values - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId) - .ToHashSet(); + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId) + .ToHashSet(); - var colors = _configService.Current.NameplateColors; + //Check if player should be colored + if (!ShouldColorPlayer(player, visibleUsersIds)) + return; - foreach (var handler in handlers) - { - var playerCharacter = handler.PlayerCharacter; - if (playerCharacter == null) - continue; + var originalName = player.Name.ToString(); - var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember); - var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend); - bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty); - bool friendColorAllowed = (_configService.Current.overrideFriendColor && isFriend); + //Check if not null of the name + if (string.IsNullOrEmpty(originalName)) + return; - if (visibleUsersIds.Contains(handler.GameObjectId) && - !( - (isInParty && !partyColorAllowed) || - (isFriend && !friendColorAllowed) - )) - { - handler.NameParts.TextWrap = CreateTextWrap(colors); + //Check if any characters/symbols are forbidden + if (HasForbiddenSeStringChars(originalName)) + return; - if (_configService.Current.overrideFcTagColor) - { - bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0; - bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId; - bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm); + //Swap color channels as we store them in BGR format as FF loves that + var cfgColors = SwapColorChannels(_configService.Current.NameplateColors); + var coloredName = WrapStringInColor(originalName, cfgColors.Glow, cfgColors.Foreground); - if (shouldColorFcArea) - { - handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors); - handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors); - } - } - } - } + //Replace string of nameplate with our colored one + namePlateInfo->Name.SetString(coloredName.EncodeWithNullTerminator()); } + /// + /// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina + /// + /// Color code + /// Vector4 Color + private static Vector4 RgbUintToVector4(uint rgb) + { + float r = ((rgb >> 16) & 0xFF) / 255f; + float g = ((rgb >> 8) & 0xFF) / 255f; + float b = (rgb & 0xFF) / 255f; + return new Vector4(r, g, b, 1f); + } + + /// + /// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append. + /// + /// String that has to be checked + /// Contains forbidden characters/symbols or not + private static bool HasForbiddenSeStringChars(string s) + { + if (string.IsNullOrEmpty(s)) + return false; + + foreach (var ch in s) + { + if (ch == '\0' || ch == '\u0002') + return true; + } + + return false; + } + + /// + /// Wraps the given string with the given edge and text color. + /// + /// String that has to be wrapped + /// Edge(border) color + /// Text color + /// Color wrapped SeString + public static SeString WrapStringInColor(string text, uint? edgeColor = null, uint? textColor = null) + { + if (string.IsNullOrEmpty(text)) + return SeString.Empty; + + var builder = new LSeStringBuilder(); + + if (textColor is uint tc) + builder.PushColorRgba(RgbUintToVector4(tc)); + + if (edgeColor is uint ec) + builder.PushEdgeColorRgba(RgbUintToVector4(ec)); + + builder.Append(text); + + if (edgeColor != null) + builder.PopEdgeColor(); + + if (textColor != null) + builder.PopColor(); + + return builder.ToReadOnlySeString().ToDalamudString(); + } + + /// + /// Request redraw of nameplates + /// public void RequestRedraw() { - _namePlateGui.RequestRedraw(); + Refresh(); } - private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color) + /// + /// Toggles the refresh of the Nameplate addon + /// + protected void Refresh() { - var left = new Lumina.Text.SeStringBuilder(); - var right = new Lumina.Text.SeStringBuilder(); + AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate"); - left.PushColorRgba(color.Foreground); - right.PopColor(); + if (namePlateAddon.IsNull) + { + _logger.LogInformation("NamePlate addon is null, cannot refresh nameplates."); + return; + } - left.PushEdgeColorRgba(color.Glow); - right.PopEdgeColor(); + var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address; - return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString()); + if (addonNamePlate == null) + { + _logger.LogInformation("addonNamePlate addon is null, cannot refresh nameplates."); + return; + } + + addonNamePlate->DoFullUpdate = 1; } protected override void Dispose(bool disposing) { - base.Dispose(disposing); + if (disposing) + { + _nameplateHook?.Dispose(); + } - _namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; - _namePlateGui.RequestRedraw(); + base.Dispose(disposing); } } \ No newline at end of file diff --git a/LightlessSync/Services/TextureCompression/IndexDownscaler.cs b/LightlessSync/Services/TextureCompression/IndexDownscaler.cs new file mode 100644 index 0000000..615a5e2 --- /dev/null +++ b/LightlessSync/Services/TextureCompression/IndexDownscaler.cs @@ -0,0 +1,312 @@ +using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +/* + * Index upscaler code (converted/reversed for downscaling purposes) provided by Ny + * thank you!! +*/ + +namespace LightlessSync.Services.TextureCompression; + +internal static class IndexDownscaler +{ + private static readonly Vector2[] SampleOffsets = + { + new(0.25f, 0.25f), + new(0.75f, 0.25f), + new(0.25f, 0.75f), + new(0.75f, 0.75f), + }; + + public static Image Downscale(Image source, int targetWidth, int targetHeight, int blockMultiple) + { + var current = source.Clone(); + + while (current.Width > targetWidth || current.Height > targetHeight) + { + var nextWidth = Math.Max(targetWidth, Math.Max(blockMultiple, current.Width / 2)); + var nextHeight = Math.Max(targetHeight, Math.Max(blockMultiple, current.Height / 2)); + var next = new Image(nextWidth, nextHeight); + + for (var y = 0; y < nextHeight; y++) + { + var srcY = Math.Min(current.Height - 1, y * 2); + for (var x = 0; x < nextWidth; x++) + { + var srcX = Math.Min(current.Width - 1, x * 2); + + var topLeft = current[srcX, srcY]; + var topRight = current[Math.Min(current.Width - 1, srcX + 1), srcY]; + var bottomLeft = current[srcX, Math.Min(current.Height - 1, srcY + 1)]; + var bottomRight = current[Math.Min(current.Width - 1, srcX + 1), Math.Min(current.Height - 1, srcY + 1)]; + + next[x, y] = DownscaleIndexBlock(topLeft, topRight, bottomLeft, bottomRight); + } + } + + current.Dispose(); + current = next; + } + + return current; + } + + private static Rgba32 DownscaleIndexBlock(in Rgba32 topLeft, in Rgba32 topRight, in Rgba32 bottomLeft, in Rgba32 bottomRight) + { + Span ordered = stackalloc Rgba32[4] + { + bottomLeft, + bottomRight, + topRight, + topLeft + }; + + Span weights = stackalloc float[4]; + var hasContribution = false; + + foreach (var sample in SampleOffsets) + { + if (TryAccumulateSampleWeights(ordered, sample, weights)) + { + hasContribution = true; + } + } + + if (hasContribution) + { + var bestIndex = IndexOfMax(weights); + if (bestIndex >= 0 && weights[bestIndex] > 0f) + { + return ordered[bestIndex]; + } + } + + Span fallback = stackalloc Rgba32[4] { topLeft, topRight, bottomLeft, bottomRight }; + return PickMajorityColor(fallback); + } + + private static bool TryAccumulateSampleWeights(ReadOnlySpan colors, in Vector2 sampleUv, Span weights) + { + var red = new Vector4( + colors[0].R / 255f, + colors[1].R / 255f, + colors[2].R / 255f, + colors[3].R / 255f); + + var symbols = QuantizeSymbols(red); + var cellUv = ComputeShiftedUv(sampleUv); + + Span order = stackalloc int[4]; + order[0] = 0; + order[1] = 1; + order[2] = 2; + order[3] = 3; + + ApplySymmetry(ref symbols, ref cellUv, order); + + var equality = BuildEquality(symbols, symbols.W); + var selector = BuildSelector(equality, symbols, cellUv); + + const uint lut = 0x00000C07u; + + if (((lut >> (int)selector) & 1u) != 0u) + { + weights[order[3]] += 1f; + return true; + } + + if (selector == 3u) + { + equality = BuildEquality(symbols, symbols.Z); + } + + var weight = ComputeWeight(equality, cellUv); + if (weight <= 1e-6f) + { + return false; + } + + var factor = 1f / weight; + + var wW = equality.W * (1f - cellUv.X) * (1f - cellUv.Y) * factor; + var wX = equality.X * (1f - cellUv.X) * cellUv.Y * factor; + var wZ = equality.Z * cellUv.X * (1f - cellUv.Y) * factor; + var wY = equality.Y * cellUv.X * cellUv.Y * factor; + + var contributed = false; + + if (wW > 0f) + { + weights[order[3]] += wW; + contributed = true; + } + + if (wX > 0f) + { + weights[order[0]] += wX; + contributed = true; + } + + if (wZ > 0f) + { + weights[order[2]] += wZ; + contributed = true; + } + + if (wY > 0f) + { + weights[order[1]] += wY; + contributed = true; + } + + return contributed; + } + + private static Vector4 QuantizeSymbols(in Vector4 channel) + => new( + Quantize(channel.X), + Quantize(channel.Y), + Quantize(channel.Z), + Quantize(channel.W)); + + private static float Quantize(float value) + { + var clamped = Math.Clamp(value, 0f, 1f); + return (MathF.Round(clamped * 16f) + 0.5f) / 16f; + } + + private static void ApplySymmetry(ref Vector4 symbols, ref Vector2 cellUv, Span order) + { + if (cellUv.X >= 0.5f) + { + symbols = SwapYxwz(symbols, order); + cellUv.X = 1f - cellUv.X; + } + + if (cellUv.Y >= 0.5f) + { + symbols = SwapWzyx(symbols, order); + cellUv.Y = 1f - cellUv.Y; + } + } + + private static Vector4 BuildEquality(in Vector4 symbols, float reference) + => new( + AreEqual(symbols.X, reference) ? 1f : 0f, + AreEqual(symbols.Y, reference) ? 1f : 0f, + AreEqual(symbols.Z, reference) ? 1f : 0f, + AreEqual(symbols.W, reference) ? 1f : 0f); + + private static uint BuildSelector(in Vector4 equality, in Vector4 symbols, in Vector2 cellUv) + { + uint selector = 0; + if (equality.X > 0.5f) selector |= 4u; + if (equality.Y > 0.5f) selector |= 8u; + if (equality.Z > 0.5f) selector |= 16u; + if (AreEqual(symbols.X, symbols.Z)) selector |= 2u; + if (cellUv.X + cellUv.Y >= 0.5f) selector |= 1u; + + return selector; + } + + private static float ComputeWeight(in Vector4 equality, in Vector2 cellUv) + => equality.W * (1f - cellUv.X) * (1f - cellUv.Y) + + equality.X * (1f - cellUv.X) * cellUv.Y + + equality.Z * cellUv.X * (1f - cellUv.Y) + + equality.Y * cellUv.X * cellUv.Y; + + private static Vector2 ComputeShiftedUv(in Vector2 uv) + { + var shifted = new Vector2( + uv.X - MathF.Floor(uv.X), + uv.Y - MathF.Floor(uv.Y)); + + shifted.X -= 0.5f; + if (shifted.X < 0f) + { + shifted.X += 1f; + } + + shifted.Y -= 0.5f; + if (shifted.Y < 0f) + { + shifted.Y += 1f; + } + + return shifted; + } + + private static Vector4 SwapYxwz(in Vector4 v, Span order) + { + var o0 = order[0]; + var o1 = order[1]; + var o2 = order[2]; + var o3 = order[3]; + + order[0] = o1; + order[1] = o0; + order[2] = o3; + order[3] = o2; + + return new Vector4(v.Y, v.X, v.W, v.Z); + } + + private static Vector4 SwapWzyx(in Vector4 v, Span order) + { + var o0 = order[0]; + var o1 = order[1]; + var o2 = order[2]; + var o3 = order[3]; + + order[0] = o3; + order[1] = o2; + order[2] = o1; + order[3] = o0; + + return new Vector4(v.W, v.Z, v.Y, v.X); + } + + private static int IndexOfMax(ReadOnlySpan values) + { + var bestIndex = -1; + var bestValue = 0f; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] > bestValue) + { + bestValue = values[i]; + bestIndex = i; + } + } + + return bestIndex; + } + + private static bool AreEqual(float a, float b) => MathF.Abs(a - b) <= 1e-5f; + + private static Rgba32 PickMajorityColor(ReadOnlySpan colors) + { + var counts = new Dictionary(colors.Length); + foreach (var color in colors) + { + if (counts.TryGetValue(color, out var count)) + { + counts[color] = count + 1; + } + else + { + counts[color] = 1; + } + } + + return counts + .OrderByDescending(kvp => kvp.Value) + .ThenByDescending(kvp => kvp.Key.A) + .ThenByDescending(kvp => kvp.Key.R) + .ThenByDescending(kvp => kvp.Key.G) + .ThenByDescending(kvp => kvp.Key.B) + .First().Key; + } +} \ No newline at end of file diff --git a/LightlessSync/Services/TextureCompression/TexFileHelper.cs b/LightlessSync/Services/TextureCompression/TexFileHelper.cs index b5e2ab8..7258fbf 100644 --- a/LightlessSync/Services/TextureCompression/TexFileHelper.cs +++ b/LightlessSync/Services/TextureCompression/TexFileHelper.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Runtime.InteropServices; using Lumina.Data.Files; using OtterTex; diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs b/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs index 81e10c5..8725d07 100644 --- a/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs +++ b/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; -using System.Linq; using Penumbra.Api.Enums; namespace LightlessSync.Services.TextureCompression; diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionRequest.cs b/LightlessSync/Services/TextureCompression/TextureCompressionRequest.cs index 0877d55..18681d8 100644 --- a/LightlessSync/Services/TextureCompression/TextureCompressionRequest.cs +++ b/LightlessSync/Services/TextureCompression/TextureCompressionRequest.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; namespace LightlessSync.Services.TextureCompression; diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionService.cs b/LightlessSync/Services/TextureCompression/TextureCompressionService.cs index 2d4a1d2..c31539f 100644 --- a/LightlessSync/Services/TextureCompression/TextureCompressionService.cs +++ b/LightlessSync/Services/TextureCompression/TextureCompressionService.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.Interop.Ipc; using LightlessSync.FileCache; using Microsoft.Extensions.Logging; diff --git a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs index e5ead9d..b43b1b5 100644 --- a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs +++ b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Buffers.Binary; using System.Globalization; -using System.Numerics; using System.IO; using OtterTex; using OtterImage = OtterTex.Image; @@ -15,7 +14,6 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; /* - * Index upscaler code (converted/reversed for downscaling purposes) provided by Ny * OtterTex made by Ottermandias * thank you!! */ @@ -183,7 +181,7 @@ public sealed class TextureDownscaleService return; } - using var resized = ReduceIndexTexture(originalImage, targetSize.width, targetSize.height); + using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple); var resizedPixels = new byte[targetSize.width * targetSize.height * 4]; resized.CopyPixelDataTo(resizedPixels); @@ -231,8 +229,7 @@ public sealed class TextureDownscaleService private static bool IsIndexMap(TextureMapKind kind) => kind is TextureMapKind.Mask - or TextureMapKind.Index - or TextureMapKind.Ui; + or TextureMapKind.Index; private Task TryDropTopMipAsync( string hash, @@ -423,39 +420,6 @@ public sealed class TextureDownscaleService private static int ReduceDimension(int value) => value <= 1 ? 1 : Math.Max(1, value / 2); - private static Image ReduceIndexTexture(Image source, int targetWidth, int targetHeight) - { - var current = source.Clone(); - - while (current.Width > targetWidth || current.Height > targetHeight) - { - var nextWidth = Math.Max(targetWidth, Math.Max(BlockMultiple, current.Width / 2)); - var nextHeight = Math.Max(targetHeight, Math.Max(BlockMultiple, current.Height / 2)); - var next = new Image(nextWidth, nextHeight); - - for (int y = 0; y < nextHeight; y++) - { - var srcY = Math.Min(current.Height - 1, y * 2); - for (int x = 0; x < nextWidth; x++) - { - var srcX = Math.Min(current.Width - 1, x * 2); - - var topLeft = current[srcX, srcY]; - var topRight = current[Math.Min(current.Width - 1, srcX + 1), srcY]; - var bottomLeft = current[srcX, Math.Min(current.Height - 1, srcY + 1)]; - var bottomRight = current[Math.Min(current.Width - 1, srcX + 1), Math.Min(current.Height - 1, srcY + 1)]; - - next[x, y] = DownscaleIndexBlock(topLeft, topRight, bottomLeft, bottomRight); - } - } - - current.Dispose(); - current = next; - } - - return current; - } - private static Image ReduceLinearTexture(Image source, int targetWidth, int targetHeight) { var clone = source.Clone(); @@ -470,271 +434,6 @@ public sealed class TextureDownscaleService return clone; } - private static Rgba32 DownscaleIndexBlock(in Rgba32 topLeft, in Rgba32 topRight, in Rgba32 bottomLeft, in Rgba32 bottomRight) - { - Span ordered = stackalloc Rgba32[4] - { - bottomLeft, - bottomRight, - topRight, - topLeft - }; - - Span weights = stackalloc float[4]; - var hasContribution = false; - - foreach (var sample in SampleOffsets) - { - if (TryAccumulateSampleWeights(ordered, sample, weights)) - { - hasContribution = true; - } - } - - if (hasContribution) - { - var bestIndex = IndexOfMax(weights); - if (bestIndex >= 0 && weights[bestIndex] > 0f) - { - return ordered[bestIndex]; - } - } - - Span fallback = stackalloc Rgba32[4] { topLeft, topRight, bottomLeft, bottomRight }; - return PickMajorityColor(fallback); - } - - private static readonly Vector2[] SampleOffsets = - { - new(0.25f, 0.25f), - new(0.75f, 0.25f), - new(0.25f, 0.75f), - new(0.75f, 0.75f), - }; - - private static bool TryAccumulateSampleWeights(ReadOnlySpan colors, in Vector2 sampleUv, Span weights) - { - var red = new Vector4( - colors[0].R / 255f, - colors[1].R / 255f, - colors[2].R / 255f, - colors[3].R / 255f); - - var symbols = QuantizeSymbols(red); - var cellUv = ComputeShiftedUv(sampleUv); - - Span order = stackalloc int[4]; - order[0] = 0; - order[1] = 1; - order[2] = 2; - order[3] = 3; - - ApplySymmetry(ref symbols, ref cellUv, order); - - var equality = BuildEquality(symbols, symbols.W); - var selector = BuildSelector(equality, symbols, cellUv); - - const uint lut = 0x00000C07u; - - if (((lut >> (int)selector) & 1u) != 0u) - { - weights[order[3]] += 1f; - return true; - } - - if (selector == 3u) - { - equality = BuildEquality(symbols, symbols.Z); - } - - var weight = ComputeWeight(equality, cellUv); - if (weight <= 1e-6f) - { - return false; - } - - var factor = 1f / weight; - - var wW = equality.W * (1f - cellUv.X) * (1f - cellUv.Y) * factor; - var wX = equality.X * (1f - cellUv.X) * cellUv.Y * factor; - var wZ = equality.Z * cellUv.X * (1f - cellUv.Y) * factor; - var wY = equality.Y * cellUv.X * cellUv.Y * factor; - - var contributed = false; - - if (wW > 0f) - { - weights[order[3]] += wW; - contributed = true; - } - - if (wX > 0f) - { - weights[order[0]] += wX; - contributed = true; - } - - if (wZ > 0f) - { - weights[order[2]] += wZ; - contributed = true; - } - - if (wY > 0f) - { - weights[order[1]] += wY; - contributed = true; - } - - return contributed; - } - - private static Vector4 QuantizeSymbols(in Vector4 channel) - => new( - Quantize(channel.X), - Quantize(channel.Y), - Quantize(channel.Z), - Quantize(channel.W)); - - private static float Quantize(float value) - { - var clamped = Math.Clamp(value, 0f, 1f); - return (MathF.Round(clamped * 16f) + 0.5f) / 16f; - } - - private static void ApplySymmetry(ref Vector4 symbols, ref Vector2 cellUv, Span order) - { - if (cellUv.X >= 0.5f) - { - symbols = SwapYxwz(symbols, order); - cellUv.X = 1f - cellUv.X; - } - - if (cellUv.Y >= 0.5f) - { - symbols = SwapWzyx(symbols, order); - cellUv.Y = 1f - cellUv.Y; - } - } - - private static Vector4 BuildEquality(in Vector4 symbols, float reference) - => new( - AreEqual(symbols.X, reference) ? 1f : 0f, - AreEqual(symbols.Y, reference) ? 1f : 0f, - AreEqual(symbols.Z, reference) ? 1f : 0f, - AreEqual(symbols.W, reference) ? 1f : 0f); - - private static uint BuildSelector(in Vector4 equality, in Vector4 symbols, in Vector2 cellUv) - { - uint selector = 0; - if (equality.X > 0.5f) selector |= 4u; - if (equality.Y > 0.5f) selector |= 8u; - if (equality.Z > 0.5f) selector |= 16u; - if (AreEqual(symbols.X, symbols.Z)) selector |= 2u; - if (cellUv.X + cellUv.Y >= 0.5f) selector |= 1u; - - return selector; - } - - private static float ComputeWeight(in Vector4 equality, in Vector2 cellUv) - => equality.W * (1f - cellUv.X) * (1f - cellUv.Y) - + equality.X * (1f - cellUv.X) * cellUv.Y - + equality.Z * cellUv.X * (1f - cellUv.Y) - + equality.Y * cellUv.X * cellUv.Y; - - private static Vector2 ComputeShiftedUv(in Vector2 uv) - { - var shifted = new Vector2( - uv.X - MathF.Floor(uv.X), - uv.Y - MathF.Floor(uv.Y)); - - shifted.X -= 0.5f; - if (shifted.X < 0f) - { - shifted.X += 1f; - } - - shifted.Y -= 0.5f; - if (shifted.Y < 0f) - { - shifted.Y += 1f; - } - - return shifted; - } - - private static Vector4 SwapYxwz(in Vector4 v, Span order) - { - var o0 = order[0]; - var o1 = order[1]; - var o2 = order[2]; - var o3 = order[3]; - - order[0] = o1; - order[1] = o0; - order[2] = o3; - order[3] = o2; - - return new Vector4(v.Y, v.X, v.W, v.Z); - } - - private static Vector4 SwapWzyx(in Vector4 v, Span order) - { - var o0 = order[0]; - var o1 = order[1]; - var o2 = order[2]; - var o3 = order[3]; - - order[0] = o3; - order[1] = o2; - order[2] = o1; - order[3] = o0; - - return new Vector4(v.W, v.Z, v.Y, v.X); - } - - private static int IndexOfMax(ReadOnlySpan values) - { - var bestIndex = -1; - var bestValue = 0f; - - for (var i = 0; i < values.Length; i++) - { - if (values[i] > bestValue) - { - bestValue = values[i]; - bestIndex = i; - } - } - - return bestIndex; - } - - private static bool AreEqual(float a, float b) => MathF.Abs(a - b) <= 1e-5f; - - private static Rgba32 PickMajorityColor(ReadOnlySpan colors) - { - var counts = new Dictionary(colors.Length); - foreach (var color in colors) - { - if (counts.TryGetValue(color, out var count)) - { - counts[color] = count + 1; - } - else - { - counts[color] = 1; - } - } - - return counts - .OrderByDescending(kvp => kvp.Value) - .ThenByDescending(kvp => kvp.Key.A) - .ThenByDescending(kvp => kvp.Key.R) - .ThenByDescending(kvp => kvp.Key.G) - .ThenByDescending(kvp => kvp.Key.B) - .First().Key; - } private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension) { diff --git a/LightlessSync/Services/TextureCompression/TextureMapKind.cs b/LightlessSync/Services/TextureCompression/TextureMapKind.cs index ed2dee1..b007613 100644 --- a/LightlessSync/Services/TextureCompression/TextureMapKind.cs +++ b/LightlessSync/Services/TextureCompression/TextureMapKind.cs @@ -7,7 +7,5 @@ public enum TextureMapKind Specular, Mask, Index, - Emissive, - Ui, Unknown } diff --git a/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs index 3c0934c..010f9be 100644 --- a/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs +++ b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; -using Penumbra.Api.Enums; using Penumbra.GameData.Files; namespace LightlessSync.Services.TextureCompression; @@ -37,9 +32,9 @@ public sealed class TextureMetadataHelper private static readonly (TextureUsageCategory Category, string Token)[] CategoryTokens = { - (TextureUsageCategory.Ui, "/ui/"), - (TextureUsageCategory.Ui, "/uld/"), - (TextureUsageCategory.Ui, "/icon/"), + (TextureUsageCategory.UI, "/ui/"), + (TextureUsageCategory.UI, "/uld/"), + (TextureUsageCategory.UI, "/icon/"), (TextureUsageCategory.VisualEffect, "/vfx/"), @@ -104,9 +99,6 @@ public sealed class TextureMetadataHelper (TextureMapKind.Specular, "_s"), (TextureMapKind.Specular, "_spec"), - (TextureMapKind.Emissive, "_em"), - (TextureMapKind.Emissive, "_glow"), - (TextureMapKind.Index, "_id"), (TextureMapKind.Index, "_idx"), (TextureMapKind.Index, "_index"), @@ -133,10 +125,10 @@ public sealed class TextureMetadataHelper _dataManager = dataManager; } - public bool TryGetRecommendationInfo(TextureCompressionTarget target, out (string Title, string Description) info) + public static bool TryGetRecommendationInfo(TextureCompressionTarget target, out (string Title, string Description) info) => RecommendationCatalog.TryGetValue(target, out info); - public TextureUsageCategory DetermineCategory(string? gamePath) + public static TextureUsageCategory DetermineCategory(string? gamePath) { var normalized = Normalize(gamePath); if (string.IsNullOrEmpty(normalized)) @@ -193,7 +185,7 @@ public sealed class TextureMetadataHelper return TextureUsageCategory.Unknown; } - public string DetermineSlot(TextureUsageCategory category, string? gamePath) + public static string DetermineSlot(TextureUsageCategory category, string? gamePath) { if (category == TextureUsageCategory.Customization) return GuessCustomizationSlot(gamePath); @@ -218,7 +210,7 @@ public sealed class TextureMetadataHelper TextureUsageCategory.Companion => "Companion", TextureUsageCategory.VisualEffect => "VFX", TextureUsageCategory.Housing => "Housing", - TextureUsageCategory.Ui => "UI", + TextureUsageCategory.UI => "UI", _ => "General" }; } @@ -260,7 +252,7 @@ public sealed class TextureMetadataHelper return false; } - private void AddGameMaterialCandidates(string? gamePath, IList candidates) + private static void AddGameMaterialCandidates(string? gamePath, IList candidates) { var normalized = Normalize(gamePath); if (string.IsNullOrEmpty(normalized)) @@ -286,7 +278,7 @@ public sealed class TextureMetadataHelper } } - private void AddLocalMaterialCandidates(string? localTexturePath, IList candidates) + private static void AddLocalMaterialCandidates(string? localTexturePath, IList candidates) { if (string.IsNullOrEmpty(localTexturePath)) return; @@ -397,7 +389,7 @@ public sealed class TextureMetadataHelper return TextureMapKind.Unknown; } - public bool TryMapFormatToTarget(string? format, out TextureCompressionTarget target) + public static bool TryMapFormatToTarget(string? format, out TextureCompressionTarget target) { var normalized = (format ?? string.Empty).ToUpperInvariant(); if (normalized.Contains("BC1", StringComparison.Ordinal)) @@ -434,7 +426,7 @@ public sealed class TextureMetadataHelper return false; } - public (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget(string? format, TextureMapKind mapKind) + public static (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget(string? format, TextureMapKind mapKind) { TextureCompressionTarget? current = null; if (TryMapFormatToTarget(format, out var mapped)) @@ -446,7 +438,6 @@ public sealed class TextureMetadataHelper TextureMapKind.Mask => TextureCompressionTarget.BC4, TextureMapKind.Index => TextureCompressionTarget.BC3, TextureMapKind.Specular => TextureCompressionTarget.BC4, - TextureMapKind.Emissive => TextureCompressionTarget.BC3, TextureMapKind.Diffuse => TextureCompressionTarget.BC7, _ => TextureCompressionTarget.BC7 }; diff --git a/LightlessSync/Services/TextureCompression/TextureUsageCategory.cs b/LightlessSync/Services/TextureCompression/TextureUsageCategory.cs index c4af7b7..ac01393 100644 --- a/LightlessSync/Services/TextureCompression/TextureUsageCategory.cs +++ b/LightlessSync/Services/TextureCompression/TextureUsageCategory.cs @@ -10,7 +10,7 @@ public enum TextureUsageCategory Companion, Monster, Housing, - Ui, + UI, VisualEffect, Unknown } diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 5540b02..6878ce3 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -2,7 +2,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; -using Dalamud.Utility; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 90d735f..4c46fd4 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -2,8 +2,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Utility; -using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.Interop.Ipc; @@ -24,11 +22,9 @@ using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Globalization; -using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 725e004..932653d 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -12,16 +12,14 @@ using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.TextureCompression; using LightlessSync.Utils; -using Penumbra.Api.Enums; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; +using OtterTex; using System.Globalization; -using System.IO; -using System.Linq; using System.Numerics; -using System.Threading; -using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using ImageSharpImage = SixLabors.ImageSharp.Image; namespace LightlessSync.UI; @@ -810,11 +808,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase var primaryGamePath = entry.GamePaths.FirstOrDefault() ?? string.Empty; var classificationPath = string.IsNullOrEmpty(primaryGamePath) ? primaryFile : primaryGamePath; var mapKind = _textureMetadataHelper.DetermineMapKind(primaryGamePath, primaryFile); - var category = _textureMetadataHelper.DetermineCategory(classificationPath); - var slot = _textureMetadataHelper.DetermineSlot(category, classificationPath); + var category = TextureMetadataHelper.DetermineCategory(classificationPath); + var slot = TextureMetadataHelper.DetermineSlot(category, classificationPath); var format = entry.Format.Value; - var suggestion = _textureMetadataHelper.GetSuggestedTarget(format, mapKind); - TextureCompressionTarget? currentTarget = _textureMetadataHelper.TryMapFormatToTarget(format, out var mappedTarget) + var suggestion = TextureMetadataHelper.GetSuggestedTarget(format, mapKind); + TextureCompressionTarget? currentTarget = TextureMetadataHelper.TryMapFormatToTarget(format, out var mappedTarget) ? mappedTarget : null; var displayName = Path.GetFileName(primaryFile); @@ -2014,23 +2012,43 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private async Task BuildPreviewAsync(TextureRow row, CancellationToken token) { - if (!_ipcManager.Penumbra.APIAvailable) + const int PreviewMaxDimension = 1024; + + token.ThrowIfCancellationRequested(); + + if (!File.Exists(row.PrimaryFilePath)) { return null; } - var tempFile = Path.Combine(Path.GetTempPath(), $"lightless_preview_{Guid.NewGuid():N}.png"); try { - var job = new TextureConversionJob(row.PrimaryFilePath, tempFile, TextureType.Png, IncludeMipMaps: false); - await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { job }, null, token).ConfigureAwait(false); - if (!File.Exists(tempFile)) + using var scratch = TexFileHelper.Load(row.PrimaryFilePath); + using var rgbaScratch = scratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo); + + var meta = rgbaInfo.Meta; + var width = meta.Width; + var height = meta.Height; + var bytesPerPixel = meta.Format.BitsPerPixel() / 8; + var requiredLength = width * height * bytesPerPixel; + + token.ThrowIfCancellationRequested(); + + var rgbaPixels = rgbaScratch.Pixels[..requiredLength].ToArray(); + using var image = ImageSharpImage.LoadPixelData(rgbaPixels, width, height); + + if (Math.Max(width, height) > PreviewMaxDimension) { - return null; + var dominant = Math.Max(width, height); + var scale = PreviewMaxDimension / (float)dominant; + var targetWidth = Math.Max(1, (int)MathF.Round(width * scale)); + var targetHeight = Math.Max(1, (int)MathF.Round(height * scale)); + image.Mutate(ctx => ctx.Resize(targetWidth, targetHeight, KnownResamplers.Lanczos3)); } - var data = await File.ReadAllBytesAsync(tempFile, token).ConfigureAwait(false); - return _uiSharedService.LoadImage(data); + using var ms = new MemoryStream(); + await image.SaveAsPngAsync(ms, cancellationToken: token).ConfigureAwait(false); + return _uiSharedService.LoadImage(ms.ToArray()); } catch (OperationCanceledException) { @@ -2041,20 +2059,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _logger.LogDebug(ex, "Preview generation failed for {File}", row.PrimaryFilePath); return null; } - finally - { - try - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - } - catch (Exception ex) - { - _logger.LogTrace(ex, "Failed to clean up preview temp file {File}", tempFile); - } - } } private void ResetPreview(string key) @@ -2291,7 +2295,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { _textureSelections[row.Key] = selectedTarget; } - var hasSelectedInfo = _textureMetadataHelper.TryGetRecommendationInfo(selectedTarget, out var selectedInfo); + var hasSelectedInfo = TextureMetadataHelper.TryGetRecommendationInfo(selectedTarget, out var selectedInfo); using (ImRaii.Child("textureDetailInfo", new Vector2(-1, 0), true, ImGuiWindowFlags.AlwaysVerticalScrollbar)) { @@ -2425,7 +2429,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase if (row.SuggestedTarget.HasValue) { var recommendedTarget = row.SuggestedTarget.Value; - var hasRecommendationInfo = _textureMetadataHelper.TryGetRecommendationInfo(recommendedTarget, out var recommendedInfo); + var hasRecommendationInfo = TextureMetadataHelper.TryGetRecommendationInfo(recommendedTarget, out var recommendedInfo); var recommendedTitle = hasRecommendationInfo ? recommendedInfo!.Title : recommendedTarget.ToString(); var recommendedDescription = hasRecommendationInfo ? recommendedInfo!.Description @@ -2634,4 +2638,4 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } } -} \ No newline at end of file +} diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 063a106..5bff130 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -490,7 +490,7 @@ public sealed class DtrEntry : IDisposable, IHostedService private const byte _colorTypeForeground = 0x13; private const byte _colorTypeGlow = 0x14; - private static Colors SwapColorChannels(Colors colors) + internal static Colors SwapColorChannels(Colors colors) => new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow)); private static uint SwapColorComponent(uint color) diff --git a/LightlessSync/UI/ProfileTags.cs b/LightlessSync/UI/ProfileTags.cs deleted file mode 100644 index 885eb7a..0000000 --- a/LightlessSync/UI/ProfileTags.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace LightlessSync.UI -{ - public enum ProfileTags - { - SFW = 0, - NSFW = 1, - - RP = 2, - ERP = 3, - No_RP = 4, - No_ERP = 5, - - Venues = 6, - Gpose = 7, - - Limsa = 8, - Gridania = 9, - Ul_dah = 10, - - WUT = 11, - - PVP = 1001, - Ultimate = 1002, - Raids = 1003, - Roulette = 1004, - Crafting = 1005, - Casual = 1006, - Hardcore = 1007, - Glamour = 1008, - Mentor = 1009, - - } -} \ No newline at end of file diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 6211750..29e1880 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -28,23 +28,16 @@ using LightlessSync.WebAPI; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.SignalR.Utils; -using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using Microsoft.AspNetCore.Http.Connections; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Numerics; using System.Text; using System.Text.Json; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FfxivCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; -using FfxivCharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; namespace LightlessSync.UI; @@ -2246,7 +2239,6 @@ public class SettingsUi : WindowMediatorSubscriberBase var nameColors = _configService.Current.NameplateColors; var isFriendOverride = _configService.Current.overrideFriendColor; var isPartyOverride = _configService.Current.overridePartyColor; - var isFcTagOverride = _configService.Current.overrideFcTagColor; if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) { @@ -2280,13 +2272,6 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); _nameplateService.RequestRedraw(); } - - if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride)) - { - _configService.Current.overrideFcTagColor = isFcTagOverride; - _configService.Save(); - _nameplateService.RequestRedraw(); - } } ImGui.Spacing(); diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index 22e42aa..2332387 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -1,5 +1,4 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using LightlessSync.API.Data; @@ -13,9 +12,6 @@ using LightlessSync.UI.Services; using LightlessSync.UI.Tags; using LightlessSync.Utils; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; using System.Numerics; namespace LightlessSync.UI; diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index d8ba6fc..4941912 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -5,22 +5,17 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; -using LightlessSync.API.Dto.User; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; -using LightlessSync.Services.Profiles; -using LightlessSync.UI.Handlers; +using LightlessSync.PlayerData.Pairs; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System.Globalization; -using System.Numerics; namespace LightlessSync.UI; diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 83f32d7..629c18b 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -14,9 +14,7 @@ using LightlessSync.Utils; using LightlessSync.WebAPI; using LightlessSync.UI.Services; using Microsoft.Extensions.Logging; -using System.Collections.Specialized; using System.Numerics; -using System.Threading.Tasks; namespace LightlessSync.UI; diff --git a/LightlessSync/UI/Tags/ProfileTagRenderer.cs b/LightlessSync/UI/Tags/ProfileTagRenderer.cs index 67147ee..28f0295 100644 --- a/LightlessSync/UI/Tags/ProfileTagRenderer.cs +++ b/LightlessSync/UI/Tags/ProfileTagRenderer.cs @@ -4,8 +4,6 @@ using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using LightlessSync.Utils; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; using System.Numerics; namespace LightlessSync.UI.Tags; diff --git a/LightlessSync/UI/Tags/ProfileTagService.cs b/LightlessSync/UI/Tags/ProfileTagService.cs index 14b1a45..6f9a3ff 100644 --- a/LightlessSync/UI/Tags/ProfileTagService.cs +++ b/LightlessSync/UI/Tags/ProfileTagService.cs @@ -1,6 +1,3 @@ -using LightlessSync.UI; -using System; -using System.Collections.Generic; using System.Numerics; namespace LightlessSync.UI.Tags; @@ -35,16 +32,16 @@ public sealed class ProfileTagService private static IReadOnlyDictionary CreateTagLibrary() { - var dictionary = new Dictionary + return new Dictionary { - [(int)ProfileTags.SFW] = ProfileTagDefinition.FromIconAndText( + [0] = ProfileTagDefinition.FromIconAndText( 230419, "SFW", background: new Vector4(0.16f, 0.24f, 0.18f, 0.95f), border: new Vector4(0.32f, 0.52f, 0.34f, 0.85f), textColor: new Vector4(0.78f, 0.94f, 0.80f, 1f)), - [(int)ProfileTags.NSFW] = ProfileTagDefinition.FromIconAndText( + [1] = ProfileTagDefinition.FromIconAndText( 230419, "NSFW", background: new Vector4(0.32f, 0.18f, 0.22f, 0.95f), @@ -52,28 +49,28 @@ public sealed class ProfileTagService textColor: new Vector4(1f, 0.82f, 0.86f, 1f)), - [(int)ProfileTags.RP] = ProfileTagDefinition.FromIconAndText( + [2] = ProfileTagDefinition.FromIconAndText( 61545, "RP", background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f), border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f), textColor: new Vector4(0.80f, 0.84f, 1f, 1f)), - [(int)ProfileTags.ERP] = ProfileTagDefinition.FromIconAndText( + [3] = ProfileTagDefinition.FromIconAndText( 61545, "ERP", background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f), border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f), textColor: new Vector4(0.80f, 0.84f, 1f, 1f)), - [(int)ProfileTags.No_RP] = ProfileTagDefinition.FromIconAndText( + [4] = ProfileTagDefinition.FromIconAndText( 230420, "No RP", background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f), border: new Vector4(0.69f, 0.40f, 0.65f, 0.85f), textColor: new Vector4(1f, 0.84f, 1f, 1f)), - [(int)ProfileTags.No_ERP] = ProfileTagDefinition.FromIconAndText( + [5] = ProfileTagDefinition.FromIconAndText( 230420, "No ERP", background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f), @@ -81,14 +78,14 @@ public sealed class ProfileTagService textColor: new Vector4(1f, 0.84f, 1f, 1f)), - [(int)ProfileTags.Venues] = ProfileTagDefinition.FromIconAndText( + [6] = ProfileTagDefinition.FromIconAndText( 60756, "Venues", background: new Vector4(0.18f, 0.24f, 0.28f, 0.95f), border: new Vector4(0.33f, 0.55f, 0.63f, 0.85f), textColor: new Vector4(0.78f, 0.90f, 0.97f, 1f)), - [(int)ProfileTags.Gpose] = ProfileTagDefinition.FromIconAndText( + [7] = ProfileTagDefinition.FromIconAndText( 61546, "GPose", background: new Vector4(0.18f, 0.18f, 0.26f, 0.95f), @@ -96,36 +93,33 @@ public sealed class ProfileTagService textColor: new Vector4(0.80f, 0.82f, 0.96f, 1f)), - [(int)ProfileTags.Limsa] = ProfileTagDefinition.FromIconAndText( + [8] = ProfileTagDefinition.FromIconAndText( 60572, "Limsa"), - [(int)ProfileTags.Gridania] = ProfileTagDefinition.FromIconAndText( + [9] = ProfileTagDefinition.FromIconAndText( 60573, "Gridania"), - [(int)ProfileTags.Ul_dah] = ProfileTagDefinition.FromIconAndText( + [10] = ProfileTagDefinition.FromIconAndText( 60574, "Ul'dah"), - [(int)ProfileTags.WUT] = ProfileTagDefinition.FromIconAndText( + [11] = ProfileTagDefinition.FromIconAndText( 61397, "WU/T"), - [(int)ProfileTags.PVP] = ProfileTagDefinition.FromIcon(61806), - [(int)ProfileTags.Ultimate] = ProfileTagDefinition.FromIcon(61832), - [(int)ProfileTags.Raids] = ProfileTagDefinition.FromIcon(61802), - [(int)ProfileTags.Roulette] = ProfileTagDefinition.FromIcon(61807), - [(int)ProfileTags.Crafting] = ProfileTagDefinition.FromIcon(61816), - [(int)ProfileTags.Casual] = ProfileTagDefinition.FromIcon(61753), - [(int)ProfileTags.Hardcore] = ProfileTagDefinition.FromIcon(61754), - [(int)ProfileTags.Glamour] = ProfileTagDefinition.FromIcon(61759), - [(int)ProfileTags.Mentor] = ProfileTagDefinition.FromIcon(61760) - + [1001] = ProfileTagDefinition.FromIcon(61806), // PVP + [1002] = ProfileTagDefinition.FromIcon(61832), // Ultimate + [1003] = ProfileTagDefinition.FromIcon(61802), // Raids + [1004] = ProfileTagDefinition.FromIcon(61807), // Roulette + [1005] = ProfileTagDefinition.FromIcon(61816), // Crafting + [1006] = ProfileTagDefinition.FromIcon(61753), // Casual + [1007] = ProfileTagDefinition.FromIcon(61754), // Hardcore + [1008] = ProfileTagDefinition.FromIcon(61759), // Glamour + [1009] = ProfileTagDefinition.FromIcon(61760) // Mentor }; - - return dictionary; } } diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index ae96655..0673682 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -179,9 +179,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase int i = 0; double dblSByte = bytes; - while (dblSByte >= 1000 && i < suffix.Length - 1) + while (dblSByte >= 1024 && i < suffix.Length - 1) { - dblSByte /= 1000.0; + dblSByte /= 1024.0; i++; } diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 91bdee0..94df4fa 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -1,4 +1,3 @@ -using Dalamud.Utility; using K4os.Compression.LZ4.Legacy; using LightlessSync.API.Data; using LightlessSync.API.Dto.Files; @@ -10,13 +9,9 @@ using LightlessSync.Services.Mediator; using LightlessSync.Services.TextureCompression; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; -using System.IO; using System.Net; using System.Net.Http.Json; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.LightlessConfiguration; namespace LightlessSync.WebAPI.Files; diff --git a/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs b/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs index d6937c4..ac77b23 100644 --- a/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs +++ b/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs @@ -4,7 +4,6 @@ using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.SignalR; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Net.Sockets; diff --git a/LightlessSync/WebAPI/Files/FileUploadManager.cs b/LightlessSync/WebAPI/Files/FileUploadManager.cs index 4fb89b7..b5db541 100644 --- a/LightlessSync/WebAPI/Files/FileUploadManager.cs +++ b/LightlessSync/WebAPI/Files/FileUploadManager.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Collections.Concurrent; -using System.Threading; namespace LightlessSync.WebAPI.Files;