merge 2.0.0 into migration

This commit is contained in:
cake
2025-11-29 17:27:24 +01:00
60 changed files with 2604 additions and 1939 deletions

View File

@@ -471,8 +471,55 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
FileCacheSize = total; FileCacheSize = total;
var maxCacheBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled"))
if (FileCacheSize < maxCacheBytes) return; {
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 buffer = (long)(maxCacheBytes * 0.05d);
var target = maxCacheBytes - buffer; var target = maxCacheBytes - buffer;

View File

@@ -4,6 +4,7 @@ using LightlessSync.Services.Compactor;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Win32.SafeHandles; using Microsoft.Win32.SafeHandles;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Channels; using System.Threading.Channels;
@@ -16,6 +17,7 @@ public sealed partial class FileCompactor : IDisposable
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
public const ulong WOF_PROVIDER_FILE = 2UL; public const ulong WOF_PROVIDER_FILE = 2UL;
public const int _maxRetries = 3; public const int _maxRetries = 3;
private readonly bool _isWindows;
private readonly ConcurrentDictionary<string, byte> _pendingCompactions; private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
private readonly ILogger<FileCompactor> _logger; private readonly ILogger<FileCompactor> _logger;
@@ -263,24 +265,12 @@ public sealed partial class FileCompactor : IDisposable
{ {
try try
{ {
bool isWine = _dalamudUtilService?.IsWine ?? false; var (_, linuxPath) = ResolvePathsForBtrfs(fileInfo.FullName);
string linuxPath = isWine ? ToLinuxPathIfWine(fileInfo.FullName, isWine) var (ok, output, err, code) =
: fileInfo.FullName; _isWindows
? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000)
(bool ok, string so, string se, int code) res; : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000);
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);
}
if (_logger.IsEnabled(LogLevel.Debug)) 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()); _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim());
@@ -305,17 +295,28 @@ public sealed partial class FileCompactor : IDisposable
try try
{ {
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.IsWine); var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.IsWine);
var losize = GetCompressedFileSizeW(fileInfo.FullName, out uint hosize); if (blockSize <= 0)
var size = (long)hosize << 32 | losize; throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}");
return (flowControl: false, value: ((size + blockSize - 1) / blockSize) * blockSize);
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) catch (Exception ex)
{ {
if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName);
_logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName); return (flowControl: true, value: default);
} }
return (flowControl: true, value: default);
} }
/// <summary> /// <summary>
@@ -1149,18 +1150,16 @@ public sealed partial class FileCompactor : IDisposable
{ {
try try
{ {
var pathToOpen = _isWindows ? winePath : linuxPath; if (_isWindows)
{
if (string.IsNullOrEmpty(pathToOpen) || !File.Exists(pathToOpen)) using var _ = new FileStream(winePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return false; }
else
using var _ = new FileStream(pathToOpen, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); {
using var _ = new FileStream(linuxPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
return true; return true;
} }
catch
{
return false;
}
} }
/// <summary> /// <summary>

View 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();
}
}

View File

@@ -1,7 +0,0 @@
namespace LightlessSync.Interop.Ipc;
public interface IIpcCaller : IDisposable
{
bool APIAvailable { get; }
void CheckAPI();
}

View File

@@ -2,15 +2,19 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics; using System.Numerics;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
namespace LightlessSync.Interop.Ipc; 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 ILogger<IpcCallerBrio> _logger;
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly ICallGateSubscriber<(int, int)> _brioApiVersion; private readonly ICallGateSubscriber<(int, int)> _brioApiVersion;
@@ -25,10 +29,8 @@ public sealed class IpcCallerBrio : IIpcCaller
private readonly ICallGateSubscriber<bool> _brioFreezePhysics; private readonly ICallGateSubscriber<bool> _brioFreezePhysics;
public bool APIAvailable { get; private set; }
public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface, public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface,
DalamudUtilService dalamudUtilService) DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator, dalamudPluginInterface, BrioDescriptor)
{ {
_logger = logger; _logger = logger;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
@@ -46,19 +48,6 @@ public sealed class IpcCallerBrio : IIpcCaller
CheckAPI(); CheckAPI();
} }
public void CheckAPI()
{
try
{
var version = _brioApiVersion.InvokeFunc();
APIAvailable = (version.Item1 == 2 && version.Item2 >= 0);
}
catch
{
APIAvailable = false;
}
}
public async Task<IGameObject?> SpawnActorAsync() public async Task<IGameObject?> SpawnActorAsync()
{ {
if (!APIAvailable) return null; 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); 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);
} }
} }

View File

@@ -2,6 +2,7 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using Dalamud.Utility; using Dalamud.Utility;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -9,8 +10,10 @@ using System.Text;
namespace LightlessSync.Interop.Ipc; 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<(int, int)> _customizePlusApiVersion;
private readonly ICallGateSubscriber<ushort, (int, Guid?)> _customizePlusGetActiveProfile; private readonly ICallGateSubscriber<ushort, (int, Guid?)> _customizePlusGetActiveProfile;
private readonly ICallGateSubscriber<Guid, (int, string?)> _customizePlusGetProfileById; private readonly ICallGateSubscriber<Guid, (int, string?)> _customizePlusGetProfileById;
@@ -23,7 +26,7 @@ public sealed class IpcCallerCustomize : IIpcCaller
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
public IpcCallerCustomize(ILogger<IpcCallerCustomize> logger, IDalamudPluginInterface dalamudPluginInterface, 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"); _customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion");
_customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber<ushort, (int, Guid?)>("CustomizePlus.Profile.GetActiveProfileIdOnCharacter"); _customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber<ushort, (int, Guid?)>("CustomizePlus.Profile.GetActiveProfileIdOnCharacter");
@@ -41,8 +44,6 @@ public sealed class IpcCallerCustomize : IIpcCaller
CheckAPI(); CheckAPI();
} }
public bool APIAvailable { get; private set; } = false;
public async Task RevertAsync(nint character) public async Task RevertAsync(nint character)
{ {
if (!APIAvailable) return; if (!APIAvailable) return;
@@ -113,16 +114,25 @@ public sealed class IpcCallerCustomize : IIpcCaller
return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); 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 try
{ {
var version = _customizePlusApiVersion.InvokeFunc(); 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)); _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); _customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange);
} }
} }

View File

