merge 2.0.0 into migration
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<string, byte> _pendingCompactions;
|
||||
private readonly ILogger<FileCompactor> _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,18 +295,29 @@ 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);
|
||||
}
|
||||
|
||||
return (flowControl: true, value: default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compressing the given path with BTRFS or NTFS file system.
|
||||
@@ -1149,17 +1150,15 @@ 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);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
if (_isWindows)
|
||||
{
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
193
LightlessSync/Interop/Ipc/Framework/IpcFramework.cs
Normal file
193
LightlessSync/Interop/Ipc/Framework/IpcFramework.cs
Normal file
@@ -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<IIpcInterop> _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>(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();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public interface IIpcCaller : IDisposable
|
||||
{
|
||||
bool APIAvailable { get; }
|
||||
void CheckAPI();
|
||||
}
|
||||
@@ -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<IpcCallerBrio> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ICallGateSubscriber<(int, int)> _brioApiVersion;
|
||||
@@ -25,10 +29,8 @@ public sealed class IpcCallerBrio : IIpcCaller
|
||||
private readonly ICallGateSubscriber<bool> _brioFreezePhysics;
|
||||
|
||||
|
||||
public bool APIAvailable { get; private set; }
|
||||
|
||||
public IpcCallerBrio(ILogger<IpcCallerBrio> 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<IGameObject?> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ushort, (int, Guid?)> _customizePlusGetActiveProfile;
|
||||
private readonly ICallGateSubscriber<Guid, (int, string?)> _customizePlusGetProfileById;
|
||||
@@ -23,7 +26,7 @@ public sealed class IpcCallerCustomize : IIpcCaller
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
public IpcCallerCustomize(ILogger<IpcCallerCustomize> 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<ushort, (int, Guid?)>("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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IpcCallerGlamourer> _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<IpcCallerGlamourer> 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));
|
||||
|
||||
@@ -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<IpcCallerHeels> _logger;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
@@ -18,6 +21,7 @@ public sealed class IpcCallerHeels : IIpcCaller
|
||||
private readonly ICallGateSubscriber<int, object?> _heelsUnregisterPlayer;
|
||||
|
||||
public IpcCallerHeels(ILogger<IpcCallerHeels> 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 };
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int, object> _honorificClearCharacterTitle;
|
||||
private readonly ICallGateSubscriber<object> _honorificDisposing;
|
||||
@@ -22,7 +25,7 @@ public sealed class IpcCallerHonorific : IIpcCaller
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
|
||||
public IpcCallerHonorific(ILogger<IpcCallerHonorific> 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 };
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
@@ -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<int> _moodlesApiVersion;
|
||||
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||
@@ -19,7 +22,7 @@ public sealed class IpcCallerMoodles : IIpcCaller
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
public IpcCallerMoodles(ILogger<IpcCallerMoodles> 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;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IntPtr, bool> _penumbraRedrawRequests = new();
|
||||
private readonly ConcurrentDictionary<IntPtr, byte> _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<nint, string, string> _penumbraGameObjectResourcePathResolved;
|
||||
private readonly EventSubscriber _penumbraInit;
|
||||
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||
private readonly EventSubscriber<nint, int> _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<Guid, string> _activeTemporaryCollections = new();
|
||||
private int _performedInitialCleanup;
|
||||
private readonly GetModDirectory _penumbraGetModDirectory;
|
||||
private readonly EventSubscriber _penumbraInit;
|
||||
private readonly EventSubscriber _penumbraDispose;
|
||||
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||
|
||||
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator, RedrawManager redrawManager, ActorObjectService actorObjectService) : base(logger, lightlessMediator)
|
||||
{
|
||||
_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);
|
||||
private bool _shownPenumbraUnavailable;
|
||||
private string? _modDirectory;
|
||||
|
||||
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded);
|
||||
public IpcCallerPenumbra(
|
||||
ILogger<IpcCallerPenumbra> logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
RedrawManager redrawManager,
|
||||
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||
{
|
||||
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
||||
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
||||
_penumbraInit = Initialized.Subscriber(pluginInterface, HandlePenumbraInitialized);
|
||||
_penumbraDispose = Disposed.Subscriber(pluginInterface, HandlePenumbraDisposed);
|
||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
||||
|
||||
_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<PenumbraRedrawCharacterMessage>(this, (msg) =>
|
||||
{
|
||||
_penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (msg) => _shownPenumbraUnavailable = false);
|
||||
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg =>
|
||||
{
|
||||
if (msg.Descriptor.Address != nint.Zero)
|
||||
{
|
||||
_trackedActors[(IntPtr)msg.Descriptor.Address] = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg =>
|
||||
public string? ModDirectory
|
||||
{
|
||||
if (msg.Descriptor.Address != nint.Zero)
|
||||
get => _modDirectory;
|
||||
private set
|
||||
{
|
||||
_trackedActors.TryRemove((IntPtr)msg.Descriptor.Address, out _);
|
||||
if (string.Equals(_modDirectory, value, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, msg =>
|
||||
{
|
||||
if (msg.GameObjectHandler.Address != nint.Zero)
|
||||
{
|
||||
_trackedActors[(IntPtr)msg.GameObjectHandler.Address] = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(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;
|
||||
}
|
||||
_modDirectory = value;
|
||||
Mediator.Publish(new PenumbraDirectoryChangedMessage(_modDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
public Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex)
|
||||
=> _collections.AssignTemporaryCollectionAsync(logger, collectionId, objectIndex);
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
bool penumbraAvailable = false;
|
||||
try
|
||||
{
|
||||
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
|
||||
{
|
||||
penumbraAvailable &= _penumbraEnabled.Invoke();
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
public Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
=> _collections.CreateTemporaryCollectionAsync(logger, uid);
|
||||
|
||||
if (APIAvailable)
|
||||
{
|
||||
ScheduleTemporaryCollectionCleanup();
|
||||
}
|
||||
}
|
||||
public Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId)
|
||||
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
|
||||
|
||||
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> 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<Dictionary<string, HashSet<string>>?> 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<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? 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<PenumbraRedrawCharacterMessage>(this, msg =>
|
||||
{
|
||||
_redraw.RequestImmediateRedraw(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false);
|
||||
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => _resources.TrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => _resources.UntrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(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<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? 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<Guid> 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<Dictionary<string, HashSet<string>>?> 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<string, string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IpcCallerPetNames> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
@@ -24,7 +27,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
private readonly ICallGateSubscriber<ushort, object> _clearPlayerData;
|
||||
|
||||
public IpcCallerPetNames(ILogger<IpcCallerPetNames> 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);
|
||||
|
||||
@@ -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<IpcProvider> _logger;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider;
|
||||
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
|
||||
private ICallGateProvider<List<nint>>? _handledGameAddresses;
|
||||
private readonly List<IpcRegister> _ipcRegisters = [];
|
||||
private readonly List<GameObjectHandler> _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<string, IGameObject, bool>("LightlessSync.LoadMcdf");
|
||||
_loadFileProvider.RegisterFunc(LoadMcdf);
|
||||
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("LightlessSync.LoadMcdfAsync");
|
||||
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
|
||||
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("LightlessSync.GetHandledAddresses");
|
||||
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
|
||||
_ipcRegisters.Add(RegisterFunc<string, IGameObject, bool>("LightlessSync.LoadMcdf", LoadMcdf));
|
||||
_ipcRegisters.Add(RegisterFunc<string, IGameObject, Task<bool>>("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<List<nint>> handler)
|
||||
{
|
||||
var provider = _pi.GetIpcProvider<List<nint>>(label);
|
||||
provider.RegisterFunc(handler);
|
||||
return new IpcRegister(provider.UnregisterFunc);
|
||||
}
|
||||
|
||||
private IpcRegister RegisterFunc<T1, T2, TRet>(string label, Func<T1, T2, TRet> handler)
|
||||
{
|
||||
var provider = _pi.GetIpcProvider<T1, T2, TRet>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs
Normal file
27
LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs
Normal file
@@ -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; }
|
||||
}
|
||||
197
LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs
Normal file
197
LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs
Normal file
@@ -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<Guid, string> _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<Guid> 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<string, string> 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<string, string>(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);
|
||||
}
|
||||
81
LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs
Normal file
81
LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs
Normal file
@@ -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<nint, int> _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();
|
||||
}
|
||||
}
|
||||
141
LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs
Normal file
141
LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs
Normal file
@@ -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<nint, string, string> _gameObjectResourcePathResolved;
|
||||
private readonly ConcurrentDictionary<IntPtr, byte> _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<Dictionary<string, HashSet<string>>?> 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<string>(), Array.Empty<string[]>());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
121
LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs
Normal file
121
LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs
Normal file
@@ -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<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -2,6 +2,9 @@ using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// performance metrics for each pair handler
|
||||
/// </summary>
|
||||
public interface IPairPerformanceSubject
|
||||
{
|
||||
string Ident { get; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// ui wrapper around a pair connection
|
||||
/// </summary>
|
||||
public class Pair
|
||||
{
|
||||
private readonly PairLedger _pairLedger;
|
||||
|
||||
136
LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs
Normal file
136
LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// handles group related pair events
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
302
LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs
Normal file
302
LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// handles user pair events
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// wires mediator events into the pair system
|
||||
/// </summary>
|
||||
public sealed partial class PairCoordinator : MediatorSubscriberBase
|
||||
{
|
||||
private readonly ILogger<PairCoordinator> _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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
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,23 +1044,46 @@ 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<ObjectKind, HashSet<PlayerChanges>> 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)
|
||||
{
|
||||
if (cachedModdedPaths is not null)
|
||||
{
|
||||
moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer);
|
||||
}
|
||||
else
|
||||
{
|
||||
int attempts = 0;
|
||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
@@ -1082,6 +1134,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
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<DownloadFileTransfer>());
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// creates, tracks, and removes pair handlers
|
||||
/// </summary>
|
||||
public sealed class PairHandlerRegistry : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<string, IPairHandlerAdapter> _identToHandler = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<IPairHandlerAdapter, HashSet<PairUniqueIdentifier>> _handlerToPairs = new();
|
||||
private readonly Dictionary<string, CancellationTokenSource> _waitingRequests = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new();
|
||||
|
||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
@@ -24,7 +19,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
private readonly ILogger<PairHandlerRegistry> _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<PairUniqueIdentifier>();
|
||||
_handlerToPairs[handler] = set;
|
||||
}
|
||||
|
||||
set.Add(registration.PairIdent);
|
||||
entry.AddPair(registration.PairIdent);
|
||||
}
|
||||
|
||||
ApplyPauseStateForHandler(handler);
|
||||
@@ -109,15 +97,14 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_identToHandler.TryGetValue(registration.CharacterIdent, out handler))
|
||||
if (!_entriesByIdent.TryGetValue(registration.CharacterIdent, out var entry))
|
||||
{
|
||||
return PairOperationResult<PairUniqueIdentifier>.Fail($"Ident {registration.CharacterIdent} not registered.");
|
||||
}
|
||||
|
||||
if (_handlerToPairs.TryGetValue(handler, out var set))
|
||||
{
|
||||
set.Remove(registration.PairIdent);
|
||||
if (set.Count == 0)
|
||||
handler = entry.Handler;
|
||||
entry.RemovePair(registration.PairIdent);
|
||||
if (entry.PairCount == 0)
|
||||
{
|
||||
if (forceDisposal)
|
||||
{
|
||||
@@ -130,7 +117,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisposeImmediately && handler is not null)
|
||||
{
|
||||
@@ -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,16 +148,11 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
return PairOperationResult.Fail(registerResult.Error);
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_identToHandler.TryGetValue(registration.CharacterIdent, out handler);
|
||||
}
|
||||
}
|
||||
|
||||
if (handler is null)
|
||||
if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}.");
|
||||
}
|
||||
}
|
||||
|
||||
handler.ApplyData(dto.CharaData);
|
||||
return PairOperationResult.Ok();
|
||||
@@ -185,13 +160,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
|
||||
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<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>> GetPairConnections(string ident)
|
||||
{
|
||||
IPairHandlerAdapter? handler;
|
||||
HashSet<PairUniqueIdentifier>? 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<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.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<IPairHandlerAdapter> 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<IPairHandlerAdapter> 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<PairUniqueIdentifier>();
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// keeps pair info for ui and reapplication
|
||||
/// </summary>
|
||||
public sealed class PairLedger : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly PairManager _pairManager;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// in memory state for pairs, groups, and syncshells
|
||||
/// </summary>
|
||||
public sealed class PairManager
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// core models for the pair system
|
||||
/// </summary>
|
||||
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 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 struct PairOperationResult<T>
|
||||
{
|
||||
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<T> Ok(T value) => new(true, value, null);
|
||||
|
||||
public static PairOperationResult<T> Fail(string error) => new(false, default!, error);
|
||||
}
|
||||
public readonly record struct PairUniqueIdentifier(string UserId);
|
||||
|
||||
/// <summary>
|
||||
/// link between a pair id and character ident
|
||||
/// </summary>
|
||||
public sealed record PairRegistration(PairUniqueIdentifier PairIdent, string? CharacterIdent);
|
||||
|
||||
/// <summary>
|
||||
/// per group membership info for a pair
|
||||
/// </summary>
|
||||
public sealed class GroupPairRelationship
|
||||
{
|
||||
public GroupPairRelationship(string groupId, GroupPairUserInfo? info)
|
||||
@@ -60,6 +43,9 @@ public sealed class GroupPairRelationship
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// runtime view of a single pair connection
|
||||
/// </summary>
|
||||
public sealed class PairConnection
|
||||
{
|
||||
public PairConnection(UserData user)
|
||||
@@ -121,6 +107,9 @@ public sealed class PairConnection
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// syncshell metadata plus member connections
|
||||
/// </summary>
|
||||
public sealed class Syncshell
|
||||
{
|
||||
public Syncshell(GroupFullInfoDto dto)
|
||||
@@ -138,12 +127,94 @@ public sealed class Syncshell
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PairState
|
||||
/// <summary>
|
||||
/// simple success/failure result
|
||||
/// </summary>
|
||||
public readonly struct PairOperationResult
|
||||
{
|
||||
public CharacterData? CharacterData { get; set; }
|
||||
public Guid? TemporaryCollectionId { get; set; }
|
||||
|
||||
public bool IsEmpty => CharacterData is null && (TemporaryCollectionId is null || TemporaryCollectionId == Guid.Empty);
|
||||
private PairOperationResult(bool success, string? error)
|
||||
{
|
||||
Success = success;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public readonly record struct PairUniqueIdentifier(string UserId);
|
||||
public bool Success { get; }
|
||||
public string? Error { get; }
|
||||
|
||||
public static PairOperationResult Ok() => new(true, null);
|
||||
|
||||
public static PairOperationResult Fail(string error) => new(false, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// typed success/failure result
|
||||
/// </summary>
|
||||
public readonly struct PairOperationResult<T>
|
||||
{
|
||||
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<T> Ok(T value) => new(true, value, null);
|
||||
|
||||
public static PairOperationResult<T> Fail(string error) => new(false, default!, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// state of which optional plugin warnings were shown
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// tracks the handler registered pairs for an ident
|
||||
/// </summary>
|
||||
internal sealed class PairHandlerEntry
|
||||
{
|
||||
private readonly HashSet<PairUniqueIdentifier> _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<PairUniqueIdentifier> SnapshotPairs()
|
||||
{
|
||||
if (_pairs.Count == 0)
|
||||
{
|
||||
return Array.Empty<PairUniqueIdentifier>();
|
||||
}
|
||||
|
||||
return _pairs.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// cache for character/pair data and penumbra collections
|
||||
/// </summary>
|
||||
public sealed class PairStateCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PairState> _cache = new(StringComparer.Ordinal);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// pushes character data to visible pairs
|
||||
/// </summary>
|
||||
public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
|
||||
@@ -228,7 +228,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService<ILogger<IpcCallerPetNames>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new IpcCallerBrio(s.GetRequiredService<ILogger<IpcCallerBrio>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>()));
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new IpcManager(s.GetRequiredService<ILogger<IpcManager>>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
|
||||
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
|
||||
@@ -340,8 +340,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<DalamudUtilService>(),
|
||||
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
|
||||
s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||
s.GetRequiredService<PairUiService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), clientState, gameGui, objectTable, gameInteropProvider,
|
||||
s.GetRequiredService<LightlessMediator>(),s.GetRequiredService<PairUiService>()));
|
||||
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairUiService>()));
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
|
||||
namespace LightlessSync.Services.Chat;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,45 +1,122 @@
|
||||
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
|
||||
/// <summary>
|
||||
/// NameplateService is used for coloring our nameplates based on the settings of the user.
|
||||
/// </summary>
|
||||
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<UpdateNameplateDelegate>? _nameplateHook = null;
|
||||
|
||||
private readonly ILogger<NameplateService> _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<NameplateService> 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<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||
interop.InitializeFromAttributes(this);
|
||||
_nameplateHook?.Enable();
|
||||
Refresh();
|
||||
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
||||
}
|
||||
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
/// <summary>
|
||||
/// 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 <see cref="SetNameplate"/>,
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the player should be colored based on conditions (isFriend, IsInParty)
|
||||
/// </summary>
|
||||
/// <param name="playerCharacter">Player character that will be checked</param>
|
||||
/// <param name="visibleUserIds">All visible users in the current object table</param>
|
||||
/// <returns>PLayer should or shouldnt be colored based on the result. True means colored</returns>
|
||||
private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet<ulong> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting up the nameplate of the user to be colored
|
||||
/// </summary>
|
||||
/// <param name="namePlateInfo">Information given from the Signature to be updated</param>
|
||||
/// <param name="battleChara">Character from FF</param>
|
||||
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();
|
||||
@@ -48,67 +125,130 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
||||
.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 originalName = player.Name.ToString();
|
||||
|
||||
//Check if not null of the name
|
||||
if (string.IsNullOrEmpty(originalName))
|
||||
return;
|
||||
|
||||
//Check if any characters/symbols are forbidden
|
||||
if (HasForbiddenSeStringChars(originalName))
|
||||
return;
|
||||
|
||||
//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);
|
||||
|
||||
//Replace string of nameplate with our colored one
|
||||
namePlateInfo->Name.SetString(coloredName.EncodeWithNullTerminator());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina
|
||||
/// </summary>
|
||||
/// <param name="rgb">Color code</param>
|
||||
/// <returns>Vector4 Color</returns>
|
||||
private static Vector4 RgbUintToVector4(uint rgb)
|
||||
{
|
||||
var playerCharacter = handler.PlayerCharacter;
|
||||
if (playerCharacter == null)
|
||||
continue;
|
||||
float r = ((rgb >> 16) & 0xFF) / 255f;
|
||||
float g = ((rgb >> 8) & 0xFF) / 255f;
|
||||
float b = (rgb & 0xFF) / 255f;
|
||||
return new Vector4(r, g, b, 1f);
|
||||
}
|
||||
|
||||
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 (visibleUsersIds.Contains(handler.GameObjectId) &&
|
||||
!(
|
||||
(isInParty && !partyColorAllowed) ||
|
||||
(isFriend && !friendColorAllowed)
|
||||
))
|
||||
/// <summary>
|
||||
/// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append.
|
||||
/// </summary>
|
||||
/// <param name="s">String that has to be checked</param>
|
||||
/// <returns>Contains forbidden characters/symbols or not</returns>
|
||||
private static bool HasForbiddenSeStringChars(string s)
|
||||
{
|
||||
handler.NameParts.TextWrap = CreateTextWrap(colors);
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return false;
|
||||
|
||||
if (_configService.Current.overrideFcTagColor)
|
||||
foreach (var ch in s)
|
||||
{
|
||||
bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0;
|
||||
bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId;
|
||||
bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm);
|
||||
if (ch == '\0' || ch == '\u0002')
|
||||
return true;
|
||||
}
|
||||
|
||||
if (shouldColorFcArea)
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the given string with the given edge and text color.
|
||||
/// </summary>
|
||||
/// <param name="text">String that has to be wrapped</param>
|
||||
/// <param name="edgeColor">Edge(border) color</param>
|
||||
/// <param name="textColor">Text color</param>
|
||||
/// <returns>Color wrapped SeString</returns>
|
||||
public static SeString WrapStringInColor(string text, uint? edgeColor = null, uint? textColor = null)
|
||||
{
|
||||
handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors);
|
||||
handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request redraw of nameplates
|
||||
/// </summary>
|
||||
public void RequestRedraw()
|
||||
{
|
||||
_namePlateGui.RequestRedraw();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color)
|
||||
/// <summary>
|
||||
/// Toggles the refresh of the Nameplate addon
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
312
LightlessSync/Services/TextureCompression/IndexDownscaler.cs
Normal file
312
LightlessSync/Services/TextureCompression/IndexDownscaler.cs
Normal file
@@ -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<Rgba32> Downscale(Image<Rgba32> 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<Rgba32>(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<Rgba32> ordered = stackalloc Rgba32[4]
|
||||
{
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
topRight,
|
||||
topLeft
|
||||
};
|
||||
|
||||
Span<float> 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<Rgba32> fallback = stackalloc Rgba32[4] { topLeft, topRight, bottomLeft, bottomRight };
|
||||
return PickMajorityColor(fallback);
|
||||
}
|
||||
|
||||
private static bool TryAccumulateSampleWeights(ReadOnlySpan<Rgba32> colors, in Vector2 sampleUv, Span<float> 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<int> 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<int> 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<int> 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<int> 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<float> 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<Rgba32> colors)
|
||||
{
|
||||
var counts = new Dictionary<Rgba32, int>(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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Lumina.Data.Files;
|
||||
using OtterTex;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.Services.TextureCompression;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<bool> 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<Rgba32> ReduceIndexTexture(Image<Rgba32> 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<Rgba32>(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<Rgba32> ReduceLinearTexture(Image<Rgba32> 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<Rgba32> ordered = stackalloc Rgba32[4]
|
||||
{
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
topRight,
|
||||
topLeft
|
||||
};
|
||||
|
||||
Span<float> 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<Rgba32> 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<Rgba32> colors, in Vector2 sampleUv, Span<float> 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<int> 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<int> 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<int> 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<int> 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<float> 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<Rgba32> colors)
|
||||
{
|
||||
var counts = new Dictionary<Rgba32, int>(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)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,5 @@ public enum TextureMapKind
|
||||
Specular,
|
||||
Mask,
|
||||
Index,
|
||||
Emissive,
|
||||
Ui,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -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<MaterialCandidate> candidates)
|
||||
private static void AddGameMaterialCandidates(string? gamePath, IList<MaterialCandidate> candidates)
|
||||
{
|
||||
var normalized = Normalize(gamePath);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
@@ -286,7 +278,7 @@ public sealed class TextureMetadataHelper
|
||||
}
|
||||
}
|
||||
|
||||
private void AddLocalMaterialCandidates(string? localTexturePath, IList<MaterialCandidate> candidates)
|
||||
private static void AddLocalMaterialCandidates(string? localTexturePath, IList<MaterialCandidate> 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
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ public enum TextureUsageCategory
|
||||
Companion,
|
||||
Monster,
|
||||
Housing,
|
||||
Ui,
|
||||
UI,
|
||||
VisualEffect,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<IDalamudTextureWrap?> 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<Rgba32>(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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<int, ProfileTagDefinition> CreateTagLibrary()
|
||||
{
|
||||
var dictionary = new Dictionary<int, ProfileTagDefinition>
|
||||
return new Dictionary<int, ProfileTagDefinition>
|
||||
{
|
||||
[(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user