From 04b76ddee1e36664cb2ba118a520e401b2ed06f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Aug 2023 17:25:06 +0200 Subject: [PATCH] Add support for the DalamudSubstitutionProvider for textures. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 127 +++++++++++++++++- Penumbra/Collections/Cache/CollectionCache.cs | 62 ++++++--- .../Cache/CollectionCacheManager.cs | 39 +++--- Penumbra/Communication/CollectionChange.cs | 4 + Penumbra/Communication/EnabledChanged.cs | 3 + Penumbra/Communication/ResolvedFileChanged.cs | 43 ++++++ Penumbra/Configuration.cs | 7 +- Penumbra/Services/CommunicatorService.cs | 4 + Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 59 ++++---- 10 files changed, 285 insertions(+), 66 deletions(-) create mode 100644 Penumbra/Communication/ResolvedFileChanged.cs diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index faa6710a..42abd1cd 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -1,7 +1,14 @@ using System; +using System.Collections.Generic; +using System.Linq; using Dalamud.Plugin.Services; +using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Mods; +using Penumbra.Services; using Penumbra.String.Classes; +using static Penumbra.Api.Ipc; namespace Penumbra.Api; @@ -9,19 +16,109 @@ public class DalamudSubstitutionProvider : IDisposable { private readonly ITextureSubstitutionProvider _substitution; private readonly ActiveCollectionData _activeCollectionData; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; - public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData) + public bool Enabled + => _config.UseDalamudUiTextureRedirection; + + public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData, + Configuration config, CommunicatorService communicator) { - _substitution = substitution; - _activeCollectionData = activeCollectionData; - _substitution.InterceptTexDataLoad += Substitute; + _substitution = substitution; + _activeCollectionData = activeCollectionData; + _config = config; + _communicator = communicator; + if (Enabled) + Subscribe(); + } + + public void Set(bool value) + { + if (value) + Enable(); + else + Disable(); + } + + public void ResetSubstitutions(IEnumerable paths) + { + var transformed = paths + .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) + .Select(p => p.ToString()); + _substitution.InvalidatePaths(transformed); + } + + public void Enable() + { + if (Enabled) + return; + + _config.UseDalamudUiTextureRedirection = true; + _config.Save(); + Subscribe(); + } + + public void Disable() + { + if (!Enabled) + return; + + Unsubscribe(); + _config.UseDalamudUiTextureRedirection = false; + _config.Save(); } public void Dispose() - => _substitution.InterceptTexDataLoad -= Substitute; + => Unsubscribe(); + + private void OnCollectionChange(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _) + { + if (type is not CollectionType.Interface) + return; + + var enumerable = oldCollection?.ResolvedFiles.Keys ?? Array.Empty().AsEnumerable(); + enumerable = enumerable.Concat(newCollection?.ResolvedFiles.Keys ?? Array.Empty().AsEnumerable()); + ResetSubstitutions(enumerable); + } + + private void OnResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath key, FullPath _1, FullPath _2, + IMod? _3) + { + if (_activeCollectionData.Interface != collection) + return; + + switch (type) + { + case ResolvedFileChanged.Type.Added: + case ResolvedFileChanged.Type.Removed: + case ResolvedFileChanged.Type.Replaced: + ResetSubstitutions(new[] + { + key, + }); + break; + case ResolvedFileChanged.Type.FullRecomputeStart: + case ResolvedFileChanged.Type.FullRecomputeFinished: + ResetSubstitutions(collection.ResolvedFiles.Keys); + break; + } + } + + private void OnEnabledChange(bool state) + { + if (state) + OnCollectionChange(CollectionType.Interface, null, _activeCollectionData.Interface, string.Empty); + else + OnCollectionChange(CollectionType.Interface, _activeCollectionData.Interface, null, string.Empty); + } private void Substitute(string path, ref string? replacementPath) { + // Do not replace when not enabled. + if (!_config.EnableMods) + return; + // Let other plugins prioritize replacement paths. if (replacementPath != null) return; @@ -43,4 +140,22 @@ public class DalamudSubstitutionProvider : IDisposable // ignored } } -} \ No newline at end of file + + private void Subscribe() + { + _substitution.InterceptTexDataLoad += Substitute; + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.DalamudSubstitutionProvider); + _communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.DalamudSubstitutionProvider); + _communicator.EnabledChanged.Subscribe(OnEnabledChange, EnabledChanged.Priority.DalamudSubstitutionProvider); + OnCollectionChange(CollectionType.Interface, null, _activeCollectionData.Interface, string.Empty); + } + + private void Unsubscribe() + { + _substitution.InterceptTexDataLoad -= Substitute; + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange); + _communicator.EnabledChanged.Unsubscribe(OnEnabledChange); + OnCollectionChange(CollectionType.Interface, _activeCollectionData.Interface, null, string.Empty); + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 3477fdf0..1f712124 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Penumbra.Api.Enums; +using Penumbra.Communication; using Penumbra.String.Classes; using Penumbra.Mods.Manager; @@ -34,7 +35,7 @@ public class CollectionCache : IDisposable public int Calculating = -1; public string AnonymizedName - => _collection.AnonymizedName; + => _collection.AnonymizedName; public IEnumerable> AllConflicts => _conflicts.Values; @@ -63,9 +64,7 @@ public class CollectionCache : IDisposable } public void Dispose() - { - Meta.Dispose(); - } + => Meta.Dispose(); ~CollectionCache() => Meta.Dispose(); @@ -130,8 +129,8 @@ public class CollectionCache : IDisposable => _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath)); public void RemovePath(Utf8GamePath path) - => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty)); - + => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty)); + public void ReloadMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges)); @@ -139,8 +138,8 @@ public class CollectionCache : IDisposable => _manager.AddChange(ChangeData.ModAddition(this, mod, addMetaChanges)); public void RemoveMod(IMod mod, bool addMetaChanges) - => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); - + => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); + /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFileSync(Utf8GamePath path, FullPath fullPath) { @@ -148,9 +147,24 @@ public class CollectionCache : IDisposable return; if (ResolvedFiles.Remove(path, out var modPath)) + { ModData.RemovePath(modPath.Mod, path); - if (fullPath.FullName.Length > 0) + if (fullPath.FullName.Length > 0) + { + ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, + Mod.ForcedFiles); + } + else + { + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null); + } + } + else if (fullPath.FullName.Length > 0) + { ResolvedFiles.Add(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); + } } private void ReloadModSync(IMod mod, bool addMetaChanges) @@ -169,9 +183,14 @@ public class CollectionCache : IDisposable foreach (var path in paths) { - if (ResolvedFiles.Remove(path, out var mp) && mp.Mod != mod) - Penumbra.Log.Warning( - $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); + if (ResolvedFiles.Remove(path, out var mp)) + { + if (mp.Mod != mod) + Penumbra.Log.Warning( + $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); + else + _manager.ResolvedFileChanged.Invoke(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, mp.Path, mp.Mod); + } } foreach (var manipulation in manipulations) @@ -203,7 +222,7 @@ public class CollectionCache : IDisposable } - /// Add all files and possibly manipulations of a given mod according to its settings in this collection. + /// Add all files and possibly manipulations of a given mod according to its settings in this collection. internal void AddModSync(IMod mod, bool addMetaChanges) { if (mod.Index >= 0) @@ -257,6 +276,14 @@ public class CollectionCache : IDisposable foreach (var manip in subMod.Manipulations) AddManipulation(manip, parentMod); } + + /// Invoke only if not in a full recalculation. + private void InvokeResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath key, FullPath value, + FullPath old, IMod? mod) + { + if (Calculating == -1) + _manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod); + } // Add a specific file redirection, handling potential conflicts. // For different mods, higher mod priority takes precedence before option group priority, @@ -271,7 +298,8 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { - ModData.AddPath(mod, path); + ModData.AddPath(mod, path); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } @@ -285,11 +313,13 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); ModData.AddPath(mod, path); + InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod); } } catch (Exception ex) { - Penumbra.Log.Error($"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); + Penumbra.Log.Error( + $"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); } } @@ -491,7 +521,7 @@ public class CollectionCache : IDisposable case 2: Cache.ReloadModSync(Mod, AddMetaChanges); break; - case 3: + case 3: Cache.ForceFileSync(Path, FullPath); break; } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index f2223849..abe5bfca 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -15,17 +15,19 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; public class CollectionCacheManager : IDisposable { - private readonly FrameworkManager _framework; - private readonly CommunicatorService _communicator; - private readonly TempModManager _tempMods; - private readonly ModStorage _modStorage; - private readonly CollectionStorage _storage; - private readonly ActiveCollections _active; + private readonly FrameworkManager _framework; + private readonly CommunicatorService _communicator; + private readonly TempModManager _tempMods; + private readonly ModStorage _modStorage; + private readonly CollectionStorage _storage; + private readonly ActiveCollections _active; + internal readonly ResolvedFileChanged ResolvedFileChanged; internal readonly MetaFileManager MetaFileManager; @@ -39,16 +41,17 @@ public class CollectionCacheManager : IDisposable public IEnumerable Active => _storage.Where(c => c.HasCache); - public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, - TempModManager tempMods, ModStorage modStorage, MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage) + public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage, + MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage) { - _framework = framework; - _communicator = communicator; - _tempMods = tempMods; - _modStorage = modStorage; - MetaFileManager = metaFileManager; - _active = active; - _storage = storage; + _framework = framework; + _communicator = communicator; + _tempMods = tempMods; + _modStorage = modStorage; + MetaFileManager = metaFileManager; + _active = active; + _storage = storage; + ResolvedFileChanged = _communicator.ResolvedFileChanged; if (!_active.Individuals.IsLoaded) _active.Individuals.Loaded += CreateNecessaryCaches; @@ -158,6 +161,9 @@ public class CollectionCacheManager : IDisposable cache.Calculating = Environment.CurrentManagedThreadId; try { + ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty, + FullPath.Empty, + null); cache.ResolvedFiles.Clear(); cache.Meta.Reset(); cache._conflicts.Clear(); @@ -177,6 +183,9 @@ public class CollectionCacheManager : IDisposable collection.IncrementCounter(); MetaFileManager.ApplyDefaultFiles(collection); + ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty, + FullPath.Empty, + null); } finally { diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index 7c4946d2..96dd61ab 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -17,6 +17,9 @@ public sealed class CollectionChange : EventWrapper + DalamudSubstitutionProvider = -3, + /// CollectionCacheManager = -2, @@ -43,6 +46,7 @@ public sealed class CollectionChange : EventWrapper ModFileSystemSelector = 0, + } public CollectionChange() diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index dee5e50f..38c6b387 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -16,6 +16,9 @@ public sealed class EnabledChanged : EventWrapper, EnabledChanged.P { /// Api = int.MinValue, + + /// + DalamudSubstitutionProvider = 0, } public EnabledChanged() diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs new file mode 100644 index 00000000..99cec829 --- /dev/null +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -0,0 +1,43 @@ +using System; +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a redirection in a mod collection cache is manipulated. +/// +/// Parameter is collection with a changed cache. +/// Parameter is the type of change. +/// Parameter is the game path to be redirected or empty for FullRecompute. +/// Parameter is the new redirection path or empty for Removed or FullRecompute +/// Parameter is the old redirection path for Replaced, or empty. +/// Parameter is the mod responsible for the new redirection if any. +/// +public sealed class ResolvedFileChanged : EventWrapper, + ResolvedFileChanged.Priority> +{ + public enum Type + { + Added, + Removed, + Replaced, + FullRecomputeStart, + FullRecomputeFinished, + } + + public enum Priority + { + /// + DalamudSubstitutionProvider = 0, + } + + public ResolvedFileChanged() + : base(nameof(ResolvedFileChanged)) + { } + + public void Invoke(ModCollection collection, Type type, Utf8GamePath key, FullPath value, FullPath old, IMod? mod) + => Invoke(this, collection, type, key, value, old, mod); +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index beaf1960..cc7cc026 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -38,9 +38,10 @@ public class Configuration : IPluginConfiguration, ISavable public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public bool HideUiInGPose { get; set; } = false; - public bool HideUiInCutscenes { get; set; } = true; - public bool HideUiWhenUiHidden { get; set; } = false; + public bool HideUiInGPose { get; set; } = false; + public bool HideUiInCutscenes { get; set; } = true; + public bool HideUiWhenUiHidden { get; set; } = false; + public bool UseDalamudUiTextureRedirection { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 784b7f89..728b391c 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -66,6 +66,9 @@ public class CommunicatorService : IDisposable /// public readonly SelectTab SelectTab = new(); + /// + public readonly ResolvedFileChanged ResolvedFileChanged = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -86,5 +89,6 @@ public class CommunicatorService : IDisposable ChangedItemHover.Dispose(); ChangedItemClick.Dispose(); SelectTab.Dispose(); + ResolvedFileChanged.Dispose(); } } diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index f0864b97..728585ae 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -181,5 +181,6 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton(x => x.GetRequiredService()) .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 375ada2d..fa4c23f9 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -26,38 +26,41 @@ public class SettingsTab : ITab public ReadOnlySpan Label => "Settings"u8; - private readonly Configuration _config; - private readonly FontReloader _fontReloader; - private readonly TutorialService _tutorial; - private readonly Penumbra _penumbra; - private readonly FileDialogService _fileDialog; - private readonly ModManager _modManager; - private readonly ModExportManager _modExportManager; - private readonly ModFileSystemSelector _selector; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; - private readonly DalamudServices _dalamud; - private readonly HttpApi _httpApi; + private readonly Configuration _config; + private readonly FontReloader _fontReloader; + private readonly TutorialService _tutorial; + private readonly Penumbra _penumbra; + private readonly FileDialogService _fileDialog; + private readonly ModManager _modManager; + private readonly ModExportManager _modExportManager; + private readonly ModFileSystemSelector _selector; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly DalamudServices _dalamud; + private readonly HttpApi _httpApi; + private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, - ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi) + ResidentResourceManager residentResources, DalamudServices dalamud, ModExportManager modExportManager, HttpApi httpApi, + DalamudSubstitutionProvider dalamudSubstitutionProvider) { - _config = config; - _fontReloader = fontReloader; - _tutorial = tutorial; - _penumbra = penumbra; - _fileDialog = fileDialog; - _modManager = modManager; - _selector = selector; - _characterUtility = characterUtility; - _residentResources = residentResources; - _dalamud = dalamud; - _modExportManager = modExportManager; - _httpApi = httpApi; + _config = config; + _fontReloader = fontReloader; + _tutorial = tutorial; + _penumbra = penumbra; + _fileDialog = fileDialog; + _modManager = modManager; + _selector = selector; + _characterUtility = characterUtility; + _residentResources = residentResources; + _dalamud = dalamud; + _modExportManager = modExportManager; + _httpApi = httpApi; + _dalamudSubstitutionProvider = dalamudSubstitutionProvider; } public void DrawHeader() @@ -389,6 +392,12 @@ public class SettingsTab : ITab /// Draw all settings pertaining to actor identification for collections. private void DrawIdentificationSettings() { + Checkbox("Use Interface Collection for other Plugin UIs", + "Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud.", + _dalamudSubstitutionProvider.Enabled, _dalamudSubstitutionProvider.Set); + var icon = _dalamud.TextureProvider.GetIcon(60026); + if (icon != null) + ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); Checkbox($"Use {TutorialService.AssignedCollections} in Character Window", "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", _config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v);