@@ -2,6 +2,7 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using Glamourer.Api.Helpers; using Glamourer.Api.Helpers;
using Glamourer.Api.IpcSubscribers; using Glamourer.Api.IpcSubscribers;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
@@ -10,8 +11,9 @@ using Microsoft.Extensions.Logging;
namespace LightlessSync.Interop.Ipc; 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 ILogger<IpcCallerGlamourer> _logger;
private readonly IDalamudPluginInterface _pi; private readonly IDalamudPluginInterface _pi;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
@@ -31,7 +33,7 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC
private readonly uint LockCode = 0x6D617265; private readonly uint LockCode = 0x6D617265;
public IpcCallerGlamourer(ILogger<IpcCallerGlamourer> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator, 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); _glamourerApiVersions = new ApiVersion(pi);
_glamourerGetAllCustomization = new GetStateBase64(pi); _glamourerGetAllCustomization = new GetStateBase64(pi);
@@ -62,47 +64,6 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC
_glamourerStateChanged?.Dispose(); _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) 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; 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) private void GlamourerChanged(nint address)
{ {
_lightlessMediator.Publish(new GlamourerChangedMessage(address)); _lightlessMediator.Publish(new GlamourerChangedMessage(address));

View File

@@ -1,13 +1,16 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Interop.Ipc; 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 ILogger<IpcCallerHeels> _logger;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
@@ -18,6 +21,7 @@ public sealed class IpcCallerHeels : IIpcCaller
private readonly ICallGateSubscriber<int, object?> _heelsUnregisterPlayer; private readonly ICallGateSubscriber<int, object?> _heelsUnregisterPlayer;
public IpcCallerHeels(ILogger<IpcCallerHeels> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) public IpcCallerHeels(ILogger<IpcCallerHeels> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator)
: base(logger, lightlessMediator, pi, HeelsDescriptor)
{ {
_logger = logger; _logger = logger;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
@@ -32,8 +36,26 @@ public sealed class IpcCallerHeels : IIpcCaller
CheckAPI(); 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) private void HeelsOffsetChange(string offset)
{ {
@@ -74,20 +96,14 @@ public sealed class IpcCallerHeels : IIpcCaller
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
public void CheckAPI() protected override void Dispose(bool disposing)
{ {
try base.Dispose(disposing);
if (!disposing)
{ {
APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 }; return;
} }
catch
{
APIAvailable = false;
}
}
public void Dispose()
{
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
} }
} }

View File

@@ -1,6 +1,7 @@
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -8,8 +9,10 @@ using System.Text;
namespace LightlessSync.Interop.Ipc; 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<(uint major, uint minor)> _honorificApiVersion;
private readonly ICallGateSubscriber<int, object> _honorificClearCharacterTitle; private readonly ICallGateSubscriber<int, object> _honorificClearCharacterTitle;
private readonly ICallGateSubscriber<object> _honorificDisposing; private readonly ICallGateSubscriber<object> _honorificDisposing;
@@ -22,7 +25,7 @@ public sealed class IpcCallerHonorific : IIpcCaller
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
public IpcCallerHonorific(ILogger<IpcCallerHonorific> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, public IpcCallerHonorific(ILogger<IpcCallerHonorific> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
LightlessMediator lightlessMediator) LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, HonorificDescriptor)
{ {
_logger = logger; _logger = logger;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
@@ -41,23 +44,14 @@ public sealed class IpcCallerHonorific : IIpcCaller
CheckAPI(); CheckAPI();
} }
protected override void Dispose(bool disposing)
public bool APIAvailable { get; private set; } = false;
public void CheckAPI()
{ {
try base.Dispose(disposing);
if (!disposing)
{ {
APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 }; return;
} }
catch
{
APIAvailable = false;
}
}
public void Dispose()
{
_honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged); _honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged);
_honorificDisposing.Unsubscribe(OnHonorificDisposing); _honorificDisposing.Unsubscribe(OnHonorificDisposing);
_honorificReady.Unsubscribe(OnHonorificReady); _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() private void OnHonorificDisposing()
{ {
_lightlessMediator.Publish(new HonorificMessage(string.Empty)); _lightlessMediator.Publish(new HonorificMessage(string.Empty));

View File

@@ -1,14 +1,17 @@
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Interop.Ipc; 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<int> _moodlesApiVersion;
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange; private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus; private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
@@ -19,7 +22,7 @@ public sealed class IpcCallerMoodles : IIpcCaller
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
public IpcCallerMoodles(ILogger<IpcCallerMoodles> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, public IpcCallerMoodles(ILogger<IpcCallerMoodles> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
LightlessMediator lightlessMediator) LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, MoodlesDescriptor)
{ {
_logger = logger; _logger = logger;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -41,22 +44,14 @@ public sealed class IpcCallerMoodles : IIpcCaller
_lightlessMediator.Publish(new MoodlesMessage(character.Address)); _lightlessMediator.Publish(new MoodlesMessage(character.Address));
} }
public bool APIAvailable { get; private set; } = false; protected override void Dispose(bool disposing)
public void CheckAPI()
{ {
try base.Dispose(disposing);
if (!disposing)
{ {
APIAvailable = _moodlesApiVersion.InvokeFunc() == 3; return;
} }
catch
{
APIAvailable = false;
}
}
public void Dispose()
{
_moodlesOnChange.Unsubscribe(OnMoodlesChange); _moodlesOnChange.Unsubscribe(OnMoodlesChange);
} }
@@ -101,4 +96,25 @@ public sealed class IpcCallerMoodles : IIpcCaller
_logger.LogWarning(e, "Could not Set Moodles Status"); _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;
}
}
} }

View File

@@ -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.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
@@ -8,525 +10,210 @@ using Microsoft.Extensions.Logging;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Api.Helpers; using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers; 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; namespace LightlessSync.Interop.Ipc;
public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller public sealed class IpcCallerPenumbra : IpcServiceBase
{ {
private readonly IDalamudPluginInterface _pi; private static readonly IpcServiceDescriptor PenumbraDescriptor = new("Penumbra", "Penumbra", new Version(1, 2, 0, 22));
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 readonly ConcurrentDictionary<IntPtr, bool> _penumbraRedrawRequests = new(); private readonly PenumbraCollections _collections;
private readonly ConcurrentDictionary<IntPtr, byte> _trackedActors = new(); 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 GetEnabledState _penumbraEnabled;
private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations; private readonly GetModDirectory _penumbraGetModDirectory;
private readonly RedrawObject _penumbraRedraw; private readonly EventSubscriber _penumbraInit;
private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection; private readonly EventSubscriber _penumbraDispose;
private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod; private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
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;
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, private bool _shownPenumbraUnavailable;
LightlessMediator lightlessMediator, RedrawManager redrawManager, ActorObjectService actorObjectService) : base(logger, lightlessMediator) private string? _modDirectory;
public IpcCallerPenumbra(
ILogger<IpcCallerPenumbra> logger,
IDalamudPluginInterface pluginInterface,
DalamudUtilService dalamudUtil,
LightlessMediator mediator,
RedrawManager redrawManager,
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
{ {
_pi = pi; _penumbraEnabled = new GetEnabledState(pluginInterface);
_dalamudUtil = dalamudUtil; _penumbraGetModDirectory = new GetModDirectory(pluginInterface);
_lightlessMediator = lightlessMediator; _penumbraInit = Initialized.Subscriber(pluginInterface, HandlePenumbraInitialized);
_redrawManager = redrawManager; _penumbraDispose = Disposed.Subscriber(pluginInterface, HandlePenumbraDisposed);
_actorObjectService = actorObjectService; _penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
_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);
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); _collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService));
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
SubscribeMediatorEvents();
CheckAPI(); CheckAPI();
CheckModDirectory(); 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 =>
{
if (msg.Descriptor.Address != nint.Zero)
{
_trackedActors.TryRemove((IntPtr)msg.Descriptor.Address, out _);
}
});
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;
}
}
} }
public bool APIAvailable { get; private set; } = false; public string? ModDirectory
public void CheckAPI()
{ {
bool penumbraAvailable = false; get => _modDirectory;
try private set
{ {
var penumbraVersion = (_pi.InstalledPlugins if (string.Equals(_modDirectory, value, StringComparison.Ordinal))
.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(); return;
} }
catch
{
penumbraAvailable = false;
}
_shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable;
APIAvailable = penumbraAvailable;
}
catch
{
APIAvailable = penumbraAvailable;
}
finally
{
if (!penumbraAvailable && !_shownPenumbraUnavailable)
{
_shownPenumbraUnavailable = true;
_lightlessMediator.Publish(new NotificationMessage("Penumbra inactive",
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.",
NotificationType.Error));
}
}
if (APIAvailable) _modDirectory = value;
{ Mediator.Publish(new PenumbraDirectoryChangedMessage(_modDirectory));
ScheduleTemporaryCollectionCleanup();
} }
} }
public Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex)
=> _collections.AssignTemporaryCollectionAsync(logger, collectionId, objectIndex);
public Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
=> _collections.CreateTemporaryCollectionAsync(logger, uid);
public Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId)
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<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() public void CheckModDirectory()
{ {
if (!APIAvailable) if (!APIAvailable)
{ {
ModDirectory = string.Empty; 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; return;
}
try try
{ {
var collections = await _dalamudUtil.RunOnFrameworkThread(() => _penumbraGetCollections.Invoke()).ConfigureAwait(false); ModDirectory = _penumbraGetModDirectory.Invoke().ToLowerInvariant();
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);
}
}
} }
catch (Exception ex) 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) protected override bool IsPluginEnabled()
=> !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal); {
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) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
_redrawManager.Cancel(); if (!disposing)
{
return;
}
_penumbraModSettingChanged.Dispose(); _penumbraModSettingChanged.Dispose();
_penumbraGameObjectResourcePathResolved.Dispose();
_penumbraDispose.Dispose(); _penumbraDispose.Dispose();
_penumbraInit.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);
} }
} }

View File

