Add support for the DalamudSubstitutionProvider for textures.

This commit is contained in:
Ottermandias 2023-08-16 17:25:06 +02:00
parent cf3810a1b8
commit 04b76ddee1
10 changed files with 285 additions and 66 deletions

View file

@ -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<Utf8GamePath> 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<Utf8GamePath>().AsEnumerable();
enumerable = enumerable.Concat(newCollection?.ResolvedFiles.Keys ?? Array.Empty<Utf8GamePath>().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
}
}
}
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);
}
}

View file

@ -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<SingleArray<ModConflicts>> 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));
/// <summary> Force a file to be resolved to a specific path regardless of conflicts. </summary>
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
}
/// <summary> Add all files and possibly manipulations of a given mod according to its settings in this collection. </summary>
/// <summary> Add all files and possibly manipulations of a given mod according to its settings in this collection. </summary>
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);
}
/// <summary> Invoke only if not in a full recalculation. </summary>
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;
}

View file

@ -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<ModCollection> 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
{

View file

@ -17,6 +17,9 @@ public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCo
{
public enum Priority
{
/// <seealso cref="Api.DalamudSubstitutionProvider.OnCollectionChange"/>
DalamudSubstitutionProvider = -3,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnCollectionChange"/>
CollectionCacheManager = -2,
@ -43,6 +46,7 @@ public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCo
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
ModFileSystemSelector = 0,
}
public CollectionChange()

View file

@ -16,6 +16,9 @@ public sealed class EnabledChanged : EventWrapper<Action<bool>, EnabledChanged.P
{
/// <seealso cref="Ipc.EnabledChange"/>
Api = int.MinValue,
/// <seealso cref="Api.DalamudSubstitutionProvider.OnEnabledChange"/>
DalamudSubstitutionProvider = 0,
}
public EnabledChanged()

View file

@ -0,0 +1,43 @@
using System;
using OtterGui.Classes;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.String.Classes;
namespace Penumbra.Communication;
/// <summary>
/// Triggered whenever a redirection in a mod collection cache is manipulated.
/// <list type="number">
/// <item>Parameter is collection with a changed cache. </item>
/// <item>Parameter is the type of change. </item>
/// <item>Parameter is the game path to be redirected or empty for FullRecompute. </item>
/// <item>Parameter is the new redirection path or empty for Removed or FullRecompute </item>
/// <item>Parameter is the old redirection path for Replaced, or empty. </item>
/// <item>Parameter is the mod responsible for the new redirection if any. </item>
/// </list> </summary>
public sealed class ResolvedFileChanged : EventWrapper<Action<ModCollection, ResolvedFileChanged.Type, Utf8GamePath, FullPath, FullPath, IMod?>,
ResolvedFileChanged.Priority>
{
public enum Type
{
Added,
Removed,
Replaced,
FullRecomputeStart,
FullRecomputeFinished,
}
public enum Priority
{
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChanged"/>
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);
}

View file

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

View file

@ -66,6 +66,9 @@ public class CommunicatorService : IDisposable
/// <inheritdoc cref="Communication.SelectTab"/>
public readonly SelectTab SelectTab = new();
/// <inheritdoc cref="Communication.ResolvedFileChanged"/>
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();
}
}

View file

@ -181,5 +181,6 @@ public static class ServiceManager
=> services.AddSingleton<PenumbraApi>()
.AddSingleton<IPenumbraApi>(x => x.GetRequiredService<PenumbraApi>())
.AddSingleton<PenumbraIpcProviders>()
.AddSingleton<HttpApi>();
.AddSingleton<HttpApi>()
.AddSingleton<DalamudSubstitutionProvider>();
}

View file

@ -26,38 +26,41 @@ public class SettingsTab : ITab
public ReadOnlySpan<byte> 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
/// <summary> Draw all settings pertaining to actor identification for collections. </summary>
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);