using Dalamud.Plugin; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; using System.Collections.Concurrent; namespace MareSynchronos.Interop.Ipc; public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller { private readonly DalamudUtilService _dalamudUtil; private readonly MareMediator _mareMediator; private readonly RedrawManager _redrawManager; private bool _shownPenumbraUnavailable = false; private string? _penumbraModDirectory; public string? ModDirectory { get => _penumbraModDirectory; private set { if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal)) { _penumbraModDirectory = value; _mareMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory)); } } } private readonly ConcurrentDictionary _penumbraRedrawRequests = new(); private readonly EventSubscriber _penumbraDispose; private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; private readonly EventSubscriber _penumbraInit; private readonly EventSubscriber _penumbraModSettingChanged; private readonly EventSubscriber _penumbraObjectIsRedrawn; private readonly AddTemporaryMod _penumbraAddTemporaryMod; private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection; private readonly ConvertTextureFile _penumbraConvertTextureFile; private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection; private readonly GetEnabledState _penumbraEnabled; private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations; private readonly RedrawObject _penumbraRedraw; private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection; private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod; private readonly GetModDirectory _penumbraResolveModDir; private readonly ResolvePlayerPathsAsync _penumbraResolvePaths; private readonly GetGameObjectResourcePaths _penumbraResourcePaths; private bool _pluginLoaded; private Version _pluginVersion; public IpcCallerPenumbra(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator, RedrawManager redrawManager) : base(logger, mareMediator) { _dalamudUtil = dalamudUtil; _mareMediator = mareMediator; _redrawManager = redrawManager; _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); _penumbraResolvePaths = new ResolvePlayerPathsAsync(pi); _penumbraEnabled = new GetEnabledState(pi); _penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) => { if (change == ModSettingChange.EnableState) _mareMediator.Publish(new PenumbraModSettingChangedMessage()); }); _penumbraConvertTextureFile = new ConvertTextureFile(pi); _penumbraResourcePaths = new GetGameObjectResourcePaths(pi); _penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); var plugin = PluginWatcherService.GetInitialPluginState(pi, "Penumbra"); _pluginLoaded = plugin?.IsLoaded ?? false; _pluginVersion = plugin?.Version ?? new(0, 0, 0, 0); Mediator.SubscribeKeyed(this, "Penumbra", (msg) => { _pluginLoaded = msg.IsLoaded; _pluginVersion = msg.Version; CheckAPI(); }); CheckAPI(); CheckModDirectory(); Mediator.Subscribe(this, (msg) => { _penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose); }); Mediator.Subscribe(this, (msg) => _shownPenumbraUnavailable = false); } public bool APIAvailable { get; private set; } = false; public void CheckAPI() { bool penumbraAvailable = false; try { penumbraAvailable = _pluginLoaded && _pluginVersion >= new Version(1, 0, 1, 0); try { penumbraAvailable &= _penumbraEnabled.Invoke(); } catch { penumbraAvailable = false; } _shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable; APIAvailable = penumbraAvailable; } catch { APIAvailable = penumbraAvailable; } finally { if (!penumbraAvailable && !_shownPenumbraUnavailable) { _shownPenumbraUnavailable = true; _mareMediator.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 Snowcloak. If you just updated Penumbra, ignore this message.", NotificationType.Error)); } } } public void CheckModDirectory() { if (!APIAvailable) { ModDirectory = string.Empty; } else { ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant(); } } 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, Dictionary textures, IProgress<(string, int)> progress, CancellationToken token) { if (!APIAvailable) return; _mareMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles))); int currentTexture = 0; foreach (var texture in textures) { if (token.IsCancellationRequested) break; progress.Report((texture.Key, ++currentTexture)); logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex); var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, mipMaps: true); await convertTask.ConfigureAwait(false); if (convertTask.IsCompletedSuccessfully && texture.Value.Any()) { foreach (var duplicatedTexture in texture.Value) { logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture); try { File.Copy(texture.Key, duplicatedTexture, overwrite: true); } catch (Exception ex) { logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture); } } } } _mareMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles))); await _dalamudUtil.RunOnFrameworkThread(async () => { var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false); _penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw); }).ConfigureAwait(false); } public async Task CreateTemporaryCollectionAsync(ILogger logger, string uid) { if (!APIAvailable) return Guid.Empty; return await _dalamudUtil.RunOnFrameworkThread(() => { Guid collId; var collName = "ElfSync_" + uid; PenumbraApiEc penEC = _penumbraCreateNamedTemporaryCollection.Invoke(uid, collName, out collId); logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId); if (penEC != PenumbraApiEc.Success) { logger.LogError("Failed to create temporary collection for {collName} with error code {penEC}. Please include this line in any error reports", collName, penEC); return Guid.Empty; } return collId; }).ConfigureAwait(false); } public async Task>?> GetCharacterData(ILogger logger, GameObjectHandler handler) { if (!APIAvailable) return null; return await _dalamudUtil.RunOnFrameworkThread(() => { logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); var idx = handler.GetGameObject()?.ObjectIndex; if (idx == null) return null; return _penumbraResourcePaths.Invoke(idx.Value)[0]; }).ConfigureAwait(false); } public string GetMetaManipulations() { if (!APIAvailable) return string.Empty; return _penumbraGetMetaManipulations.Invoke(); } public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) { if (!APIAvailable || _dalamudUtil.IsZoning) return; try { await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false); await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) => { logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId); _penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw); }, token).ConfigureAwait(false); } finally { _redrawManager.RedrawSemaphore.Release(); } } public void RedrawNow(ILogger logger, Guid applicationId, int objectIndex) { if (!APIAvailable || _dalamudUtil.IsZoning) return; logger.LogTrace("[{applicationId}] Immediately redrawing object index {objId}", applicationId, objectIndex); _penumbraRedraw.Invoke(objectIndex); } 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); } public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse) { return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false); } 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("MareChara_Meta", collId, [], manipulationData, 0); logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd); }).ConfigureAwait(false); } public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary modPaths) { if (!APIAvailable) return; await _dalamudUtil.RunOnFrameworkThread(() => { foreach (var mod in modPaths) { logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value); } var retRemove = _penumbraRemoveTemporaryMod.Invoke("MareChara_Files", collId, 0); logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove); var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_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 { _mareMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested)); } } private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) { if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0) { _mareMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2)); } } private void PenumbraDispose() { _redrawManager.Cancel(); _mareMediator.Publish(new PenumbraDisposedMessage()); } private void PenumbraInit() { APIAvailable = true; ModDirectory = _penumbraResolveModDir.Invoke(); _mareMediator.Publish(new PenumbraInitializedMessage()); _penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw); } }