@@ -1,14 +1,17 @@
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Interop.Ipc; 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 ILogger<IpcCallerPetNames> _logger;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
@@ -24,7 +27,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
private readonly ICallGateSubscriber<ushort, object> _clearPlayerData; private readonly ICallGateSubscriber<ushort, object> _clearPlayerData;
public IpcCallerPetNames(ILogger<IpcCallerPetNames> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, public IpcCallerPetNames(ILogger<IpcCallerPetNames> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
LightlessMediator lightlessMediator) LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, PetRenamerDescriptor)
{ {
_logger = logger; _logger = logger;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -46,25 +49,6 @@ public sealed class IpcCallerPetNames : IIpcCaller
CheckAPI(); 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() private void OnPetNicknamesReady()
{ {
CheckAPI(); CheckAPI();
@@ -76,6 +60,34 @@ public sealed class IpcCallerPetNames : IIpcCaller
_lightlessMediator.Publish(new PetNamesMessage(string.Empty)); _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() public string GetLocalNames()
{ {
if (!APIAvailable) return string.Empty; if (!APIAvailable) return string.Empty;
@@ -149,8 +161,14 @@ public sealed class IpcCallerPetNames : IIpcCaller
_lightlessMediator.Publish(new PetNamesMessage(data)); _lightlessMediator.Publish(new PetNamesMessage(data));
} }
public void Dispose() protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing);
if (!disposing)
{
return;
}
_petnamesReady.Unsubscribe(OnPetNicknamesReady); _petnamesReady.Unsubscribe(OnPetNicknamesReady);
_petnamesDisposing.Unsubscribe(OnPetNicknamesDispose); _petnamesDisposing.Unsubscribe(OnPetNicknamesDispose);
_playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange); _playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange);

View File

@@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Objects.Types; using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
@@ -14,9 +15,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
private readonly ILogger<IpcProvider> _logger; private readonly ILogger<IpcProvider> _logger;
private readonly IDalamudPluginInterface _pi; private readonly IDalamudPluginInterface _pi;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider; private readonly List<IpcRegister> _ipcRegisters = [];
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
private ICallGateProvider<List<nint>>? _handledGameAddresses;
private readonly List<GameObjectHandler> _activeGameObjectHandlers = []; private readonly List<GameObjectHandler> _activeGameObjectHandlers = [];
public LightlessMediator Mediator { get; init; } public LightlessMediator Mediator { get; init; }
@@ -44,12 +43,9 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Starting IpcProviderService"); _logger.LogInformation("Starting IpcProviderService");
_loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("LightlessSync.LoadMcdf"); _ipcRegisters.Add(RegisterFunc<string, IGameObject, bool>("LightlessSync.LoadMcdf", LoadMcdf));
_loadFileProvider.RegisterFunc(LoadMcdf); _ipcRegisters.Add(RegisterFunc<string, IGameObject, Task<bool>>("LightlessSync.LoadMcdfAsync", LoadMcdfAsync));
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("LightlessSync.LoadMcdfAsync"); _ipcRegisters.Add(RegisterFunc("LightlessSync.GetHandledAddresses", GetHandledAddresses));
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("LightlessSync.GetHandledAddresses");
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
_logger.LogInformation("Started IpcProviderService"); _logger.LogInformation("Started IpcProviderService");
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -57,9 +53,11 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
_logger.LogDebug("Stopping IpcProvider Service"); _logger.LogDebug("Stopping IpcProvider Service");
_loadFileProvider?.UnregisterFunc(); foreach (var register in _ipcRegisters)
_loadFileAsyncProvider?.UnregisterFunc(); {
_handledGameAddresses?.UnregisterFunc(); register.Dispose();
}
_ipcRegisters.Clear();
Mediator.UnsubscribeAll(this); Mediator.UnsubscribeAll(this);
return Task.CompletedTask; 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(); 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;
}
}
} }

View 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; }
}

View 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);
}

View 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();
}
}

View 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();
}
}

View 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)
{
}
}

View File

@@ -1,7 +1,4 @@
using System; using LightlessSync.API.Data.Enum;
using System.Collections.Generic;
using System.Linq;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;

View File

@@ -2,6 +2,9 @@ using LightlessSync.API.Data;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// performance metrics for each pair handler
/// </summary>
public interface IPairPerformanceSubject public interface IPairPerformanceSubject
{ {
string Ident { get; } string Ident { get; }

View File

@@ -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;
}

View File

@@ -1,12 +1,9 @@
using System;
using System.Linq;
using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.ServerConfiguration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -14,6 +11,9 @@ using LightlessSync.WebAPI;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// ui wrapper around a pair connection
/// </summary>
public class Pair public class Pair
{ {
private readonly PairLedger _pairLedger; private readonly PairLedger _pairLedger;

View 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);
}
}

View 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));
}
}

View File

@@ -1,21 +1,17 @@
using System;
using System.Collections.Concurrent; 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.API.Dto.User;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.Events;
using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.ServerConfiguration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.PlayerData.Pairs; 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 ILogger<PairCoordinator> _logger;
private readonly LightlessConfigService _configService; 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) private void HandleActiveServerChange(string serverUrl)
{ {
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
@@ -175,379 +132,4 @@ public sealed class PairCoordinator : MediatorSubscriberBase
_mediator.Publish(new ClearProfileGroupDataMessage()); _mediator.Publish(new ClearProfileGroupDataMessage());
PublishPairDataChanged(groupChanged: true); 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));
}
} }

View File

@@ -1,11 +1,5 @@
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
@@ -28,6 +22,9 @@ using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// orchestrates the lifecycle of a paired character
/// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{ {
string Ident { get; } string Ident { get; }
@@ -80,6 +77,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private CancellationTokenSource? _downloadCancellationTokenSource = new(); private CancellationTokenSource? _downloadCancellationTokenSource = new();
private bool _forceApplyMods = false; private bool _forceApplyMods = false;
private bool _forceFullReapply; private bool _forceFullReapply;
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
private bool _needsCollectionRebuild;
private bool _isVisible; private bool _isVisible;
private Guid _penumbraCollection; private Guid _penumbraCollection;
private readonly object _collectionGate = new(); private readonly object _collectionGate = new();
@@ -352,12 +351,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null) private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null)
{ {
Guid toRelease = Guid.Empty; Guid toRelease = Guid.Empty;
bool hadCollection = false;
lock (_collectionGate) lock (_collectionGate)
{ {
if (_penumbraCollection != Guid.Empty) if (_penumbraCollection != Guid.Empty)
{ {
toRelease = _penumbraCollection; toRelease = _penumbraCollection;
_penumbraCollection = Guid.Empty; _penumbraCollection = Guid.Empty;
hadCollection = true;
} }
} }
@@ -365,6 +366,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (cached.HasValue && cached.Value != Guid.Empty) if (cached.HasValue && cached.Value != Guid.Empty)
{ {
toRelease = cached.Value; toRelease = cached.Value;
hadCollection = true;
}
if (hadCollection)
{
_needsCollectionRebuild = true;
_forceFullReapply = true;
} }
if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable) if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable)
@@ -603,6 +611,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return data; 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() private bool CanApplyNow()
{ {
return !_dalamudUtil.IsInCombat return !_dalamudUtil.IsInCombat
@@ -847,6 +874,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
PlayerName = null; PlayerName = null;
_cachedData = null; _cachedData = null;
_lastAppliedModdedPaths = null;
_needsCollectionRebuild = false;
Logger.LogDebug("Disposing {name} complete", name); Logger.LogDebug("Disposing {name} complete", name);
} }
} }
@@ -1015,73 +1044,103 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles)); var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles));
var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip)); 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(); _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
var downloadToken = _downloadCancellationTokenSource.Token; 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 Task? _pairDownloadTask;
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, 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); await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
bool skipDownscaleForPair = ShouldSkipDownscale(); bool skipDownscaleForPair = ShouldSkipDownscale();
var user = GetPrimaryUserData(); var user = GetPrimaryUserData();
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
if (updateModdedPaths) if (updateModdedPaths)
{ {
int attempts = 0; if (cachedModdedPaths is not null)
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
{ {
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer);
}
else
{
int attempts = 0;
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
{ {
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData); if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
{
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData);
await _pairDownloadTask.ConfigureAwait(false);
}
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData);
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
$"Starting download for {toDownloadReplacements.Count} files")));
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
{
_downloadManager.ClearDownload();
return;
}
var handlerForDownload = _charaHandler;
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false));
await _pairDownloadTask.ConfigureAwait(false); await _pairDownloadTask.ConfigureAwait(false);
if (downloadToken.IsCancellationRequested)
{
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
return;
}
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
} }
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
$"Starting download for {toDownloadReplacements.Count} files")));
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
{ {
_downloadManager.ClearDownload();
return; return;
} }
var handlerForDownload = _charaHandler;
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false));
await _pairDownloadTask.ConfigureAwait(false);
if (downloadToken.IsCancellationRequested)
{
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
return;
}
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
}
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
{
return;
} }
} }
else
{
moddedPaths = cachedModdedPaths is not null
? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer)
: [];
}
downloadToken.ThrowIfCancellationRequested(); downloadToken.ThrowIfCancellationRequested();
@@ -1165,6 +1224,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection, await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection,
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false); 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; LastAppliedDataBytes = -1;
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) 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; _cachedData = charaData;
_pairStateCache.Store(Ident, charaData); _pairStateCache.Store(Ident, charaData);
_forceFullReapply = false; _forceFullReapply = false;
_needsCollectionRebuild = false;
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{ {
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>()); _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());

