533 lines
20 KiB
C#
533 lines
20 KiB
C#
using Dalamud.Plugin;
|
|
using LightlessSync.LightlessConfiguration.Models;
|
|
using LightlessSync.PlayerData.Handlers;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.ActorTracking;
|
|
using LightlessSync.Services.Mediator;
|
|
using Microsoft.Extensions.Logging;
|
|
using Penumbra.Api.Enums;
|
|
using Penumbra.Api.Helpers;
|
|
using Penumbra.Api.IpcSubscribers;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LightlessSync.Interop.Ipc;
|
|
|
|
public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller
|
|
{
|
|
private readonly IDalamudPluginInterface _pi;
|
|
private readonly DalamudUtilService _dalamudUtil;
|
|
private readonly LightlessMediator _lightlessMediator;
|
|
private readonly RedrawManager _redrawManager;
|
|
private readonly ActorObjectService _actorObjectService;
|
|
private bool _shownPenumbraUnavailable = false;
|
|
private string? _penumbraModDirectory;
|
|
public string? ModDirectory
|
|
{
|
|
get => _penumbraModDirectory;
|
|
private set
|
|
{
|
|
if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal))
|
|
{
|
|
_penumbraModDirectory = value;
|
|
_lightlessMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory));
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly ConcurrentDictionary<IntPtr, bool> _penumbraRedrawRequests = new();
|
|
private readonly ConcurrentDictionary<IntPtr, byte> _trackedActors = new();
|
|
|
|
private readonly EventSubscriber _penumbraDispose;
|
|
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
|
|
private readonly EventSubscriber _penumbraInit;
|
|
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
|
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
|
|
|
|
private readonly AddTemporaryMod _penumbraAddTemporaryMod;
|
|
private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection;
|
|
private readonly ConvertTextureFile _penumbraConvertTextureFile;
|
|
private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection;
|
|
private readonly GetEnabledState _penumbraEnabled;
|
|
private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations;
|
|
private readonly RedrawObject _penumbraRedraw;
|
|
private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection;
|
|
private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod;
|
|
private readonly GetModDirectory _penumbraResolveModDir;
|
|
private readonly ResolvePlayerPathsAsync _penumbraResolvePaths;
|
|
private readonly GetGameObjectResourcePaths _penumbraResourcePaths;
|
|
//private readonly GetPlayerResourcePaths _penumbraPlayerResourcePaths;
|
|
private readonly GetCollections _penumbraGetCollections;
|
|
private readonly ConcurrentDictionary<Guid, string> _activeTemporaryCollections = new();
|
|
private int _performedInitialCleanup;
|
|
|
|
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
|
LightlessMediator lightlessMediator, RedrawManager redrawManager, ActorObjectService actorObjectService) : base(logger, lightlessMediator)
|
|
{
|
|
_pi = pi;
|
|
_dalamudUtil = dalamudUtil;
|
|
_lightlessMediator = lightlessMediator;
|
|
_redrawManager = redrawManager;
|
|
_actorObjectService = actorObjectService;
|
|
_penumbraInit = Initialized.Subscriber(pi, PenumbraInit);
|
|
_penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose);
|
|
_penumbraResolveModDir = new GetModDirectory(pi);
|
|
_penumbraRedraw = new RedrawObject(pi);
|
|
_penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent);
|
|
_penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi);
|
|
_penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi);
|
|
_penumbraAddTemporaryMod = new AddTemporaryMod(pi);
|
|
_penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi);
|
|
_penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi);
|
|
_penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi);
|
|
_penumbraGetCollections = new GetCollections(pi);
|
|
_penumbraResolvePaths = new ResolvePlayerPathsAsync(pi);
|
|
_penumbraEnabled = new GetEnabledState(pi);
|
|
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) =>
|
|
{
|
|
if (change == ModSettingChange.EnableState)
|
|
_lightlessMediator.Publish(new PenumbraModSettingChangedMessage());
|
|
});
|
|
_penumbraConvertTextureFile = new ConvertTextureFile(pi);
|
|
_penumbraResourcePaths = new GetGameObjectResourcePaths(pi);
|
|
//_penumbraPlayerResourcePaths = new GetPlayerResourcePaths(pi);
|
|
|
|
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded);
|
|
|
|
CheckAPI();
|
|
CheckModDirectory();
|
|
|
|
Mediator.Subscribe<PenumbraRedrawCharacterMessage>(this, (msg) =>
|
|
{
|
|
_penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
|
});
|
|
|
|
Mediator.Subscribe<DalamudLoginMessage>(this, (msg) => _shownPenumbraUnavailable = false);
|
|
|
|
Mediator.Subscribe<ActorTrackedMessage>(this, msg =>
|
|
{
|
|
if (msg.Descriptor.Address != nint.Zero)
|
|
{
|
|
_trackedActors[(IntPtr)msg.Descriptor.Address] = 0;
|
|
}
|
|
});
|
|
|
|
Mediator.Subscribe<ActorUntrackedMessage>(this, msg =>
|
|
{
|
|
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 void CheckAPI()
|
|
{
|
|
bool penumbraAvailable = false;
|
|
try
|
|
{
|
|
var penumbraVersion = (_pi.InstalledPlugins
|
|
.FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase))
|
|
?.Version ?? new Version(0, 0, 0, 0));
|
|
penumbraAvailable = penumbraVersion >= new Version(1, 2, 0, 22);
|
|
try
|
|
{
|
|
penumbraAvailable &= _penumbraEnabled.Invoke();
|
|
}
|
|
catch
|
|
{
|
|
penumbraAvailable = false;
|
|
}
|
|
_shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable;
|
|
APIAvailable = penumbraAvailable;
|
|
}
|
|
catch
|
|
{
|
|
APIAvailable = penumbraAvailable;
|
|
}
|
|
finally
|
|
{
|
|
if (!penumbraAvailable && !_shownPenumbraUnavailable)
|
|
{
|
|
_shownPenumbraUnavailable = true;
|
|
_lightlessMediator.Publish(new NotificationMessage("Penumbra inactive",
|
|
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.",
|
|
NotificationType.Error));
|
|
}
|
|
}
|
|
|
|
if (APIAvailable)
|
|
{
|
|
ScheduleTemporaryCollectionCleanup();
|
|
}
|
|
}
|
|
|
|
public void CheckModDirectory()
|
|
{
|
|
if (!APIAvailable)
|
|
{
|
|
ModDirectory = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant();
|
|
}
|
|
}
|
|
|
|
private void ScheduleTemporaryCollectionCleanup()
|
|
{
|
|
if (Interlocked.Exchange(ref _performedInitialCleanup, 1) != 0)
|
|
return;
|
|
|
|
_ = Task.Run(CleanupTemporaryCollectionsAsync);
|
|
}
|
|
|
|
private async Task CleanupTemporaryCollectionsAsync()
|
|
{
|
|
if (!APIAvailable)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var collections = await _dalamudUtil.RunOnFrameworkThread(() => _penumbraGetCollections.Invoke()).ConfigureAwait(false);
|
|
foreach (var (collectionId, name) in collections)
|
|
{
|
|
if (!IsLightlessCollectionName(name))
|
|
continue;
|
|
|
|
if (_activeTemporaryCollections.ContainsKey(collectionId))
|
|
continue;
|
|
|
|
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
|
var deleteResult = await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
{
|
|
var result = (PenumbraApiEc)_penumbraRemoveTemporaryCollection.Invoke(collectionId);
|
|
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
|
return result;
|
|
}).ConfigureAwait(false);
|
|
if (deleteResult == PenumbraApiEc.Success)
|
|
{
|
|
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult);
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
|
|
_redrawManager.Cancel();
|
|
|
|
_penumbraModSettingChanged.Dispose();
|
|
_penumbraGameObjectResourcePathResolved.Dispose();
|
|
_penumbraDispose.Dispose();
|
|
_penumbraInit.Dispose();
|
|
_penumbraObjectIsRedrawn.Dispose();
|
|
}
|
|
|
|
public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx)
|
|
{
|
|
if (!APIAvailable) return;
|
|
|
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
{
|
|
var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true);
|
|
logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign);
|
|
return collName;
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
|
{
|
|
if (!APIAvailable || jobs.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lightlessMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles)));
|
|
|
|
var totalJobs = jobs.Count;
|
|
var completedJobs = 0;
|
|
|
|
try
|
|
{
|
|
foreach (var job in jobs)
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
break;
|
|
}
|
|
|
|
progress?.Report(new TextureConversionProgress(completedJobs, totalJobs, job));
|
|
|
|
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
|
var convertTask = _penumbraConvertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
|
await convertTask.ConfigureAwait(false);
|
|
|
|
if (convertTask.IsCompletedSuccessfully && job.DuplicateTargets is { Count: > 0 })
|
|
{
|
|
foreach (var duplicate in job.DuplicateTargets)
|
|
{
|
|
logger.LogInformation("Synchronizing duplicate {Duplicate}", duplicate);
|
|
try
|
|
{
|
|
File.Copy(job.OutputFile, duplicate, overwrite: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Failed to copy duplicate {Duplicate}", duplicate);
|
|
}
|
|
}
|
|
}
|
|
|
|
completedJobs++;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_lightlessMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles)));
|
|
}
|
|
|
|
if (completedJobs > 0 && !token.IsCancellationRequested)
|
|
{
|
|
await _dalamudUtil.RunOnFrameworkThread(async () =>
|
|
{
|
|
var player = await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false);
|
|
if (player == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var gameObject = await _dalamudUtil.CreateGameObjectAsync(player).ConfigureAwait(false);
|
|
_penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw);
|
|
}).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
public async Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
|
{
|
|
if (!APIAvailable) return Guid.Empty;
|
|
|
|
var (collectionId, collectionName) = await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
{
|
|
var collName = "Lightless_" + uid;
|
|
_penumbraCreateNamedTemporaryCollection.Invoke(collName, collName, out var collId);
|
|
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
|
|
return (collId, collName);
|
|
|
|
}).ConfigureAwait(false);
|
|
if (collectionId != Guid.Empty)
|
|
{
|
|
_activeTemporaryCollections[collectionId] = collectionName;
|
|
}
|
|
|
|
return collectionId;
|
|
}
|
|
|
|
public async Task<Dictionary<string, HashSet<string>>?> GetCharacterData(ILogger logger, GameObjectHandler handler)
|
|
{
|
|
if (!APIAvailable) return null;
|
|
|
|
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
{
|
|
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
|
var idx = handler.GetGameObject()?.ObjectIndex;
|
|
if (idx == null) return null;
|
|
return _penumbraResourcePaths.Invoke(idx.Value)[0];
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
public string GetMetaManipulations()
|
|
{
|
|
if (!APIAvailable) return string.Empty;
|
|
return _penumbraGetMetaManipulations.Invoke();
|
|
}
|
|
|
|
public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
|
{
|
|
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
|
try
|
|
{
|
|
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
|
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
|
{
|
|
logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId);
|
|
_penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw);
|
|
|
|
}, token).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
_redrawManager.RedrawSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId)
|
|
{
|
|
if (!APIAvailable) return;
|
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
{
|
|
logger.LogTrace("[{applicationId}] Removing temp collection for {collId}", applicationId, collId);
|
|
var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collId);
|
|
logger.LogTrace("[{applicationId}] RemoveTemporaryCollection: {ret2}", applicationId, ret2);
|
|
}).ConfigureAwait(false);
|
|
if (collId != Guid.Empty)
|
|
{
|
|
_activeTemporaryCollections.TryRemove(collId, out _);
|
|
}
|
|
}
|
|
|
|
public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
|
{
|
|
return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
|
{
|
|
if (!APIAvailable) return;
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
await _penumbraConvertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps)
|
|
.ConfigureAwait(false);
|
|
|
|
if (job.DuplicateTargets is { Count: > 0 })
|
|
{
|
|
foreach (var duplicate in job.DuplicateTargets)
|
|
{
|
|
try
|
|
{
|
|
File.Copy(job.OutputFile, duplicate, overwrite: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogDebug(ex, "Failed to copy duplicate {Duplicate} for texture conversion", duplicate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collId, string manipulationData)
|
|
{
|
|
if (!APIAvailable) return;
|
|
|
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
{
|
|
logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData);
|
|
var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Meta", collId, [], manipulationData, 0);
|
|
logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary<string, string> modPaths)
|
|
{
|
|
if (!APIAvailable) return;
|
|
|
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
{
|
|
foreach (var mod in modPaths)
|
|
{
|
|
logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value);
|
|
}
|
|
var retRemove = _penumbraRemoveTemporaryMod.Invoke("LightlessChara_Files", collId, 0);
|
|
logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove);
|
|
var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Files", collId, modPaths, string.Empty, 0);
|
|
logger.LogTrace("[{applicationId}] Setting temp files mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
|
{
|
|
bool wasRequested = false;
|
|
if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest)
|
|
{
|
|
_penumbraRedrawRequests[objectAddress] = false;
|
|
}
|
|
else
|
|
{
|
|
_lightlessMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested));
|
|
}
|
|
}
|
|
|
|
private void ResourceLoaded(IntPtr ptr, string arg1, string arg2)
|
|
{
|
|
if (ptr == IntPtr.Zero)
|
|
return;
|
|
|
|
if (!_trackedActors.ContainsKey(ptr))
|
|
{
|
|
var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr);
|
|
if (descriptor.Address != nint.Zero)
|
|
{
|
|
_trackedActors[ptr] = 0;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) == 0)
|
|
return;
|
|
|
|
_lightlessMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2));
|
|
}
|
|
|
|
private void PenumbraDispose()
|
|
{
|
|
_redrawManager.Cancel();
|
|
_lightlessMediator.Publish(new PenumbraDisposedMessage());
|
|
}
|
|
|
|
private void PenumbraInit()
|
|
{
|
|
APIAvailable = true;
|
|
ModDirectory = _penumbraResolveModDir.Invoke();
|
|
_lightlessMediator.Publish(new PenumbraInitializedMessage());
|
|
ScheduleTemporaryCollectionCleanup();
|
|
_penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw);
|
|
}
|
|
}
|