View File

@@ -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.Data.Extensions;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// creates, tracks, and removes pair handlers
/// </summary>
public sealed class PairHandlerRegistry : IDisposable public sealed class PairHandlerRegistry : IDisposable
{ {
private readonly object _gate = new(); private readonly object _gate = new();
private readonly Dictionary<string, IPairHandlerAdapter> _identToHandler = new(StringComparer.Ordinal); private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
private readonly Dictionary<IPairHandlerAdapter, HashSet<PairUniqueIdentifier>> _handlerToPairs = new(); private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new();
private readonly Dictionary<string, CancellationTokenSource> _waitingRequests = new(StringComparer.Ordinal);
private readonly IPairHandlerAdapterFactory _handlerFactory; private readonly IPairHandlerAdapterFactory _handlerFactory;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
@@ -24,7 +19,6 @@ public sealed class PairHandlerRegistry : IDisposable
private readonly ILogger<PairHandlerRegistry> _logger; private readonly ILogger<PairHandlerRegistry> _logger;
private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5); private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5);
private readonly TimeSpan _waitForHandlerGracePeriod = TimeSpan.FromMinutes(2);
public PairHandlerRegistry( public PairHandlerRegistry(
IPairHandlerAdapterFactory handlerFactory, IPairHandlerAdapterFactory handlerFactory,
@@ -42,7 +36,7 @@ public sealed class PairHandlerRegistry : IDisposable
{ {
lock (_gate) 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) 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; IPairHandlerAdapter handler;
lock (_gate) lock (_gate)
{ {
handler = GetOrAddHandler(registration.CharacterIdent); var entry = GetOrCreateEntry(registration.CharacterIdent);
handler = entry.Handler;
handler.ScheduledForDeletion = false; handler.ScheduledForDeletion = false;
entry.AddPair(registration.PairIdent);
if (!_handlerToPairs.TryGetValue(handler, out var set))
{
set = new HashSet<PairUniqueIdentifier>();
_handlerToPairs[handler] = set;
}
set.Add(registration.PairIdent);
} }
ApplyPauseStateForHandler(handler); ApplyPauseStateForHandler(handler);
@@ -109,25 +97,23 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate) 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."); return PairOperationResult<PairUniqueIdentifier>.Fail($"Ident {registration.CharacterIdent} not registered.");
} }
if (_handlerToPairs.TryGetValue(handler, out var set)) handler = entry.Handler;
entry.RemovePair(registration.PairIdent);
if (entry.PairCount == 0)
{ {
set.Remove(registration.PairIdent); if (forceDisposal)
if (set.Count == 0)
{ {
if (forceDisposal) shouldDisposeImmediately = true;
{ }
shouldDisposeImmediately = true; else
} {
else shouldScheduleRemoval = true;
{ handler.ScheduledForDeletion = true;
shouldScheduleRemoval = true;
handler.ScheduledForDeletion = true;
}
} }
} }
} }
@@ -154,13 +140,7 @@ public sealed class PairHandlerRegistry : IDisposable
return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}."); return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}.");
} }
IPairHandlerAdapter? handler; if (!TryGetHandler(registration.CharacterIdent, out var handler) || handler is null)
lock (_gate)
{
_identToHandler.TryGetValue(registration.CharacterIdent, out handler);
}
if (handler is null)
{ {
var registerResult = RegisterOnlinePair(registration); var registerResult = RegisterOnlinePair(registration);
if (!registerResult.Success) if (!registerResult.Success)
@@ -168,30 +148,19 @@ public sealed class PairHandlerRegistry : IDisposable
return PairOperationResult.Fail(registerResult.Error); return PairOperationResult.Fail(registerResult.Error);
} }
lock (_gate) if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null)
{ {
_identToHandler.TryGetValue(registration.CharacterIdent, out handler); return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}.");
} }
} }
if (handler is null)
{
return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}.");
}
handler.ApplyData(dto.CharaData); handler.ApplyData(dto.CharaData);
return PairOperationResult.Ok(); return PairOperationResult.Ok();
} }
public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false) public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false)
{ {
IPairHandlerAdapter? handler; if (!TryGetHandler(ident, out var handler) || handler is null)
lock (_gate)
{
_identToHandler.TryGetValue(ident, out handler);
}
if (handler is null)
{ {
return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found."); 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) public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading)
{ {
IPairHandlerAdapter? handler; if (!TryGetHandler(ident, out var handler) || handler is null)
lock (_gate)
{
_identToHandler.TryGetValue(ident, out handler);
}
if (handler is null)
{ {
return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found."); 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) public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused)
{ {
IPairHandlerAdapter? handler; if (!TryGetHandler(ident, out var handler) || handler is null)
lock (_gate)
{
_identToHandler.TryGetValue(ident, out handler);
}
if (handler is null)
{ {
return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found."); return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found.");
} }
_ = paused; // value reflected in pair manager already _ = paused; // value reflected in pair manager already
// Recalculate pause state against all registered pairs to ensure consistency across contexts.
ApplyPauseStateForHandler(handler); ApplyPauseStateForHandler(handler);
return PairOperationResult.Ok(); return PairOperationResult.Ok();
} }
public PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>> GetPairConnections(string ident) public PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>> GetPairConnections(string ident)
{ {
IPairHandlerAdapter? handler; PairHandlerEntry? entry;
HashSet<PairUniqueIdentifier>? identifiers = null;
lock (_gate) lock (_gate)
{ {
_identToHandler.TryGetValue(ident, out handler); _entriesByIdent.TryGetValue(ident, out entry);
if (handler is not null)
{
_handlerToPairs.TryGetValue(handler, out identifiers);
}
} }
if (handler is null || identifiers is null) if (entry is null)
{ {
return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Fail($"No handler registered for {ident}."); return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Fail($"No handler registered for {ident}.");
} }
var list = new List<(PairUniqueIdentifier, PairConnection)>(); var list = new List<(PairUniqueIdentifier, PairConnection)>();
foreach (var pairIdent in identifiers) foreach (var pairIdent in entry.SnapshotPairs())
{ {
var result = _pairManager.GetPair(pairIdent.UserId); var result = _pairManager.GetPair(pairIdent.UserId);
if (result.Success) if (result.Success)
@@ -279,8 +229,8 @@ public sealed class PairHandlerRegistry : IDisposable
{ {
lock (_gate) lock (_gate)
{ {
var success = _identToHandler.TryGetValue(ident, out var resolved); var success = _entriesByIdent.TryGetValue(ident, out var entry);
handler = resolved; handler = entry?.Handler;
return success; return success;
} }
} }
@@ -289,7 +239,7 @@ public sealed class PairHandlerRegistry : IDisposable
{ {
lock (_gate) lock (_gate)
{ {
return _identToHandler.Values.Distinct().ToList(); return _entriesByHandler.Keys.ToList();
} }
} }
@@ -297,9 +247,9 @@ public sealed class PairHandlerRegistry : IDisposable
{ {
lock (_gate) 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; List<IPairHandlerAdapter> handlers;
lock (_gate) lock (_gate)
{ {
handlers = _identToHandler.Values.Distinct().ToList(); handlers = _entriesByHandler.Keys.ToList();
_identToHandler.Clear(); _entriesByIdent.Clear();
_handlerToPairs.Clear(); _entriesByHandler.Clear();
foreach (var pending in _waitingRequests.Values)
{
pending.Cancel();
pending.Dispose();
}
_waitingRequests.Clear();
} }
foreach (var handler in handlers) foreach (var handler in handlers)
@@ -364,14 +306,9 @@ public sealed class PairHandlerRegistry : IDisposable
List<IPairHandlerAdapter> handlers; List<IPairHandlerAdapter> handlers;
lock (_gate) lock (_gate)
{ {
handlers = _identToHandler.Values.Distinct().ToList(); handlers = _entriesByHandler.Keys.ToList();
_identToHandler.Clear(); _entriesByIdent.Clear();
_handlerToPairs.Clear(); _entriesByHandler.Clear();
foreach (var kv in _waitingRequests.Values)
{
kv.Cancel();
}
_waitingRequests.Clear();
} }
foreach (var handler in handlers) 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); var handler = _handlerFactory.Create(ident);
_identToHandler[ident] = handler; entry = new PairHandlerEntry(ident, handler);
_handlerToPairs[handler] = new HashSet<PairUniqueIdentifier>(); _entriesByIdent[ident] = entry;
return handler; _entriesByHandler[handler] = entry;
} return entry;
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);
}
} }
private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler) private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler)
{ {
try await Task.Delay(_deletionGracePeriod).ConfigureAwait(false);
{
await Task.Delay(_deletionGracePeriod).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
return;
}
if (TryFinalizeHandlerRemoval(handler)) if (TryFinalizeHandlerRemoval(handler))
{ {
@@ -431,63 +345,15 @@ public sealed class PairHandlerRegistry : IDisposable
{ {
lock (_gate) lock (_gate)
{ {
if (!_handlerToPairs.TryGetValue(handler, out var set) || set.Count > 0) if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs)
{ {
handler.ScheduledForDeletion = false; handler.ScheduledForDeletion = false;
return false; return false;
} }
_handlerToPairs.Remove(handler); _entriesByHandler.Remove(handler);
_identToHandler.Remove(handler.Ident); _entriesByIdent.Remove(entry.Ident);
if (_waitingRequests.TryGetValue(handler.Ident, out var cts))
{
cts.Cancel();
cts.Dispose();
_waitingRequests.Remove(handler.Ident);
}
return true; 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();
}
}
} }

View File

@@ -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.API.Dto.Group;
using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models; using LightlessSync.UI.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// keeps pair info for ui and reapplication
/// </summary>
public sealed class PairLedger : DisposableMediatorSubscriberBase public sealed class PairLedger : DisposableMediatorSubscriberBase
{ {
private readonly PairManager _pairManager; private readonly PairManager _pairManager;

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
@@ -9,6 +6,9 @@ using LightlessSync.API.Dto.User;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// in memory state for pairs, groups, and syncshells
/// </summary>
public sealed class PairManager public sealed class PairManager
{ {
private readonly object _gate = new(); private readonly object _gate = new();

View File

@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
@@ -7,42 +5,27 @@ using LightlessSync.API.Dto.Group;
namespace LightlessSync.PlayerData.Pairs; 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) public CharacterData? CharacterData { get; set; }
{ public Guid? TemporaryCollectionId { get; set; }
Success = success;
Error = error;
}
public bool Success { get; } public bool IsEmpty => CharacterData is null && (TemporaryCollectionId is null || TemporaryCollectionId == Guid.Empty);
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> public readonly record struct PairUniqueIdentifier(string UserId);
{
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>
/// link between a pair id and character ident
/// </summary>
public sealed record PairRegistration(PairUniqueIdentifier PairIdent, string? CharacterIdent); public sealed record PairRegistration(PairUniqueIdentifier PairIdent, string? CharacterIdent);
/// <summary>
/// per group membership info for a pair
/// </summary>
public sealed class GroupPairRelationship public sealed class GroupPairRelationship
{ {
public GroupPairRelationship(string groupId, GroupPairUserInfo? info) 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 sealed class PairConnection
{ {
public PairConnection(UserData user) public PairConnection(UserData user)
@@ -121,6 +107,9 @@ public sealed class PairConnection
} }
} }
/// <summary>
/// syncshell metadata plus member connections
/// </summary>
public sealed class Syncshell public sealed class Syncshell
{ {
public Syncshell(GroupFullInfoDto dto) 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; } private PairOperationResult(bool success, string? error)
public Guid? TemporaryCollectionId { get; set; } {
Success = success;
Error = error;
}
public bool IsEmpty => CharacterData is null && (TemporaryCollectionId is null || TemporaryCollectionId == Guid.Empty); public bool Success { get; }
public string? Error { get; }
public static PairOperationResult Ok() => new(true, null);
public static PairOperationResult Fail(string error) => new(false, error);
} }
public readonly record struct PairUniqueIdentifier(string UserId); /// <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();
}
}

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.Utils; using LightlessSync.Utils;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// cache for character/pair data and penumbra collections
/// </summary>
public sealed class PairStateCache public sealed class PairStateCache
{ {
private readonly ConcurrentDictionary<string, PairState> _cache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, PairState> _cache = new(StringComparer.Ordinal);

View File

@@ -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;
using LightlessSync.API.Data.Comparer; using LightlessSync.API.Data.Comparer;
using LightlessSync.Services; using LightlessSync.Services;
@@ -14,6 +9,9 @@ using Microsoft.Extensions.Logging;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// pushes character data to visible pairs
/// </summary>
public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
{ {
private readonly ApiController _apiController; private readonly ApiController _apiController;

View File

@@ -228,7 +228,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService<ILogger<IpcCallerPetNames>>(), pluginInterface, collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService<ILogger<IpcCallerPetNames>>(), pluginInterface,
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>())); s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
collection.AddSingleton((s) => new IpcCallerBrio(s.GetRequiredService<ILogger<IpcCallerBrio>>(), pluginInterface, 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>>(), collection.AddSingleton((s) => new IpcManager(s.GetRequiredService<ILogger<IpcManager>>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(), 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>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<DalamudUtilService>(),
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(), pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
s.GetRequiredService<LightlessMediator>())); s.GetRequiredService<LightlessMediator>()));
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState, collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), clientState, gameGui, objectTable, gameInteropProvider,
s.GetRequiredService<PairUiService>(), s.GetRequiredService<LightlessMediator>())); s.GetRequiredService<LightlessMediator>(),s.GetRequiredService<PairUiService>()));
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(), collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairUiService>())); s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairUiService>()));

View File

@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Chat;
namespace LightlessSync.Services.Chat; namespace LightlessSync.Services.Chat;

View File

@@ -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;
using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Chat;
using LightlessSync.Services;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;

View File

@@ -1,114 +1,254 @@
using Dalamud.Game.ClientState.Objects.Enums; 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.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; 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.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics;
using static LightlessSync.UI.DtrEntry;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
namespace LightlessSync.Services; 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 ILogger<NameplateService> _logger;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly INamePlateGui _namePlateGui; private readonly IGameGui _gameGui;
private readonly IObjectTable _objectTable;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
public NameplateService(ILogger<NameplateService> logger, public NameplateService(ILogger<NameplateService> logger,
LightlessConfigService configService, LightlessConfigService configService,
INamePlateGui namePlateGui,
IClientState clientState, IClientState clientState,
PairUiService pairUiService, IGameGui gameGui,
LightlessMediator lightlessMediator) : base(logger, lightlessMediator) IObjectTable objectTable,
IGameInteropProvider interop,
LightlessMediator lightlessMediator,
PairUiService pairUiService) : base(logger, lightlessMediator)
{ {
_logger = logger; _logger = logger;
_configService = configService; _configService = configService;
_namePlateGui = namePlateGui;
_clientState = clientState; _clientState = clientState;
_gameGui = gameGui;
_objectTable = objectTable;
_pairUiService = pairUiService; _pairUiService = pairUiService;
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; interop.InitializeFromAttributes(this);
_namePlateGui.RequestRedraw(); _nameplateHook?.Enable();
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw()); 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; return;
var snapshot = _pairUiService.GetSnapshot(); var snapshot = _pairUiService.GetSnapshot();
var visibleUsersIds = snapshot.PairsByUid.Values var visibleUsersIds = snapshot.PairsByUid.Values
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
.Select(u => (ulong)u.PlayerCharacterId) .Select(u => (ulong)u.PlayerCharacterId)
.ToHashSet(); .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();
{
var playerCharacter = handler.PlayerCharacter;
if (playerCharacter == null)
continue;
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember); //Check if not null of the name
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend); if (string.IsNullOrEmpty(originalName))
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty); return;
bool friendColorAllowed = (_configService.Current.overrideFriendColor && isFriend);
if (visibleUsersIds.Contains(handler.GameObjectId) && //Check if any characters/symbols are forbidden
!( if (HasForbiddenSeStringChars(originalName))
(isInParty && !partyColorAllowed) || return;
(isFriend && !friendColorAllowed)
))
{
handler.NameParts.TextWrap = CreateTextWrap(colors);
if (_configService.Current.overrideFcTagColor) //Swap color channels as we store them in BGR format as FF loves that
{ var cfgColors = SwapColorChannels(_configService.Current.NameplateColors);
bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0; var coloredName = WrapStringInColor(originalName, cfgColors.Glow, cfgColors.Foreground);
bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId;
bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm);
if (shouldColorFcArea) //Replace string of nameplate with our colored one
{ namePlateInfo->Name.SetString(coloredName.EncodeWithNullTerminator());
handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors);
handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors);
}
}
}
}
} }
/// <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)
{
float r = ((rgb >> 16) & 0xFF) / 255f;
float g = ((rgb >> 8) & 0xFF) / 255f;
float b = (rgb & 0xFF) / 255f;
return new Vector4(r, g, b, 1f);
}
/// <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)
{
if (string.IsNullOrEmpty(s))
return false;
foreach (var ch in s)
{
if (ch == '\0' || ch == '\u0002')
return true;
}
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)
{
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() 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(); AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate");
var right = new Lumina.Text.SeStringBuilder();
left.PushColorRgba(color.Foreground); if (namePlateAddon.IsNull)
right.PopColor(); {
_logger.LogInformation("NamePlate addon is null, cannot refresh nameplates.");
return;
}
left.PushEdgeColorRgba(color.Glow); var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address;
right.PopEdgeColor();
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) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); if (disposing)
{
_nameplateHook?.Dispose();
}
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; base.Dispose(disposing);
_namePlateGui.RequestRedraw();
} }
} }

View 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;
}
}

View File

@@ -1,5 +1,3 @@
using System;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Lumina.Data.Files; using Lumina.Data.Files;
using OtterTex; using OtterTex;

View File

@@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
namespace LightlessSync.Services.TextureCompression; namespace LightlessSync.Services.TextureCompression;

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
namespace LightlessSync.Services.TextureCompression; namespace LightlessSync.Services.TextureCompression;

View File

@@ -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.Interop.Ipc;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Globalization; using System.Globalization;
using System.Numerics;
using System.IO; using System.IO;
using OtterTex; using OtterTex;
using OtterImage = OtterTex.Image; using OtterImage = OtterTex.Image;
@@ -15,7 +14,6 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
/* /*
* Index upscaler code (converted/reversed for downscaling purposes) provided by Ny
* OtterTex made by Ottermandias * OtterTex made by Ottermandias
* thank you!! * thank you!!
*/ */
@@ -183,7 +181,7 @@ public sealed class TextureDownscaleService
return; 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]; var resizedPixels = new byte[targetSize.width * targetSize.height * 4];
resized.CopyPixelDataTo(resizedPixels); resized.CopyPixelDataTo(resizedPixels);
@@ -231,8 +229,7 @@ public sealed class TextureDownscaleService
private static bool IsIndexMap(TextureMapKind kind) private static bool IsIndexMap(TextureMapKind kind)
=> kind is TextureMapKind.Mask => kind is TextureMapKind.Mask
or TextureMapKind.Index or TextureMapKind.Index;
or TextureMapKind.Ui;
private Task<bool> TryDropTopMipAsync( private Task<bool> TryDropTopMipAsync(
string hash, string hash,
@@ -423,39 +420,6 @@ public sealed class TextureDownscaleService
private static int ReduceDimension(int value) private static int ReduceDimension(int value)
=> value <= 1 ? 1 : Math.Max(1, value / 2); => 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) private static Image<Rgba32> ReduceLinearTexture(Image<Rgba32> source, int targetWidth, int targetHeight)
{ {
var clone = source.Clone(); var clone = source.Clone();
@@ -470,271 +434,6 @@ public sealed class TextureDownscaleService
return clone; 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) private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension)
{ {

View File

@@ -7,7 +7,5 @@ public enum TextureMapKind
Specular, Specular,
Mask, Mask,
Index, Index,
Emissive,
Ui,
Unknown Unknown
} }

View File

@@ -1,10 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Penumbra.Api.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
namespace LightlessSync.Services.TextureCompression; namespace LightlessSync.Services.TextureCompression;
@@ -37,9 +32,9 @@ public sealed class TextureMetadataHelper
private static readonly (TextureUsageCategory Category, string Token)[] CategoryTokens = private static readonly (TextureUsageCategory Category, string Token)[] CategoryTokens =
{ {
(TextureUsageCategory.Ui, "/ui/"), (TextureUsageCategory.UI, "/ui/"),
(TextureUsageCategory.Ui, "/uld/"), (TextureUsageCategory.UI, "/uld/"),
(TextureUsageCategory.Ui, "/icon/"), (TextureUsageCategory.UI, "/icon/"),
(TextureUsageCategory.VisualEffect, "/vfx/"), (TextureUsageCategory.VisualEffect, "/vfx/"),
@@ -104,9 +99,6 @@ public sealed class TextureMetadataHelper
(TextureMapKind.Specular, "_s"), (TextureMapKind.Specular, "_s"),
(TextureMapKind.Specular, "_spec"), (TextureMapKind.Specular, "_spec"),
(TextureMapKind.Emissive, "_em"),
(TextureMapKind.Emissive, "_glow"),
(TextureMapKind.Index, "_id"), (TextureMapKind.Index, "_id"),
(TextureMapKind.Index, "_idx"), (TextureMapKind.Index, "_idx"),
(TextureMapKind.Index, "_index"), (TextureMapKind.Index, "_index"),
@@ -133,10 +125,10 @@ public sealed class TextureMetadataHelper
_dataManager = dataManager; _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); => RecommendationCatalog.TryGetValue(target, out info);
public TextureUsageCategory DetermineCategory(string? gamePath) public static TextureUsageCategory DetermineCategory(string? gamePath)
{ {
var normalized = Normalize(gamePath); var normalized = Normalize(gamePath);
if (string.IsNullOrEmpty(normalized)) if (string.IsNullOrEmpty(normalized))
@@ -193,7 +185,7 @@ public sealed class TextureMetadataHelper
return TextureUsageCategory.Unknown; return TextureUsageCategory.Unknown;
} }
public string DetermineSlot(TextureUsageCategory category, string? gamePath) public static string DetermineSlot(TextureUsageCategory category, string? gamePath)
{ {
if (category == TextureUsageCategory.Customization) if (category == TextureUsageCategory.Customization)
return GuessCustomizationSlot(gamePath); return GuessCustomizationSlot(gamePath);
@@ -218,7 +210,7 @@ public sealed class TextureMetadataHelper
TextureUsageCategory.Companion => "Companion", TextureUsageCategory.Companion => "Companion",
TextureUsageCategory.VisualEffect => "VFX", TextureUsageCategory.VisualEffect => "VFX",
TextureUsageCategory.Housing => "Housing", TextureUsageCategory.Housing => "Housing",
TextureUsageCategory.Ui => "UI", TextureUsageCategory.UI => "UI",
_ => "General" _ => "General"
}; };
} }
@@ -260,7 +252,7 @@ public sealed class TextureMetadataHelper
return false; return false;
} }
private void AddGameMaterialCandidates(string? gamePath, IList<MaterialCandidate> candidates) private static void AddGameMaterialCandidates(string? gamePath, IList<MaterialCandidate> candidates)
{ {
var normalized = Normalize(gamePath); var normalized = Normalize(gamePath);
if (string.IsNullOrEmpty(normalized)) 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)) if (string.IsNullOrEmpty(localTexturePath))
return; return;
@@ -397,7 +389,7 @@ public sealed class TextureMetadataHelper
return TextureMapKind.Unknown; 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(); var normalized = (format ?? string.Empty).ToUpperInvariant();
if (normalized.Contains("BC1", StringComparison.Ordinal)) if (normalized.Contains("BC1", StringComparison.Ordinal))
@@ -434,7 +426,7 @@ public sealed class TextureMetadataHelper
return false; 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; TextureCompressionTarget? current = null;
if (TryMapFormatToTarget(format, out var mapped)) if (TryMapFormatToTarget(format, out var mapped))
@@ -446,7 +438,6 @@ public sealed class TextureMetadataHelper
TextureMapKind.Mask => TextureCompressionTarget.BC4, TextureMapKind.Mask => TextureCompressionTarget.BC4,
TextureMapKind.Index => TextureCompressionTarget.BC3, TextureMapKind.Index => TextureCompressionTarget.BC3,
TextureMapKind.Specular => TextureCompressionTarget.BC4, TextureMapKind.Specular => TextureCompressionTarget.BC4,
TextureMapKind.Emissive => TextureCompressionTarget.BC3,
TextureMapKind.Diffuse => TextureCompressionTarget.BC7, TextureMapKind.Diffuse => TextureCompressionTarget.BC7,
_ => TextureCompressionTarget.BC7 _ => TextureCompressionTarget.BC7
}; };

View File

@@ -10,7 +10,7 @@ public enum TextureUsageCategory
Companion, Companion,
Monster, Monster,
Housing, Housing,
Ui, UI,
VisualEffect, VisualEffect,
Unknown Unknown
} }

View File

@@ -2,7 +2,6 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Utility;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services; using LightlessSync.Services;

View File

@@ -2,8 +2,6 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
@@ -24,11 +22,9 @@ using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.Files.Models;
using LightlessSync.WebAPI.SignalR.Utils; using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;

View File

@@ -12,16 +12,14 @@ using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.TextureCompression; using LightlessSync.Services.TextureCompression;
using LightlessSync.Utils; using LightlessSync.Utils;
using Penumbra.Api.Enums;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using OtterTex;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading; using SixLabors.ImageSharp;
using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using ImageSharpImage = SixLabors.ImageSharp.Image;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -810,11 +808,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
var primaryGamePath = entry.GamePaths.FirstOrDefault() ?? string.Empty; var primaryGamePath = entry.GamePaths.FirstOrDefault() ?? string.Empty;
var classificationPath = string.IsNullOrEmpty(primaryGamePath) ? primaryFile : primaryGamePath; var classificationPath = string.IsNullOrEmpty(primaryGamePath) ? primaryFile : primaryGamePath;
var mapKind = _textureMetadataHelper.DetermineMapKind(primaryGamePath, primaryFile); var mapKind = _textureMetadataHelper.DetermineMapKind(primaryGamePath, primaryFile);
var category = _textureMetadataHelper.DetermineCategory(classificationPath); var category = TextureMetadataHelper.DetermineCategory(classificationPath);
var slot = _textureMetadataHelper.DetermineSlot(category, classificationPath); var slot = TextureMetadataHelper.DetermineSlot(category, classificationPath);
var format = entry.Format.Value; var format = entry.Format.Value;
var suggestion = _textureMetadataHelper.GetSuggestedTarget(format, mapKind); var suggestion = TextureMetadataHelper.GetSuggestedTarget(format, mapKind);
TextureCompressionTarget? currentTarget = _textureMetadataHelper.TryMapFormatToTarget(format, out var mappedTarget) TextureCompressionTarget? currentTarget = TextureMetadataHelper.TryMapFormatToTarget(format, out var mappedTarget)
? mappedTarget ? mappedTarget
: null; : null;
var displayName = Path.GetFileName(primaryFile); var displayName = Path.GetFileName(primaryFile);
@@ -2014,23 +2012,43 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private async Task<IDalamudTextureWrap?> BuildPreviewAsync(TextureRow row, CancellationToken token) 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; return null;
} }
var tempFile = Path.Combine(Path.GetTempPath(), $"lightless_preview_{Guid.NewGuid():N}.png");
try try
{ {
var job = new TextureConversionJob(row.PrimaryFilePath, tempFile, TextureType.Png, IncludeMipMaps: false); using var scratch = TexFileHelper.Load(row.PrimaryFilePath);
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { job }, null, token).ConfigureAwait(false); using var rgbaScratch = scratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);
if (!File.Exists(tempFile))
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); using var ms = new MemoryStream();
return _uiSharedService.LoadImage(data); await image.SaveAsPngAsync(ms, cancellationToken: token).ConfigureAwait(false);
return _uiSharedService.LoadImage(ms.ToArray());
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -2041,20 +2059,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_logger.LogDebug(ex, "Preview generation failed for {File}", row.PrimaryFilePath); _logger.LogDebug(ex, "Preview generation failed for {File}", row.PrimaryFilePath);
return null; 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) private void ResetPreview(string key)
@@ -2291,7 +2295,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{ {
_textureSelections[row.Key] = selectedTarget; _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)) using (ImRaii.Child("textureDetailInfo", new Vector2(-1, 0), true, ImGuiWindowFlags.AlwaysVerticalScrollbar))
{ {
@@ -2425,7 +2429,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
if (row.SuggestedTarget.HasValue) if (row.SuggestedTarget.HasValue)
{ {
var recommendedTarget = row.SuggestedTarget.Value; 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 recommendedTitle = hasRecommendationInfo ? recommendedInfo!.Title : recommendedTarget.ToString();
var recommendedDescription = hasRecommendationInfo var recommendedDescription = hasRecommendationInfo
? recommendedInfo!.Description ? recommendedInfo!.Description
@@ -2634,4 +2638,4 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
} }
} }
} }
} }

View File

@@ -490,7 +490,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
private const byte _colorTypeForeground = 0x13; private const byte _colorTypeForeground = 0x13;
private const byte _colorTypeGlow = 0x14; private const byte _colorTypeGlow = 0x14;
private static Colors SwapColorChannels(Colors colors) internal static Colors SwapColorChannels(Colors colors)
=> new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow)); => new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow));
private static uint SwapColorComponent(uint color) private static uint SwapColorComponent(uint color)

View File

@@ -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,
}
}

View File

@@ -28,23 +28,16 @@ using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.Files.Models;
using LightlessSync.WebAPI.SignalR.Utils; using LightlessSync.WebAPI.SignalR.Utils;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using System.Text.Json; 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; namespace LightlessSync.UI;
@@ -2246,7 +2239,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
var nameColors = _configService.Current.NameplateColors; var nameColors = _configService.Current.NameplateColors;
var isFriendOverride = _configService.Current.overrideFriendColor; var isFriendOverride = _configService.Current.overrideFriendColor;
var isPartyOverride = _configService.Current.overridePartyColor; var isPartyOverride = _configService.Current.overridePartyColor;
var isFcTagOverride = _configService.Current.overrideFcTagColor;
if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled))
{ {
@@ -2280,13 +2272,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Save(); _configService.Save();
_nameplateService.RequestRedraw(); _nameplateService.RequestRedraw();
} }
if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride))
{
_configService.Current.overrideFcTagColor = isFcTagOverride;
_configService.Save();
_nameplateService.RequestRedraw();
}
} }
ImGui.Spacing(); ImGui.Spacing();

View File

@@ -1,5 +1,4 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using LightlessSync.API.Data; using LightlessSync.API.Data;
@@ -13,9 +12,6 @@ using LightlessSync.UI.Services;
using LightlessSync.UI.Tags; using LightlessSync.UI.Tags;
using LightlessSync.Utils; using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
namespace LightlessSync.UI; namespace LightlessSync.UI;

View File

@@ -5,22 +5,17 @@ using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.Profiles; using LightlessSync.PlayerData.Pairs;
using LightlessSync.UI.Handlers;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using System.Globalization; using System.Globalization;
using System.Numerics;
namespace LightlessSync.UI; namespace LightlessSync.UI;

View File

@@ -14,9 +14,7 @@ using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Specialized;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
namespace LightlessSync.UI; namespace LightlessSync.UI;

View File

@@ -4,8 +4,6 @@ using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using LightlessSync.Utils; using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
namespace LightlessSync.UI.Tags; namespace LightlessSync.UI.Tags;

View File

@@ -1,6 +1,3 @@
using LightlessSync.UI;
using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
namespace LightlessSync.UI.Tags; namespace LightlessSync.UI.Tags;
@@ -35,16 +32,16 @@ public sealed class ProfileTagService
private static IReadOnlyDictionary<int, ProfileTagDefinition> CreateTagLibrary() 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, 230419,
"SFW", "SFW",
background: new Vector4(0.16f, 0.24f, 0.18f, 0.95f), background: new Vector4(0.16f, 0.24f, 0.18f, 0.95f),
border: new Vector4(0.32f, 0.52f, 0.34f, 0.85f), border: new Vector4(0.32f, 0.52f, 0.34f, 0.85f),
textColor: new Vector4(0.78f, 0.94f, 0.80f, 1f)), textColor: new Vector4(0.78f, 0.94f, 0.80f, 1f)),
[(int)ProfileTags.NSFW] = ProfileTagDefinition.FromIconAndText( [1] = ProfileTagDefinition.FromIconAndText(
230419, 230419,
"NSFW", "NSFW",
background: new Vector4(0.32f, 0.18f, 0.22f, 0.95f), 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)), textColor: new Vector4(1f, 0.82f, 0.86f, 1f)),
[(int)ProfileTags.RP] = ProfileTagDefinition.FromIconAndText( [2] = ProfileTagDefinition.FromIconAndText(
61545, 61545,
"RP", "RP",
background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f), background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f),
border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f), border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f),
textColor: new Vector4(0.80f, 0.84f, 1f, 1f)), textColor: new Vector4(0.80f, 0.84f, 1f, 1f)),
[(int)ProfileTags.ERP] = ProfileTagDefinition.FromIconAndText( [3] = ProfileTagDefinition.FromIconAndText(
61545, 61545,
"ERP", "ERP",
background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f), background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f),
border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f), border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f),
textColor: new Vector4(0.80f, 0.84f, 1f, 1f)), textColor: new Vector4(0.80f, 0.84f, 1f, 1f)),
[(int)ProfileTags.No_RP] = ProfileTagDefinition.FromIconAndText( [4] = ProfileTagDefinition.FromIconAndText(
230420, 230420,
"No RP", "No RP",
background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f), background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f),
border: new Vector4(0.69f, 0.40f, 0.65f, 0.85f), border: new Vector4(0.69f, 0.40f, 0.65f, 0.85f),
textColor: new Vector4(1f, 0.84f, 1f, 1f)), textColor: new Vector4(1f, 0.84f, 1f, 1f)),
[(int)ProfileTags.No_ERP] = ProfileTagDefinition.FromIconAndText( [5] = ProfileTagDefinition.FromIconAndText(
230420, 230420,
"No ERP", "No ERP",
background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f), 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)), textColor: new Vector4(1f, 0.84f, 1f, 1f)),
[(int)ProfileTags.Venues] = ProfileTagDefinition.FromIconAndText( [6] = ProfileTagDefinition.FromIconAndText(
60756, 60756,
"Venues", "Venues",
background: new Vector4(0.18f, 0.24f, 0.28f, 0.95f), background: new Vector4(0.18f, 0.24f, 0.28f, 0.95f),
border: new Vector4(0.33f, 0.55f, 0.63f, 0.85f), border: new Vector4(0.33f, 0.55f, 0.63f, 0.85f),
textColor: new Vector4(0.78f, 0.90f, 0.97f, 1f)), textColor: new Vector4(0.78f, 0.90f, 0.97f, 1f)),
[(int)ProfileTags.Gpose] = ProfileTagDefinition.FromIconAndText( [7] = ProfileTagDefinition.FromIconAndText(
61546, 61546,
"GPose", "GPose",
background: new Vector4(0.18f, 0.18f, 0.26f, 0.95f), 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)), textColor: new Vector4(0.80f, 0.82f, 0.96f, 1f)),
[(int)ProfileTags.Limsa] = ProfileTagDefinition.FromIconAndText( [8] = ProfileTagDefinition.FromIconAndText(
60572, 60572,
"Limsa"), "Limsa"),
[(int)ProfileTags.Gridania] = ProfileTagDefinition.FromIconAndText( [9] = ProfileTagDefinition.FromIconAndText(
60573, 60573,
"Gridania"), "Gridania"),
[(int)ProfileTags.Ul_dah] = ProfileTagDefinition.FromIconAndText( [10] = ProfileTagDefinition.FromIconAndText(
60574, 60574,
"Ul'dah"), "Ul'dah"),
[(int)ProfileTags.WUT] = ProfileTagDefinition.FromIconAndText( [11] = ProfileTagDefinition.FromIconAndText(
61397, 61397,
"WU/T"), "WU/T"),
[(int)ProfileTags.PVP] = ProfileTagDefinition.FromIcon(61806), [1001] = ProfileTagDefinition.FromIcon(61806), // PVP
[(int)ProfileTags.Ultimate] = ProfileTagDefinition.FromIcon(61832), [1002] = ProfileTagDefinition.FromIcon(61832), // Ultimate
[(int)ProfileTags.Raids] = ProfileTagDefinition.FromIcon(61802), [1003] = ProfileTagDefinition.FromIcon(61802), // Raids
[(int)ProfileTags.Roulette] = ProfileTagDefinition.FromIcon(61807), [1004] = ProfileTagDefinition.FromIcon(61807), // Roulette
[(int)ProfileTags.Crafting] = ProfileTagDefinition.FromIcon(61816), [1005] = ProfileTagDefinition.FromIcon(61816), // Crafting
[(int)ProfileTags.Casual] = ProfileTagDefinition.FromIcon(61753), [1006] = ProfileTagDefinition.FromIcon(61753), // Casual
[(int)ProfileTags.Hardcore] = ProfileTagDefinition.FromIcon(61754), [1007] = ProfileTagDefinition.FromIcon(61754), // Hardcore
[(int)ProfileTags.Glamour] = ProfileTagDefinition.FromIcon(61759), [1008] = ProfileTagDefinition.FromIcon(61759), // Glamour
[(int)ProfileTags.Mentor] = ProfileTagDefinition.FromIcon(61760) [1009] = ProfileTagDefinition.FromIcon(61760) // Mentor
}; };
return dictionary;
} }
} }

View File

@@ -179,9 +179,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
int i = 0; int i = 0;
double dblSByte = bytes; double dblSByte = bytes;
while (dblSByte >= 1000 && i < suffix.Length - 1) while (dblSByte >= 1024 && i < suffix.Length - 1)
{ {
dblSByte /= 1000.0; dblSByte /= 1024.0;
i++; i++;
} }

View File

@@ -1,4 +1,3 @@
using Dalamud.Utility;
using K4os.Compression.LZ4.Legacy; using K4os.Compression.LZ4.Legacy;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Dto.Files; using LightlessSync.API.Dto.Files;
@@ -10,13 +9,9 @@ using LightlessSync.Services.Mediator;
using LightlessSync.Services.TextureCompression; using LightlessSync.Services.TextureCompression;
using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.Files.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
namespace LightlessSync.WebAPI.Files; namespace LightlessSync.WebAPI.Files;

View File

@@ -4,7 +4,6 @@ using LightlessSync.WebAPI.Files.Models;
using LightlessSync.WebAPI.SignalR; using LightlessSync.WebAPI.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Net.Sockets; using System.Net.Sockets;

View File

@@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading;
namespace LightlessSync.WebAPI.Files; namespace LightlessSync.WebAPI.Files;