Current State.
Some checks failed
.NET Build / build (push) Has been cancelled

This commit is contained in:
Ottermandias 2025-12-05 16:23:14 +01:00
parent 9aa1121410
commit fec7819bf2
118 changed files with 4849 additions and 4283 deletions

2
Luna

@ -1 +1 @@
Subproject commit cb294f476476f7a3d8b56a0072dbd300b3d54c4f Subproject commit 950ebea591f4ab7dc0900cce22415c5221df3685

@ -1 +1 @@
Subproject commit 18e62ab2d8b9ac7028a33707eb35f8f9c61f245a Subproject commit 1459e2b8f5e1687f659836709e23571235d4206c

@ -1 +1 @@
Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 Subproject commit 09cfde3dd43aa5afcfd147ccbe3ee61534556f12

@ -1 +1 @@
Subproject commit 0901f2b7075c280c5216198921a3c09209b667d8 Subproject commit 885e536267f814fc4e62e11a70a82cdde7b4778d

View file

@ -4,19 +4,20 @@ using Penumbra.Communication;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.MainWindow;
namespace Penumbra.Api.Api; namespace Penumbra.Api.Api;
public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly ConfigWindow _configWindow; private readonly MainWindow _mainWindow;
private readonly ModManager _modManager; private readonly ModManager _modManager;
public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager) public UiApi(CommunicatorService communicator, MainWindow mainWindow, ModManager modManager)
{ {
_communicator = communicator; _communicator = communicator;
_configWindow = configWindow; _mainWindow = mainWindow;
_modManager = modManager; _modManager = modManager;
_communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default); _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
_communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default); _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
@ -57,7 +58,7 @@ public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName)
{ {
_configWindow.IsOpen = true; _mainWindow.IsOpen = true;
if (!Enum.IsDefined(tab)) if (!Enum.IsDefined(tab))
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
@ -77,7 +78,7 @@ public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
} }
public void CloseMainWindow() public void CloseMainWindow()
=> _configWindow.IsOpen = false; => _mainWindow.IsOpen = false;
private void OnChangedItemClick(in ChangedItemClick.Arguments arguments) private void OnChangedItemClick(in ChangedItemClick.Arguments arguments)
{ {

View file

@ -85,8 +85,8 @@ public class CollectionCacheManager : IDisposable, IService
foreach (var collection in _storage) foreach (var collection in _storage)
{ {
collection._cache?.Dispose(); collection.Cache?.Dispose();
collection._cache = null; collection.Cache = null;
} }
} }
@ -115,10 +115,10 @@ public class CollectionCacheManager : IDisposable, IService
if (collection.Identity.Index == ModCollection.Empty.Identity.Index) if (collection.Identity.Index == ModCollection.Empty.Identity.Index)
return false; return false;
if (collection._cache != null) if (collection.Cache != null)
return false; return false;
collection._cache = new CollectionCache(this, collection); collection.Cache = new CollectionCache(this, collection);
if (collection.Identity.Index > 0) if (collection.Identity.Index > 0)
Interlocked.Increment(ref _count); Interlocked.Increment(ref _count);
Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}."); Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}.");
@ -146,10 +146,10 @@ public class CollectionCacheManager : IDisposable, IService
Penumbra.Log.Error( Penumbra.Log.Error(
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists."); $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists.");
} }
else if (collection._cache!.Calculating != -1) else if (collection.Cache!.Calculating != -1)
{ {
Penumbra.Log.Error( Penumbra.Log.Error(
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}]."); $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection.Cache!.Calculating}].");
} }
else else
{ {
@ -162,7 +162,7 @@ public class CollectionCacheManager : IDisposable, IService
private void FullRecalculation(ModCollection collection) private void FullRecalculation(ModCollection collection)
{ {
var cache = collection._cache; var cache = collection.Cache;
if (cache is not { Calculating: -1 }) if (cache is not { Calculating: -1 })
return; return;
@ -235,11 +235,11 @@ public class CollectionCacheManager : IDisposable, IService
case ModPathChangeType.Deleted: case ModPathChangeType.Deleted:
case ModPathChangeType.StartingReload: case ModPathChangeType.StartingReload:
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true)) foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true))
collection._cache!.RemoveMod(arguments.Mod, true); collection.Cache!.RemoveMod(arguments.Mod, true);
break; break;
case ModPathChangeType.Moved: case ModPathChangeType.Moved:
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true)) foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true))
collection._cache!.ReloadMod(arguments.Mod, true); collection.Cache!.ReloadMod(arguments.Mod, true);
break; break;
} }
} }
@ -251,7 +251,7 @@ public class CollectionCacheManager : IDisposable, IService
var index = arguments.Mod.Index; var index = arguments.Mod.Index;
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true)) foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true))
collection._cache!.AddMod(arguments.Mod, true); collection.Cache!.AddMod(arguments.Mod, true);
} }
/// <summary> Apply a mod change to all collections with a cache. </summary> /// <summary> Apply a mod change to all collections with a cache. </summary>
@ -281,7 +281,7 @@ public class CollectionCacheManager : IDisposable, IService
var index = arguments.Mod.Index; var index = arguments.Mod.Index;
foreach (var collection in _storage.Where(collection foreach (var collection in _storage.Where(collection
=> collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true })) => collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true }))
collection._cache!.RemoveMod(arguments.Mod, false); collection.Cache!.RemoveMod(arguments.Mod, false);
} }
else else
{ {
@ -295,9 +295,9 @@ public class CollectionCacheManager : IDisposable, IService
=> collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true })) => collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true }))
{ {
if (justAdd) if (justAdd)
collection._cache!.AddMod(arguments.Mod, true); collection.Cache!.AddMod(arguments.Mod, true);
else else
collection._cache!.ReloadMod(arguments.Mod, true); collection.Cache!.ReloadMod(arguments.Mod, true);
} }
} }
} }
@ -316,7 +316,7 @@ public class CollectionCacheManager : IDisposable, IService
if (!collection.HasCache) if (!collection.HasCache)
return; return;
var cache = collection._cache!; var cache = collection.Cache!;
switch (arguments.Type) switch (arguments.Type)
{ {
case ModSettingChange.Inheritance: cache.ReloadMod(arguments.Mod!, true); break; case ModSettingChange.Inheritance: cache.ReloadMod(arguments.Mod!, true); break;
@ -366,8 +366,8 @@ public class CollectionCacheManager : IDisposable, IService
if (!collection.HasCache) if (!collection.HasCache)
return; return;
collection._cache!.Dispose(); collection.Cache!.Dispose();
collection._cache = null; collection.Cache = null;
if (collection.Identity.Index > 0) if (collection.Identity.Index > 0)
Interlocked.Decrement(ref _count); Interlocked.Decrement(ref _count);
Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}."); Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}.");
@ -398,9 +398,9 @@ public class CollectionCacheManager : IDisposable, IService
{ {
foreach (var collection in Active) foreach (var collection in Active)
{ {
collection._cache!.ResolvedFiles.Clear(); collection.Cache!.ResolvedFiles.Clear();
collection._cache!.Meta.Reset(); collection.Cache!.Meta.Reset();
collection._cache!.ConflictDict.Clear(); collection.Cache!.ConflictDict.Clear();
} }
} }

View file

@ -320,7 +320,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ByName(defaultName, out var defaultCollection)) if (!_storage.ByName(defaultName, out var defaultCollection))
{ {
Penumbra.Messager.NotificationMessage( Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.", $"Last choice of {"Base Collection"} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning); NotificationType.Warning);
Default = ModCollection.Empty; Default = ModCollection.Empty;
configChanged = true; configChanged = true;
@ -335,7 +335,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ByName(interfaceName, out var interfaceCollection)) if (!_storage.ByName(interfaceName, out var interfaceCollection))
{ {
Penumbra.Messager.NotificationMessage( Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.", $"Last choice of Interface Collection {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning); NotificationType.Warning);
Interface = ModCollection.Empty; Interface = ModCollection.Empty;
configChanged = true; configChanged = true;
@ -350,7 +350,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ByName(currentName, out var currentCollection)) if (!_storage.ByName(currentName, out var currentCollection))
{ {
Penumbra.Messager.NotificationMessage( Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", $"Last choice of Selected Collection {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning); NotificationType.Warning);
Current = _storage.DefaultNamed; Current = _storage.DefaultNamed;
configChanged = true; configChanged = true;
@ -395,7 +395,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(defaultId, out var defaultCollection)) if (!_storage.ById(defaultId, out var defaultCollection))
{ {
Penumbra.Messager.NotificationMessage( Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.", $"Last choice of {"Base Collection"} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning); NotificationType.Warning);
Default = ModCollection.Empty; Default = ModCollection.Empty;
configChanged = true; configChanged = true;
@ -410,7 +410,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(interfaceId, out var interfaceCollection)) if (!_storage.ById(interfaceId, out var interfaceCollection))
{ {
Penumbra.Messager.NotificationMessage( Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.", $"Last choice of {"Interface Collection"} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning); NotificationType.Warning);
Interface = ModCollection.Empty; Interface = ModCollection.Empty;
configChanged = true; configChanged = true;
@ -425,7 +425,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(currentId, out var currentCollection)) if (!_storage.ById(currentId, out var currentCollection))
{ {
Penumbra.Messager.NotificationMessage( Penumbra.Messager.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", $"Last choice of Selected Collection {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning); NotificationType.Warning);
Current = _storage.DefaultNamed; Current = _storage.DefaultNamed;
configChanged = true; configChanged = true;

View file

@ -10,48 +10,48 @@ namespace Penumbra.Collections;
public partial class ModCollection public partial class ModCollection
{ {
// Only active collections need to have a cache. // Only active collections need to have a cache.
internal CollectionCache? _cache; public CollectionCache? Cache;
public bool HasCache public bool HasCache
=> _cache != null; => Cache != null;
// Handle temporary mods for this collection. // Handle temporary mods for this collection.
public void Apply(TemporaryMod tempMod, bool created) public void Apply(TemporaryMod tempMod, bool created)
{ {
if (created) if (created)
_cache?.AddMod(tempMod, tempMod.TotalManipulations > 0); Cache?.AddMod(tempMod, tempMod.TotalManipulations > 0);
else else
_cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0); Cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0);
} }
public void Remove(TemporaryMod tempMod) public void Remove(TemporaryMod tempMod)
{ {
_cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0); Cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0);
} }
public IEnumerable<Utf8GamePath> ReverseResolvePath(FullPath path) public IEnumerable<Utf8GamePath> ReverseResolvePath(FullPath path)
=> _cache?.ReverseResolvePath(path) ?? Array.Empty<Utf8GamePath>(); => Cache?.ReverseResolvePath(path) ?? [];
public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> paths) public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> paths)
=> _cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet<Utf8GamePath>()).ToArray(); => Cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet<Utf8GamePath>()).ToArray();
public FullPath? ResolvePath(Utf8GamePath path) public FullPath? ResolvePath(Utf8GamePath path)
=> _cache?.ResolvePath(path); => Cache?.ResolvePath(path);
// Obtain data from the cache. // Obtain data from the cache.
internal MetaCache? MetaCache internal MetaCache? MetaCache
=> _cache?.Meta; => Cache?.Meta;
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>(); => Cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)>(); => Cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>(); => Cache?.AllConflicts ?? [];
internal SingleArray<ModConflicts> Conflicts(Mod mod) internal SingleArray<ModConflicts> Conflicts(Mod mod)
=> _cache?.Conflicts(mod) ?? new SingleArray<ModConflicts>(); => Cache?.Conflicts(mod) ?? new SingleArray<ModConflicts>();
} }

View file

@ -13,6 +13,7 @@ using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Knowledge; using Penumbra.UI.Knowledge;
using Penumbra.UI.MainWindow;
namespace Penumbra; namespace Penumbra;
@ -24,7 +25,7 @@ public class CommandHandler : IDisposable, IApiService
private readonly RedrawService _redrawService; private readonly RedrawService _redrawService;
private readonly IChatGui _chat; private readonly IChatGui _chat;
private readonly Configuration _config; private readonly Configuration _config;
private readonly ConfigWindow _configWindow; private readonly MainWindow _mainWindow;
private readonly ActorManager _actors; private readonly ActorManager _actors;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
@ -33,14 +34,14 @@ public class CommandHandler : IDisposable, IApiService
private readonly KnowledgeWindow _knowledgeWindow; private readonly KnowledgeWindow _knowledgeWindow;
public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService,
Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Configuration config, MainWindow mainWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors,
Penumbra penumbra, Penumbra penumbra,
CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow) CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow)
{ {
_commandManager = commandManager; _commandManager = commandManager;
_redrawService = redrawService; _redrawService = redrawService;
_config = config; _config = config;
_configWindow = configWindow; _mainWindow = mainWindow;
_modManager = modManager; _modManager = modManager;
_collectionManager = collectionManager; _collectionManager = collectionManager;
_actors = actors; _actors = actors;
@ -146,11 +147,11 @@ public class CommandHandler : IDisposable, IApiService
private bool ToggleWindow(string arguments) private bool ToggleWindow(string arguments)
{ {
var value = ParseTrueFalseToggle(arguments) ?? !_configWindow.IsOpen; var value = ParseTrueFalseToggle(arguments) ?? !_mainWindow.IsOpen;
if (value == _configWindow.IsOpen) if (value == _mainWindow.IsOpen)
return false; return false;
_configWindow.Toggle(); _mainWindow.Toggle();
return true; return true;
} }
@ -211,12 +212,12 @@ public class CommandHandler : IDisposable, IApiService
if (value) if (value)
{ {
Print("Penumbra UI locked in place."); Print("Penumbra UI locked in place.");
_configWindow.Flags |= WindowFlags.NoMove | WindowFlags.NoResize; _mainWindow.Flags |= WindowFlags.NoMove | WindowFlags.NoResize;
} }
else else
{ {
Print("Penumbra UI unlocked."); Print("Penumbra UI unlocked.");
_configWindow.Flags &= ~(WindowFlags.NoMove | WindowFlags.NoResize); _mainWindow.Flags &= ~(WindowFlags.NoMove | WindowFlags.NoResize);
} }
_config.Ephemeral.FixMainWindow = value; _config.Ephemeral.FixMainWindow = value;

View file

@ -1,61 +1,68 @@
using Luna; using Luna;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.UI.CollectionTab; using Penumbra.UI.CollectionTab;
using Penumbra.UI.MainWindow;
namespace Penumbra.Communication;
namespace Penumbra.Communication;
/// <summary> Triggered whenever collection setup is changed. </summary>
public sealed class CollectionChange(Logger log) /// <summary> Triggered whenever collection setup is changed. </summary>
: EventBase<CollectionChange.Arguments, CollectionChange.Priority>(nameof(CollectionChange), log) public sealed class CollectionChange(Logger log)
{ : EventBase<CollectionChange.Arguments, CollectionChange.Priority>(nameof(CollectionChange), log)
public enum Priority {
{ public enum Priority
/// <seealso cref="Api.DalamudSubstitutionProvider.OnCollectionChange"/> {
DalamudSubstitutionProvider = -3, /// <see cref="EffectiveTab.Cache.OnCollectionChange"/>
EffectiveChangesCache = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnCollectionChange"/>
CollectionCacheManager = -2, /// <seealso cref="Api.DalamudSubstitutionProvider.OnCollectionChange"/>
DalamudSubstitutionProvider = -3,
/// <seealso cref="Collections.Manager.ActiveCollections.OnCollectionChange"/>
ActiveCollections = -1, /// <seealso cref="Collections.Cache.CollectionCacheManager.OnCollectionChange"/>
CollectionCacheManager = -2,
/// <seealso cref="Api.TempModManager.OnCollectionChange"/>
TempModManager = 0, /// <seealso cref="Collections.Manager.ActiveCollections.OnCollectionChange"/>
ActiveCollections = -1,
/// <seealso cref="Collections.Manager.InheritanceManager.OnCollectionChange" />
InheritanceManager = 0, /// <seealso cref="Api.TempModManager.OnCollectionChange"/>
TempModManager = 0,
/// <seealso cref="global::Penumbra.Interop.PathResolving.IdentifiedCollectionCache.CollectionChangeClear" />
IdentifiedCollectionCache = 0, /// <seealso cref="Collections.Manager.InheritanceManager.OnCollectionChange" />
InheritanceManager = 0,
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" />
ItemSwapTab = 0, /// <seealso cref="global::Penumbra.Interop.PathResolving.IdentifiedCollectionCache.CollectionChangeClear" />
IdentifiedCollectionCache = 0,
/// <seealso cref="UI.CollectionTab.CollectionSelector.OnCollectionChange" />
CollectionSelector = 0, /// <seealso cref="ChangedItemsTab.Cache.OnCollectionChange" />
ChangedItemsTabCache = 0,
/// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/>
IndividualAssignmentUi = 0, /// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" />
ItemSwapTab = 0,
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
ModFileSystemSelector = 0, /// <seealso cref="UI.CollectionTab.CollectionSelector.OnCollectionChange" />
CollectionSelector = 0,
/// <seealso cref="Mods.ModSelection.OnCollectionChange"/>
/// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/>
IndividualAssignmentUi = 0,
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
ModFileSystemSelector = 0,
/// <seealso cref="Mods.ModSelection.OnCollectionChange"/>
ModSelection = 10, ModSelection = 10,
/// <seealso cref="CollectionCombo.OnCollectionChanged"/> /// <seealso cref="CollectionCombo.OnCollectionChanged"/>
CollectionCombo = 15, CollectionCombo = 15,
} }
/// <summary> The arguments for a collection change event. </summary> /// <summary> The arguments for a collection change event. </summary>
/// <param name="Type"> The type of the changed collection (<see cref="CollectionType.Inactive"/> or <see cref="CollectionType.Temporary"/> for additions or deletions). </param> /// <param name="Type"> The type of the changed collection (<see cref="CollectionType.Inactive"/> or <see cref="CollectionType.Temporary"/> for additions or deletions). </param>
/// <param name="OldCollection"> The old collection, or null on additions. </param> /// <param name="OldCollection"> The old collection, or null on additions. </param>
/// <param name="NewCollection"> The new collection, or null on deletions. </param> /// <param name="NewCollection"> The new collection, or null on deletions. </param>
/// <param name="DisplayName"> The display name for Individual collections or an empty string otherwise. </param> /// <param name="DisplayName"> The display name for Individual collections or an empty string otherwise. </param>
public readonly record struct Arguments( public readonly record struct Arguments(
CollectionType Type, CollectionType Type,
ModCollection? OldCollection, ModCollection? OldCollection,
ModCollection? NewCollection, ModCollection? NewCollection,
string DisplayName); string DisplayName);
} }

View file

@ -1,5 +1,6 @@
using Luna; using Luna;
using Penumbra.Api.Api; using Penumbra.Api.Api;
using Penumbra.UI.Classes;
namespace Penumbra.Communication; namespace Penumbra.Communication;
@ -12,7 +13,7 @@ public sealed class ModDirectoryChanged(Logger log)
/// <seealso cref="PluginStateApi.ModDirectoryChanged"/> /// <seealso cref="PluginStateApi.ModDirectoryChanged"/>
Api = 0, Api = 0,
/// <seealso cref="UI.FileDialogService.OnModDirectoryChange"/> /// <seealso cref="UI.Classes.FileDialogService.OnModDirectoryChange"/>
FileDialogService = 0, FileDialogService = 0,
} }

View file

@ -1,44 +1,84 @@
using Luna; using Luna;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Mods.Editor; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
namespace Penumbra.Communication; using Penumbra.UI.MainWindow;
/// <summary> Triggered whenever a redirection in a mod collection cache is manipulated. </summary> namespace Penumbra.Communication;
public sealed class ResolvedFileChanged(Logger log) : EventBase<ResolvedFileChanged.Arguments, ResolvedFileChanged.Priority>(
nameof(ResolvedFileChanged), log) /// <summary> Triggered whenever a redirection in a mod collection cache is manipulated. </summary>
{ public sealed class ResolvedFileChanged(Logger log) : EventBase<ResolvedFileChanged.Arguments, ResolvedFileChanged.Priority>(
public enum Type nameof(ResolvedFileChanged), log)
{ {
Added, public enum Type
Removed, {
Replaced, Added,
FullRecomputeStart, Removed,
FullRecomputeFinished, Replaced,
} FullRecomputeStart,
FullRecomputeFinished,
public enum Priority }
{
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChange"/> public enum Priority
DalamudSubstitutionProvider = 0, {
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChange"/>
/// <seealso cref="Interop.Services.SchedulerResourceManagementService.OnResolvedFileChange"/> DalamudSubstitutionProvider = 0,
SchedulerResourceManagementService = 0,
} /// <seealso cref="global::Penumbra.Interop.Services.SchedulerResourceManagementService.OnResolvedFileChange"/>
SchedulerResourceManagementService = 0,
/// <summary> The arguments for a ResolvedFileChanged event. </summary>
/// <param name="Type"> The type of the redirection change. </param> /// <see cref="EffectiveTab.Cache.OnResolvedFileChange"/>
/// <param name="Collection"> The collection with a changed cache. </param> EffectiveChangesCache = int.MinValue,
/// <param name="GamePath"> The game path to be redirected or empty for FullRecompute </param> }
/// <param name="NewRedirection"> The new redirection path or empty for Removed or FullRecompute. </param>
/// <param name="OldRedirection"> The old redirection path for Replaced, or empty. </param> /// <summary> The arguments for a ResolvedFileChanged event. </summary>
/// <param name="Mod"> The mod responsible for the new redirection if any. </param> /// <param name="Type"> The type of the redirection change. </param>
public readonly record struct Arguments( /// <param name="Collection"> The collection with a changed cache. </param>
Type Type, /// <param name="GamePath"> The game path to be redirected or empty for FullRecompute </param>
ModCollection Collection, /// <param name="NewRedirection"> The new redirection path or empty for Removed or FullRecompute. </param>
Utf8GamePath GamePath, /// <param name="OldRedirection"> The old redirection path for Replaced, or empty. </param>
FullPath OldRedirection, /// <param name="Mod"> The mod responsible for the new redirection if any. </param>
FullPath NewRedirection, public readonly record struct Arguments(
IMod? Mod); Type Type,
} ModCollection Collection,
Utf8GamePath GamePath,
FullPath OldRedirection,
FullPath NewRedirection,
IMod? Mod);
}
/// <summary> Triggered whenever a meta edit in a mod collection cache is manipulated. </summary>
public sealed class ResolvedMetaChanged(Logger log) : EventBase<ResolvedMetaChanged.Arguments, ResolvedMetaChanged.Priority>(
nameof(ResolvedMetaChanged), log)
{
public enum Type
{
Added,
Removed,
Replaced,
FullRecomputeStart,
FullRecomputeFinished,
}
public enum Priority
{
/// <see cref="EffectiveTab.Cache.OnResolvedMetaChange"/>
EffectiveChangesCache = int.MinValue,
}
/// <summary> The arguments for a ResolvedMetaChanged event. </summary>
/// <param name="Type"> The type of the meta edit change. </param>
/// <param name="Collection"> The collection with a changed cache. </param>
/// <param name="Identifier"> The meta identifier changed, or invalid for recomputes. </param>
/// <param name="OldValue"> The old value for the meta identifier if any, null otherwise. </param>
/// <param name="NewValue"> The new value for the meta identifier if any, null otherwise. </param>
/// <param name="Mod"> The mod responsible for the new meta edit if any. </param>
public readonly record struct Arguments(
Type Type,
ModCollection Collection,
IMetaIdentifier Identifier,
object? OldValue,
object? NewValue,
IMod? Mod);
}

View file

@ -2,14 +2,12 @@ using Dalamud.Configuration;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Luna; using Luna;
using Newtonsoft.Json; using Newtonsoft.Json;
using OtterGui.Filesystem;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab;
using Penumbra.UI.ModsTab.Selector;
using Penumbra.UI.ResourceWatcher; using Penumbra.UI.ResourceWatcher;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
@ -94,7 +92,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
[JsonConverter(typeof(SortModeConverter))] [JsonConverter(typeof(SortModeConverter))]
[JsonProperty(Order = int.MaxValue)] [JsonProperty(Order = int.MaxValue)]
public ISortMode<Mod> SortMode = ISortMode<Mod>.FoldersFirst; public ISortMode SortMode = ISortMode.FoldersFirst;
public bool OpenFoldersByDefault { get; set; } = false; public bool OpenFoldersByDefault { get; set; } = false;
public int SingleGroupRadioMax { get; set; } = 2; public int SingleGroupRadioMax { get; set; } = 2;
@ -178,40 +176,24 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public const int MinScaledSize = 5; public const int MinScaledSize = 5;
public const int MinimumSizeX = 900; public const int MinimumSizeX = 900;
public const int MinimumSizeY = 675; public const int MinimumSizeY = 675;
public static readonly ISortMode<Mod>[] ValidSortModes =
{
ISortMode<Mod>.FoldersFirst,
ISortMode<Mod>.Lexicographical,
new ModFileSystem.ImportDate(),
new ModFileSystem.InverseImportDate(),
ISortMode<Mod>.InverseFoldersFirst,
ISortMode<Mod>.InverseLexicographical,
ISortMode<Mod>.FoldersLast,
ISortMode<Mod>.InverseFoldersLast,
ISortMode<Mod>.InternalOrder,
ISortMode<Mod>.InverseInternalOrder,
};
} }
/// <summary> Convert SortMode Types to their name. </summary> /// <summary> Convert SortMode Types to their name. </summary>
private class SortModeConverter : JsonConverter<ISortMode<Mod>> private class SortModeConverter : JsonConverter<ISortMode>
{ {
public override void WriteJson(JsonWriter writer, ISortMode<Mod>? value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer)
{ {
value ??= ISortMode<Mod>.FoldersFirst; value ??= ISortMode.FoldersFirst;
serializer.Serialize(writer, value.GetType().Name); serializer.Serialize(writer, value.GetType().Name);
} }
public override ISortMode<Mod> ReadJson(JsonReader reader, Type objectType, ISortMode<Mod>? existingValue, public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue,
bool hasExistingValue, bool hasExistingValue, JsonSerializer serializer)
JsonSerializer serializer)
{ {
var name = serializer.Deserialize<string>(reader); if (serializer.Deserialize<string>(reader) is { } name)
if (name == null || !Constants.ValidSortModes.FindFirst(s => s.GetType().Name == name, out var mode)) return ISortMode.Valid.GetValueOrDefault(name, existingValue ?? ISortMode.FoldersFirst);
return existingValue ?? ISortMode<Mod>.FoldersFirst;
return mode; return existingValue ?? ISortMode.FoldersFirst;
} }
} }
@ -220,8 +202,9 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public void Save(StreamWriter writer) public void Save(StreamWriter writer)
{ {
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; using var jWriter = new JsonTextWriter(writer);
var serializer = new JsonSerializer { Formatting = Formatting.Indented }; jWriter.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this); serializer.Serialize(jWriter, this);
} }

View file

@ -1,12 +1,12 @@
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Luna; using Luna;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Api.Enums;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Enums; using Penumbra.Enums;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.UI.ResourceWatcher; using Penumbra.UI.ResourceWatcher;
using Penumbra.UI.Tabs; using Penumbra.UI.Tabs;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;

View file

@ -50,7 +50,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, Luna.IRequired
private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject)
{ {
var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true); var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true);
if (resolveData.ModCollection._cache is not { } cache) if (resolveData.ModCollection.Cache is not { } cache)
return _resourceLoader.LoadResolvedSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath.Path, resolveData); return _resourceLoader.LoadResolvedSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath.Path, resolveData);
return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData);

View file

@ -14,6 +14,7 @@ using Penumbra.Meta;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Classes;
using static Penumbra.Interop.Structs.StructExtensions; using static Penumbra.Interop.Structs.StructExtensions;
using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
using SpanTextWriter = Luna.SpanTextWriter; using SpanTextWriter = Luna.SpanTextWriter;

View file

@ -3,6 +3,7 @@ using Penumbra.Mods;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Classes;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;

View file

@ -9,6 +9,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Classes;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
@ -192,7 +193,7 @@ public class ResourceTree(
{ {
var genericContext = globalContext.CreateContext(&human->CharacterBase); var genericContext = globalContext.CreateContext(&human->CharacterBase);
var cache = globalContext.Collection._cache; var cache = globalContext.Collection.Cache;
if (cache is not null if (cache is not null
&& cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle) && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)
&& genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode) && genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode)

View file

@ -4,6 +4,7 @@ using Penumbra.Api.Enums;
using Penumbra.Api.Helpers; using Penumbra.Api.Helpers;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.Classes;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;

View file

@ -29,7 +29,7 @@ public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRac
} }
public override string ToString() public override string ToString()
=> $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; => $"ATCH - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{ {

View file

@ -47,7 +47,7 @@ public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
public override string ToString() public override string ToString()
{ {
var sb = new StringBuilder(64); var sb = new StringBuilder(64);
sb.Append("Shp - ") sb.Append("ATR - ")
.Append(Attribute); .Append(Attribute);
if (Slot is HumanSlot.Unknown) if (Slot is HumanSlot.Unknown)
{ {

View file

@ -22,7 +22,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge
=> CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory()); => CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory());
public override string ToString() public override string ToString()
=> $"Eqdp - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}"; => $"EQDP - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}";
public bool Validate() public bool Validate()
{ {

View file

@ -15,7 +15,7 @@ public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : I
=> MetaIndex.Eqp; => MetaIndex.Eqp;
public override string ToString() public override string ToString()
=> $"Eqp - {SetId} - {Slot}"; => $"EQP - {SetId} - {Slot}";
public bool Validate() public bool Validate()
{ {

View file

@ -54,7 +54,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende
=> (MetaIndex)Slot; => (MetaIndex)Slot;
public override string ToString() public override string ToString()
=> $"Est - {SetId} - {Slot} - {GenderRace.ToName()}"; => $"EST - {SetId} - {Slot} - {GenderRace.ToName()}";
public bool Validate() public bool Validate()
{ {

View file

@ -15,7 +15,7 @@ public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier,
=> MetaIndex.Gmp; => MetaIndex.Gmp;
public override string ToString() public override string ToString()
=> $"Gmp - {SetId}"; => $"GMP - {SetId}";
public bool Validate() public bool Validate()
// No known conditions. // No known conditions.

View file

@ -62,9 +62,9 @@ public readonly record struct ImcIdentifier(
public override string ToString() public override string ToString()
=> ObjectType switch => ObjectType switch
{ {
ObjectType.Equipment or ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}", ObjectType.Equipment or ObjectType.Accessory => $"IMC - {PrimaryId} - {EquipSlot.ToName()} - {Variant}",
ObjectType.DemiHuman => $"Imc - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}", ObjectType.DemiHuman => $"IMC - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}",
_ => $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}", _ => $"IMC - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}",
}; };

View file

@ -66,7 +66,7 @@ public readonly record struct ShpIdentifier(
public override string ToString() public override string ToString()
{ {
var sb = new StringBuilder(64); var sb = new StringBuilder(64);
sb.Append("Shp - ") sb.Append("SHP - ")
.Append(Shape); .Append(Shape);
if (Slot is HumanSlot.Unknown) if (Slot is HumanSlot.Unknown)
{ {

View file

@ -1,12 +1,50 @@
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using ImSharp;
using Luna; using Luna;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
using Penumbra.UI.ModsTab.Selector;
using FileSystemChangeType = OtterGui.Filesystem.FileSystemChangeType; using FileSystemChangeType = OtterGui.Filesystem.FileSystemChangeType;
namespace Penumbra.Mods.Manager; namespace Penumbra.Mods.Manager;
public sealed class ModTab : TwoPanelLayout, ITab<TabType>
{
public override ReadOnlySpan<byte> Label
=> "Mods2"u8;
public ModTab(ModFileSystemDrawer drawer, ModPanel panel, CollectionSelectHeader collectionHeader)
{
LeftHeader = drawer.Header;
LeftFooter = drawer.Footer;
LeftPanel = drawer;
RightPanel = panel;
RightHeader = collectionHeader;
}
public void DrawContent()
=> Draw();
public TabType Identifier
=> TabType.Mods;
protected override void SetSize(Vector2 newSize)
{
base.SetSize(newSize);
((ModFileSystemDrawer)LeftPanel).Config.Ephemeral.CurrentModSelectorWidth = newSize.X / Im.Style.GlobalScale;
}
protected override float MinimumWidth
=> ((ModFileSystemDrawer)LeftPanel).Footer.Buttons.Count * Im.Style.FrameHeight;
protected override float MaximumWidth
=> Im.Window.Width - 500 * Im.Style.GlobalScale;
}
public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredService public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredService
{ {
private readonly Configuration _config; private readonly Configuration _config;
@ -14,7 +52,7 @@ public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredServi
private readonly ModFileSystemSaver _saver; private readonly ModFileSystemSaver _saver;
public ModFileSystem2(Configuration config, CommunicatorService communicator, SaveService saveService, Logger log, ModStorage modStorage) public ModFileSystem2(Configuration config, CommunicatorService communicator, SaveService saveService, Logger log, ModStorage modStorage)
: base("ModFileSystem", log) : base("ModFileSystem", log, true)
{ {
_config = config; _config = config;
_communicator = communicator; _communicator = communicator;

View file

@ -15,6 +15,9 @@ public sealed class ModFileSystemSaver(Logger log, BaseFileSystem fileSystem, Sa
protected override string EmptyFoldersFile(FilenameService provider) protected override string EmptyFoldersFile(FilenameService provider)
=> provider.FileSystemEmptyFolders; => provider.FileSystemEmptyFolders;
protected override string SelectionFile(FilenameService provider)
=> provider.FileSystemSelectedNodes;
protected override string MigrationFile(FilenameService provider) protected override string MigrationFile(FilenameService provider)
=> provider.OldFilesystemFile; => provider.OldFilesystemFile;

View file

@ -1,28 +0,0 @@
using Luna;
namespace Penumbra.Mods.Manager;
public readonly struct ImportDate : ISortMode
{
public ReadOnlySpan<byte> Name
=> "Import Date (Older First)"u8;
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
=> f.GetSubFolders().Cast<IFileSystemNode>().Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderBy(l => l.Value.ImportDate));
}
public readonly struct InverseImportDate : ISortMode
{
public ReadOnlySpan<byte> Name
=> "Import Date (Newer First)"u8;
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
=> f.GetSubFolders().Cast<IFileSystemNode>()
.Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderByDescending(l => l.Value.ImportDate));
}

View file

@ -3,8 +3,6 @@ using Dalamud.Plugin.Services;
using ImSharp; using ImSharp;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Luna; using Luna;
using OtterGui;
using OtterGui.Tasks;
using Penumbra.Api; using Penumbra.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
@ -21,6 +19,7 @@ using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow;
using Penumbra.UI.MainWindow;
using Penumbra.UI.Tabs; using Penumbra.UI.Tabs;
using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
@ -131,7 +130,7 @@ public class Penumbra : IDalamudPlugin
private void SetupInterface() private void SetupInterface()
{ {
AsyncTask.Run(() => Task.Run(() =>
{ {
var system = _services.GetService<PenumbraWindowSystem>(); var system = _services.GetService<PenumbraWindowSystem>();
system.Window.Setup(this, _services.GetService<MainTabBar>()); system.Window.Setup(this, _services.GetService<MainTabBar>());
@ -144,12 +143,16 @@ public class Penumbra : IDalamudPlugin
var mods = _services.GetService<ModManager>(); var mods = _services.GetService<ModManager>();
var editWindowFactory = _services.GetService<ModEditWindowFactory>(); var editWindowFactory = _services.GetService<ModEditWindowFactory>();
foreach (var identifier in _config.Ephemeral.AdvancedEditingOpenForModPaths) foreach (var identifier in _config.Ephemeral.AdvancedEditingOpenForModPaths)
{
if (mods.TryGetMod(identifier, out var mod)) if (mods.TryGetMod(identifier, out var mod))
editWindowFactory.OpenForMod(mod); editWindowFactory.OpenForMod(mod);
}
} }
} }
else else
{
system.Dispose(); system.Dispose();
}
} }
); );
} }
@ -203,8 +206,8 @@ public class Penumbra : IDalamudPlugin
"Ktisis", "Brio", "Ktisis", "Brio",
"heliosphere-plugin", "VfxEditor", "IllusioVitae", "Aetherment", "heliosphere-plugin", "VfxEditor", "IllusioVitae", "Aetherment",
"DynamicBridge", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", "DynamicBridge", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn",
"MareSynchronos", "LoporritSync", "KittenSync", "Snowcloak", "LightlessSync", "Sphene", "XivSync", "MareSempiterne" /* PlayerSync */, "AnatoliIliou", "LaciSynchroni" "MareSynchronos", "LoporritSync", "KittenSync", "Snowcloak", "LightlessSync", "Sphene", "XivSync",
"MareSempiterne" /* PlayerSync */, "AnatoliIliou", "LaciSynchroni",
]; ];
var plugins = _services.GetService<IDalamudPluginInterface>().InstalledPlugins var plugins = _services.GetService<IDalamudPluginInterface>().InstalledPlugins
.GroupBy(p => p.InternalName) .GroupBy(p => p.InternalName)
@ -238,7 +241,7 @@ public class Penumbra : IDalamudPlugin
sb.Append( sb.Append(
$"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n"); $"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n");
sb.Append( sb.Append(
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); $"> **`Free Drive Space: `** {(drive != null ? FormattingFunctions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n"); sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n");
sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n");
sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n");
@ -285,7 +288,7 @@ public class Penumbra : IDalamudPlugin
sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.Identity.AnonymizedName}\n"); sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.Identity.AnonymizedName}\n");
foreach (var collection in _collectionManager.Caches.Active) foreach (var collection in _collectionManager.Caches.Active)
PrintCollection(collection, collection._cache!); PrintCollection(collection, collection.Cache!);
return sb.ToString(); return sb.ToString();

View file

@ -72,7 +72,7 @@
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" /> <ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" /> <ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
<ProjectReference Include="..\Luna\Luna.Generators\Luna.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> <ProjectReference Include="..\Luna\Luna.Generators\Luna.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup> </ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion"> <Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">

View file

@ -1,85 +1,88 @@
using Luna; using Luna;
using Penumbra.Communication; using Penumbra.Communication;
namespace Penumbra.Services; namespace Penumbra.Services;
public class CommunicatorService(ServiceManager services) : IService public class CommunicatorService(ServiceManager services) : IService
{ {
/// <inheritdoc cref="Communication.CollectionChange"/> /// <inheritdoc cref="Communication.CollectionChange"/>
public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>(); public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>();
/// <inheritdoc cref="Communication.TemporaryGlobalModChange"/> /// <inheritdoc cref="Communication.TemporaryGlobalModChange"/>
public readonly TemporaryGlobalModChange TemporaryGlobalModChange = services.GetService<TemporaryGlobalModChange>(); public readonly TemporaryGlobalModChange TemporaryGlobalModChange = services.GetService<TemporaryGlobalModChange>();
/// <inheritdoc cref="Communication.CreatingCharacterBase"/> /// <inheritdoc cref="Communication.CreatingCharacterBase"/>
public readonly CreatingCharacterBase CreatingCharacterBase = services.GetService<CreatingCharacterBase>(); public readonly CreatingCharacterBase CreatingCharacterBase = services.GetService<CreatingCharacterBase>();
/// <inheritdoc cref="Communication.CreatedCharacterBase"/> /// <inheritdoc cref="Communication.CreatedCharacterBase"/>
public readonly CreatedCharacterBase CreatedCharacterBase = services.GetService<CreatedCharacterBase>(); public readonly CreatedCharacterBase CreatedCharacterBase = services.GetService<CreatedCharacterBase>();
/// <inheritdoc cref="Communication.MtrlLoaded"/> /// <inheritdoc cref="Communication.MtrlLoaded"/>
public readonly MtrlLoaded MtrlLoaded = services.GetService<MtrlLoaded>(); public readonly MtrlLoaded MtrlLoaded = services.GetService<MtrlLoaded>();
/// <inheritdoc cref="Communication.ModDataChanged"/> /// <inheritdoc cref="Communication.ModDataChanged"/>
public readonly ModDataChanged ModDataChanged = services.GetService<ModDataChanged>(); public readonly ModDataChanged ModDataChanged = services.GetService<ModDataChanged>();
/// <inheritdoc cref="Communication.ModOptionChanged"/> /// <inheritdoc cref="Communication.ModOptionChanged"/>
public readonly ModOptionChanged ModOptionChanged = services.GetService<ModOptionChanged>(); public readonly ModOptionChanged ModOptionChanged = services.GetService<ModOptionChanged>();
/// <inheritdoc cref="Communication.ModDiscoveryStarted"/> /// <inheritdoc cref="Communication.ModDiscoveryStarted"/>
public readonly ModDiscoveryStarted ModDiscoveryStarted = services.GetService<ModDiscoveryStarted>(); public readonly ModDiscoveryStarted ModDiscoveryStarted = services.GetService<ModDiscoveryStarted>();
/// <inheritdoc cref="Communication.ModDiscoveryFinished"/> /// <inheritdoc cref="Communication.ModDiscoveryFinished"/>
public readonly ModDiscoveryFinished ModDiscoveryFinished = services.GetService<ModDiscoveryFinished>(); public readonly ModDiscoveryFinished ModDiscoveryFinished = services.GetService<ModDiscoveryFinished>();
/// <inheritdoc cref="Communication.ModDirectoryChanged"/> /// <inheritdoc cref="Communication.ModDirectoryChanged"/>
public readonly ModDirectoryChanged ModDirectoryChanged = services.GetService<ModDirectoryChanged>(); public readonly ModDirectoryChanged ModDirectoryChanged = services.GetService<ModDirectoryChanged>();
/// <inheritdoc cref="Communication.ModFileChanged"/> /// <inheritdoc cref="Communication.ModFileChanged"/>
public readonly ModFileChanged ModFileChanged = services.GetService<ModFileChanged>(); public readonly ModFileChanged ModFileChanged = services.GetService<ModFileChanged>();
/// <inheritdoc cref="Communication.ModPathChanged"/> /// <inheritdoc cref="Communication.ModPathChanged"/>
public readonly ModPathChanged ModPathChanged = services.GetService<ModPathChanged>(); public readonly ModPathChanged ModPathChanged = services.GetService<ModPathChanged>();
/// <inheritdoc cref="Communication.ModSettingChanged"/> /// <inheritdoc cref="Communication.ModSettingChanged"/>
public readonly ModSettingChanged ModSettingChanged = services.GetService<ModSettingChanged>(); public readonly ModSettingChanged ModSettingChanged = services.GetService<ModSettingChanged>();
/// <inheritdoc cref="Communication.CollectionInheritanceChanged"/> /// <inheritdoc cref="Communication.CollectionInheritanceChanged"/>
public readonly CollectionInheritanceChanged CollectionInheritanceChanged = services.GetService<CollectionInheritanceChanged>(); public readonly CollectionInheritanceChanged CollectionInheritanceChanged = services.GetService<CollectionInheritanceChanged>();
/// <inheritdoc cref="Communication.EnabledChanged"/> /// <inheritdoc cref="Communication.EnabledChanged"/>
public readonly EnabledChanged EnabledChanged = services.GetService<EnabledChanged>(); public readonly EnabledChanged EnabledChanged = services.GetService<EnabledChanged>();
/// <inheritdoc cref="Communication.PreSettingsTabBarDraw"/> /// <inheritdoc cref="Communication.PreSettingsTabBarDraw"/>
public readonly PreSettingsTabBarDraw PreSettingsTabBarDraw = services.GetService<PreSettingsTabBarDraw>(); public readonly PreSettingsTabBarDraw PreSettingsTabBarDraw = services.GetService<PreSettingsTabBarDraw>();
/// <inheritdoc cref="Communication.PreSettingsPanelDraw"/> /// <inheritdoc cref="Communication.PreSettingsPanelDraw"/>
public readonly PreSettingsPanelDraw PreSettingsPanelDraw = services.GetService<PreSettingsPanelDraw>(); public readonly PreSettingsPanelDraw PreSettingsPanelDraw = services.GetService<PreSettingsPanelDraw>();
/// <inheritdoc cref="Communication.PostEnabledDraw"/> /// <inheritdoc cref="Communication.PostEnabledDraw"/>
public readonly PostEnabledDraw PostEnabledDraw = services.GetService<PostEnabledDraw>(); public readonly PostEnabledDraw PostEnabledDraw = services.GetService<PostEnabledDraw>();
/// <inheritdoc cref="Communication.PostSettingsPanelDraw"/> /// <inheritdoc cref="Communication.PostSettingsPanelDraw"/>
public readonly PostSettingsPanelDraw PostSettingsPanelDraw = services.GetService<PostSettingsPanelDraw>(); public readonly PostSettingsPanelDraw PostSettingsPanelDraw = services.GetService<PostSettingsPanelDraw>();
/// <inheritdoc cref="Communication.ChangedItemHover"/> /// <inheritdoc cref="Communication.ChangedItemHover"/>
public readonly ChangedItemHover ChangedItemHover = services.GetService<ChangedItemHover>(); public readonly ChangedItemHover ChangedItemHover = services.GetService<ChangedItemHover>();
/// <inheritdoc cref="Communication.ChangedItemClick"/> /// <inheritdoc cref="Communication.ChangedItemClick"/>
public readonly ChangedItemClick ChangedItemClick = services.GetService<ChangedItemClick>(); public readonly ChangedItemClick ChangedItemClick = services.GetService<ChangedItemClick>();
/// <inheritdoc cref="Communication.SelectTab"/> /// <inheritdoc cref="Communication.SelectTab"/>
public readonly SelectTab SelectTab = services.GetService<SelectTab>(); public readonly SelectTab SelectTab = services.GetService<SelectTab>();
/// <inheritdoc cref="Communication.ResolvedFileChanged"/> /// <inheritdoc cref="Communication.ResolvedFileChanged"/>
public readonly ResolvedFileChanged ResolvedFileChanged = services.GetService<ResolvedFileChanged>(); public readonly ResolvedFileChanged ResolvedFileChanged = services.GetService<ResolvedFileChanged>();
/// <inheritdoc cref="Communication.PcpCreation"/> /// <inheritdoc cref="Communication.ResolvedFileChanged"/>
public readonly PcpCreation PcpCreation = services.GetService<PcpCreation>(); public readonly ResolvedMetaChanged ResolvedMetaChanged = services.GetService<ResolvedMetaChanged>();
/// <inheritdoc cref="Communication.PcpParsing"/> /// <inheritdoc cref="Communication.PcpCreation"/>
public readonly PcpParsing PcpParsing = services.GetService<PcpParsing>(); public readonly PcpCreation PcpCreation = services.GetService<PcpCreation>();
/// <inheritdoc cref="Communication.CharacterUtilityFinished"/> /// <inheritdoc cref="Communication.PcpParsing"/>
public readonly CharacterUtilityFinished LoadingFinished = services.GetService<CharacterUtilityFinished>(); public readonly PcpParsing PcpParsing = services.GetService<PcpParsing>();
}
/// <inheritdoc cref="Communication.CharacterUtilityFinished"/>
public readonly CharacterUtilityFinished LoadingFinished = services.GetService<CharacterUtilityFinished>();
}

View file

@ -1,432 +1,431 @@
using Newtonsoft.Json; using Luna;
using Newtonsoft.Json.Linq; using Newtonsoft.Json;
using OtterGui.Filesystem; using Newtonsoft.Json.Linq;
using Penumbra.Api.Enums; using Penumbra.Collections;
using Penumbra.Collections; using Penumbra.Collections.Manager;
using Penumbra.Collections.Manager; using Penumbra.Enums;
using Penumbra.Enums; using Penumbra.Interop.Services;
using Penumbra.Interop.Services; using Penumbra.Mods.Editor;
using Penumbra.Mods; using Penumbra.Mods.Manager;
using Penumbra.Mods.Editor; using Penumbra.Mods.Settings;
using Penumbra.Mods.Manager; using Penumbra.UI;
using Penumbra.Mods.Settings; using Penumbra.UI.Classes;
using Penumbra.UI; using Penumbra.UI.ResourceWatcher;
using Penumbra.UI.Classes; using Penumbra.UI.Tabs;
using Penumbra.UI.ResourceWatcher; using TabType = Penumbra.Api.Enums.TabType;
using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType; namespace Penumbra.Services;
namespace Penumbra.Services; /// <summary>
/// Contains everything to migrate from older versions of the config to the current,
/// <summary> /// including deprecated fields.
/// Contains everything to migrate from older versions of the config to the current, /// </summary>
/// including deprecated fields. public class ConfigMigrationService(SaveService saveService, BackupService backupService) : IService
/// </summary> {
public class ConfigMigrationService(SaveService saveService, BackupService backupService) : Luna.IService private Configuration _config = null!;
{ private JObject _data = null!;
private Configuration _config = null!;
private JObject _data = null!; public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName;
public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName;
public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName; public string ForcedCollection = string.Empty;
public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName; public Dictionary<string, string> CharacterCollections = [];
public string ForcedCollection = string.Empty; public Dictionary<string, string> ModSortOrder = [];
public Dictionary<string, string> CharacterCollections = []; public bool InvertModListOrder;
public Dictionary<string, string> ModSortOrder = []; public bool SortFoldersFirst;
public bool InvertModListOrder; public SortModeV3 SortMode = SortModeV3.FoldersFirst;
public bool SortFoldersFirst;
public SortModeV3 SortMode = SortModeV3.FoldersFirst; /// <summary> Add missing colors to the dictionary if necessary. </summary>
private static void AddColors(Configuration config, bool forceSave)
/// <summary> Add missing colors to the dictionary if necessary. </summary> {
private static void AddColors(Configuration config, bool forceSave) var save = false;
{ foreach (var color in Enum.GetValues<ColorId>())
var save = false; save |= config.Colors.TryAdd(color, color.Data().DefaultColor);
foreach (var color in Enum.GetValues<ColorId>())
save |= config.Colors.TryAdd(color, color.Data().DefaultColor); if (save || forceSave)
config.Save();
if (save || forceSave)
config.Save(); Colors.SetColors(config);
}
Colors.SetColors(config);
} public void Migrate(CharacterUtility utility, Configuration config)
{
public void Migrate(CharacterUtility utility, Configuration config) _config = config;
{ // Do this on every migration from now on for a while
_config = config; // because it stayed alive for a bunch of people for some reason.
// Do this on every migration from now on for a while DeleteMetaTmp();
// because it stayed alive for a bunch of people for some reason.
DeleteMetaTmp(); if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigurationFile))
{
if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigurationFile)) AddColors(config, false);
{ return;
AddColors(config, false); }
return;
} _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigurationFile));
CreateBackup();
_data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigurationFile));
CreateBackup(); Version0To1();
Version1To2(utility);
Version0To1(); Version2To3();
Version1To2(utility); Version3To4();
Version2To3(); Version4To5();
Version3To4(); Version5To6();
Version4To5(); Version6To7();
Version5To6(); Version7To8();
Version6To7(); Version8To9();
Version7To8(); Version9To10();
Version8To9(); AddColors(config, true);
Version9To10(); }
AddColors(config, true);
} private void Version9To10()
{
private void Version9To10() if (_config.Version != 9)
{
if (_config.Version != 9)
return; return;
backupService.CreateMigrationBackup("pre_filesystem_update", saveService.FileNames.OldFilesystemFile); backupService.CreateMigrationBackup("pre_filesystem_update", saveService.FileNames.OldFilesystemFile);
_config.Version = 10; _config.Version = 10;
_config.Ephemeral.Version = 10; _config.Ephemeral.Version = 10;
_config.Save(); _config.Save();
_config.Ephemeral.Save(); _config.Ephemeral.Save();
} }
// Migrate to ephemeral config. // Migrate to ephemeral config.
private void Version8To9() private void Version8To9()
{ {
if (_config.Version != 8) if (_config.Version != 8)
return; return;
backupService.CreateMigrationBackup("pre_collection_identifiers"); backupService.CreateMigrationBackup("pre_collection_identifiers");
_config.Version = 9; _config.Version = 9;
_config.Ephemeral.Version = 9; _config.Ephemeral.Version = 9;
_config.Save(); _config.Save();
_config.Ephemeral.Save(); _config.Ephemeral.Save();
} }
// Migrate to ephemeral config. // Migrate to ephemeral config.
private void Version7To8() private void Version7To8()
{ {
if (_config.Version != 7) if (_config.Version != 7)
return; return;
_config.Version = 8; _config.Version = 8;
_config.Ephemeral.Version = 8; _config.Ephemeral.Version = 8;
_config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject<int>() ?? _config.Ephemeral.LastSeenVersion; _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject<int>() ?? _config.Ephemeral.LastSeenVersion;
_config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject<bool>() ?? _config.Ephemeral.DebugSeparateWindow; _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject<bool>() ?? _config.Ephemeral.DebugSeparateWindow;
_config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject<int>() ?? _config.Ephemeral.TutorialStep; _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject<int>() ?? _config.Ephemeral.TutorialStep;
_config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject<bool>() ?? _config.Ephemeral.EnableResourceLogging; _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject<bool>() ?? _config.Ephemeral.EnableResourceLogging;
_config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject<string>() ?? _config.Ephemeral.ResourceLoggingFilter; _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject<string>() ?? _config.Ephemeral.ResourceLoggingFilter;
_config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject<bool>() ?? _config.Ephemeral.EnableResourceWatcher; _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject<bool>() ?? _config.Ephemeral.EnableResourceWatcher;
_config.Ephemeral.OnlyAddMatchingResources = _config.Ephemeral.OnlyAddMatchingResources =
_data["OnlyAddMatchingResources"]?.ToObject<bool>() ?? _config.Ephemeral.OnlyAddMatchingResources; _data["OnlyAddMatchingResources"]?.ToObject<bool>() ?? _config.Ephemeral.OnlyAddMatchingResources;
_config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject<ResourceTypeFlag>() _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject<ResourceTypeFlag>()
?? _config.Ephemeral.ResourceWatcherResourceTypes; ?? _config.Ephemeral.ResourceWatcherResourceTypes;
_config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject<ResourceCategoryFlag>() _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject<ResourceCategoryFlag>()
?? _config.Ephemeral.ResourceWatcherResourceCategories; ?? _config.Ephemeral.ResourceWatcherResourceCategories;
_config.Ephemeral.ResourceWatcherRecordTypes = _config.Ephemeral.ResourceWatcherRecordTypes =
_data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes; _data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
_config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionsTab.PanelMode>() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionsTab.PanelMode>() ?? _config.Ephemeral.CollectionPanel;
_config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab;
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>() _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()
?? _config.Ephemeral.ChangedItemFilter; ?? _config.Ephemeral.ChangedItemFilter;
_config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject<bool>() ?? _config.Ephemeral.FixMainWindow; _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject<bool>() ?? _config.Ephemeral.FixMainWindow;
_config.Ephemeral.Save(); _config.Ephemeral.Save();
} }
// Gendered special collections were added. // Gendered special collections were added.
private void Version6To7() private void Version6To7()
{ {
if (_config.Version != 6) if (_config.Version != 6)
return; return;
ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames); ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames);
_config.Version = 7; _config.Version = 7;
} }
// A new tutorial step was inserted in the middle. // A new tutorial step was inserted in the middle.
// The UI collection and a new tutorial for it was added. // The UI collection and a new tutorial for it was added.
// The migration for the UI collection itself happens in the ActiveCollections file. // The migration for the UI collection itself happens in the ActiveCollections file.
private void Version5To6() private void Version5To6()
{ {
if (_config.Version != 5) if (_config.Version != 5)
return; return;
if (_config.Ephemeral.TutorialStep == 25) if (_config.Ephemeral.TutorialStep == 25)
_config.Ephemeral.TutorialStep = 27; _config.Ephemeral.TutorialStep = 27;
_config.Version = 6; _config.Version = 6;
} }
// Mod backup extension was changed from .zip to .pmp. // Mod backup extension was changed from .zip to .pmp.
// Actual migration takes place in ModManager. // Actual migration takes place in ModManager.
private void Version4To5() private void Version4To5()
{ {
if (_config.Version != 4) if (_config.Version != 4)
return; return;
ModBackup.MigrateModBackups = true; ModBackup.MigrateModBackups = true;
_config.Version = 5; _config.Version = 5;
} }
// SortMode was changed from an enum to a type. // SortMode was changed from an enum to a type.
private void Version3To4() private void Version3To4()
{ {
if (_config.Version != 3) if (_config.Version != 3)
return; return;
SortMode = _data[nameof(SortMode)]?.ToObject<SortModeV3>() ?? SortMode; SortMode = _data[nameof(SortMode)]?.ToObject<SortModeV3>() ?? SortMode;
_config.SortMode = SortMode switch _config.SortMode = SortMode switch
{ {
SortModeV3.FoldersFirst => ISortMode<Mod>.FoldersFirst, SortModeV3.FoldersFirst => ISortMode.FoldersFirst,
SortModeV3.Lexicographical => ISortMode<Mod>.Lexicographical, SortModeV3.Lexicographical => ISortMode.Lexicographical,
SortModeV3.InverseFoldersFirst => ISortMode<Mod>.InverseFoldersFirst, SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst,
SortModeV3.InverseLexicographical => ISortMode<Mod>.InverseLexicographical, SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical,
SortModeV3.FoldersLast => ISortMode<Mod>.FoldersLast, SortModeV3.FoldersLast => ISortMode.FoldersLast,
SortModeV3.InverseFoldersLast => ISortMode<Mod>.InverseFoldersLast, SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast,
SortModeV3.InternalOrder => ISortMode<Mod>.InternalOrder, SortModeV3.InternalOrder => ISortMode.InternalOrder,
SortModeV3.InternalOrderInverse => ISortMode<Mod>.InverseInternalOrder, SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder,
_ => ISortMode<Mod>.FoldersFirst, _ => ISortMode.FoldersFirst,
}; };
_config.Version = 4; _config.Version = 4;
} }
// SortFoldersFirst was changed from a bool to the enum SortMode. // SortFoldersFirst was changed from a bool to the enum SortMode.
private void Version2To3() private void Version2To3()
{ {
if (_config.Version != 2) if (_config.Version != 2)
return; return;
SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject<bool>() ?? false; SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject<bool>() ?? false;
SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical;
_config.Version = 3; _config.Version = 3;
} }
// The forced collection was removed due to general inheritance. // The forced collection was removed due to general inheritance.
// Sort Order was moved to a separate file and may contain empty folders. // Sort Order was moved to a separate file and may contain empty folders.
// Active collections in general were moved to their own file. // Active collections in general were moved to their own file.
// Delete the penumbrametatmp folder if it exists. // Delete the penumbrametatmp folder if it exists.
private void Version1To2(CharacterUtility utility) private void Version1To2(CharacterUtility utility)
{ {
if (_config.Version != 1) if (_config.Version != 1)
return; return;
// Ensure the right meta files are loaded. // Ensure the right meta files are loaded.
DeleteMetaTmp(); DeleteMetaTmp();
if (utility.Ready) if (utility.Ready)
utility.LoadCharacterResources(); utility.LoadCharacterResources();
ResettleSortOrder(); ResettleSortOrder();
ResettleCollectionSettings(); ResettleCollectionSettings();
ResettleForcedCollection(); ResettleForcedCollection();
_config.Version = 2; _config.Version = 2;
} }
private void DeleteMetaTmp() private void DeleteMetaTmp()
{ {
var path = Path.Combine(_config.ModDirectory, "penumbrametatmp"); var path = Path.Combine(_config.ModDirectory, "penumbrametatmp");
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return; return;
try try
{ {
Directory.Delete(path, true); Directory.Delete(path, true);
} }
catch (Exception e) catch (Exception e)
{ {
Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}"); Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}");
} }
} }
private void ResettleForcedCollection() private void ResettleForcedCollection()
{ {
ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject<string>() ?? ForcedCollection; ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject<string>() ?? ForcedCollection;
if (ForcedCollection.Length <= 0) if (ForcedCollection.Length <= 0)
return; return;
// Add the previous forced collection to all current collections except itself as an inheritance. // Add the previous forced collection to all current collections except itself as an inheritance.
foreach (var collection in saveService.FileNames.CollectionFiles) foreach (var collection in saveService.FileNames.CollectionFiles)
{ {
try try
{ {
var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); var jObject = JObject.Parse(File.ReadAllText(collection.FullName));
if (jObject["Name"]?.ToObject<string>() == ForcedCollection) if (jObject["Name"]?.ToObject<string>() == ForcedCollection)
continue; continue;
jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List<string> { ForcedCollection }); jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List<string> { ForcedCollection });
File.WriteAllText(collection.FullName, jObject.ToString()); File.WriteAllText(collection.FullName, jObject.ToString());
} }
catch (Exception e) catch (Exception e)
{ {
Penumbra.Log.Error( Penumbra.Log.Error(
$"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}"); $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}");
} }
} }
} }
// Move the current sort order to its own file. // Move the current sort order to its own file.
private void ResettleSortOrder() private void ResettleSortOrder()
{ {
ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject<Dictionary<string, string>>() ?? ModSortOrder; ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject<Dictionary<string, string>>() ?? ModSortOrder;
var file = saveService.FileNames.OldFilesystemFile; var file = saveService.FileNames.OldFilesystemFile;
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
using var writer = new StreamWriter(stream); using var writer = new StreamWriter(stream);
using var j = new JsonTextWriter(writer); using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented; j.Formatting = Formatting.Indented;
j.WriteStartObject(); j.WriteStartObject();
j.WritePropertyName("Data"); j.WritePropertyName("Data");
j.WriteStartObject(); j.WriteStartObject();
foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key)))) foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key))))
{ {
j.WritePropertyName(mod, true); j.WritePropertyName(mod, true);
j.WriteValue(path); j.WriteValue(path);
} }
j.WriteEndObject(); j.WriteEndObject();
j.WritePropertyName("EmptyFolders"); j.WritePropertyName("EmptyFolders");
j.WriteStartArray(); j.WriteStartArray();
j.WriteEndArray(); j.WriteEndArray();
j.WriteEndObject(); j.WriteEndObject();
} }
// Move the active collections to their own file. // Move the active collections to their own file.
private void ResettleCollectionSettings() private void ResettleCollectionSettings()
{ {
CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject<string>() ?? CurrentCollection; CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject<string>() ?? CurrentCollection;
DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject<string>() ?? DefaultCollection; DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject<string>() ?? DefaultCollection;
CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject<Dictionary<string, string>>() ?? CharacterCollections; CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject<Dictionary<string, string>>() ?? CharacterCollections;
SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection,
CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>()); CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>());
} }
// Outdated saving using the Characters list. // Outdated saving using the Characters list.
private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters,
IEnumerable<(CollectionType, string)> special) IEnumerable<(CollectionType, string)> special)
{ {
var file = saveService.FileNames.ActiveCollectionsFile; var file = saveService.FileNames.ActiveCollectionsFile;
try try
{ {
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
using var writer = new StreamWriter(stream); using var writer = new StreamWriter(stream);
using var j = new JsonTextWriter(writer); using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented; j.Formatting = Formatting.Indented;
j.WriteStartObject(); j.WriteStartObject();
j.WritePropertyName(nameof(ActiveCollectionData.Default)); j.WritePropertyName(nameof(ActiveCollectionData.Default));
j.WriteValue(def); j.WriteValue(def);
j.WritePropertyName(nameof(ActiveCollectionData.Interface)); j.WritePropertyName(nameof(ActiveCollectionData.Interface));
j.WriteValue(ui); j.WriteValue(ui);
j.WritePropertyName(nameof(ActiveCollectionData.Current)); j.WritePropertyName(nameof(ActiveCollectionData.Current));
j.WriteValue(current); j.WriteValue(current);
foreach (var (type, collection) in special) foreach (var (type, collection) in special)
{ {
j.WritePropertyName(type.ToString()); j.WritePropertyName(type.ToString());
j.WriteValue(collection); j.WriteValue(collection);
} }
j.WritePropertyName("Characters"); j.WritePropertyName("Characters");
j.WriteStartObject(); j.WriteStartObject();
foreach (var (character, collection) in characters) foreach (var (character, collection) in characters)
{ {
j.WritePropertyName(character, true); j.WritePropertyName(character, true);
j.WriteValue(collection); j.WriteValue(collection);
} }
j.WriteEndObject(); j.WriteEndObject();
j.WriteEndObject(); j.WriteEndObject();
Penumbra.Log.Verbose("Active Collections saved."); Penumbra.Log.Verbose("Active Collections saved.");
} }
catch (Exception e) catch (Exception e)
{ {
Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}");
} }
} }
// Collections were introduced and the previous CurrentCollection got put into ModDirectory. // Collections were introduced and the previous CurrentCollection got put into ModDirectory.
private void Version0To1() private void Version0To1()
{ {
if (_config.Version != 0) if (_config.Version != 0)
return; return;
_config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject<string>() ?? string.Empty; _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject<string>() ?? string.Empty;
_config.Version = 1; _config.Version = 1;
ResettleCollectionJson(); ResettleCollectionJson();
} }
/// <summary> Move the previous mod configurations to a new default collection file. </summary> /// <summary> Move the previous mod configurations to a new default collection file. </summary>
private void ResettleCollectionJson() private void ResettleCollectionJson()
{ {
var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json")); var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json"));
if (!collectionJson.Exists) if (!collectionJson.Exists)
return; return;
var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName)); var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName));
if (defaultCollectionFile.Exists) if (defaultCollectionFile.Exists)
return; return;
try try
{ {
var text = File.ReadAllText(collectionJson.FullName); var text = File.ReadAllText(collectionJson.FullName);
var data = JArray.Parse(text); var data = JArray.Parse(text);
var maxPriority = ModPriority.Default; var maxPriority = ModPriority.Default;
var dict = new Dictionary<string, ModSettings.SavedSettings>(); var dict = new Dictionary<string, ModSettings.SavedSettings>();
foreach (var setting in data.Cast<JObject>()) foreach (var setting in data.Cast<JObject>())
{ {
var modName = setting["FolderName"]?.ToObject<string>()!; var modName = setting["FolderName"]?.ToObject<string>()!;
var enabled = setting["Enabled"]?.ToObject<bool>() ?? false; var enabled = setting["Enabled"]?.ToObject<bool>() ?? false;
var priority = setting["Priority"]?.ToObject<ModPriority>() ?? ModPriority.Default; var priority = setting["Priority"]?.ToObject<ModPriority>() ?? ModPriority.Default;
var settings = setting["Settings"]!.ToObject<Dictionary<string, Setting>>() var settings = setting["Settings"]!.ToObject<Dictionary<string, Setting>>()
?? setting["Conf"]!.ToObject<Dictionary<string, Setting>>(); ?? setting["Conf"]!.ToObject<Dictionary<string, Setting>>();
dict[modName] = new ModSettings.SavedSettings() dict[modName] = new ModSettings.SavedSettings
{ {
Enabled = enabled, Enabled = enabled,
Priority = priority, Priority = priority,
Settings = settings!, Settings = settings!,
}; };
maxPriority = maxPriority.Max(priority); maxPriority = maxPriority.Max(priority);
} }
InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject<bool>() ?? InvertModListOrder; InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject<bool>() ?? InvertModListOrder;
if (!InvertModListOrder) if (!InvertModListOrder)
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
var emptyStorage = new ModStorage(); var emptyStorage = new ModStorage();
// Only used for saving and immediately discarded, so the local collection id here is irrelevant. // Only used for saving and immediately discarded, so the local collection id here is irrelevant.
var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []); var collection = ModCollection.CreateFromData(saveService, emptyStorage,
saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []);
} saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
catch (Exception e) }
{ catch (Exception e)
Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}"); {
throw; Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}");
} throw;
} }
}
// Create a backup of the configuration file specifically.
private void CreateBackup() // Create a backup of the configuration file specifically.
{ private void CreateBackup()
var name = saveService.FileNames.ConfigurationFile; {
var bakName = name + ".bak"; var name = saveService.FileNames.ConfigurationFile;
try var bakName = name + ".bak";
{ try
File.Copy(name, bakName, true); {
} File.Copy(name, bakName, true);
catch (Exception e) }
{ catch (Exception e)
Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}"); {
} Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}");
} }
}
public enum SortModeV3 : byte
{ public enum SortModeV3 : byte
FoldersFirst = 0x00, {
Lexicographical = 0x01, FoldersFirst = 0x00,
InverseFoldersFirst = 0x02, Lexicographical = 0x01,
InverseLexicographical = 0x03, InverseFoldersFirst = 0x02,
FoldersLast = 0x04, InverseLexicographical = 0x03,
InverseFoldersLast = 0x05, FoldersLast = 0x04,
InternalOrder = 0x06, InverseFoldersLast = 0x05,
InternalOrderInverse = 0x07, InternalOrder = 0x06,
} InternalOrderInverse = 0x07,
} }
}

View file

@ -17,6 +17,7 @@ public sealed class FilenameService(IDalamudPluginInterface pi) : BaseFilePathPr
public readonly string FileSystemEmptyFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "empty_folders.json"); public readonly string FileSystemEmptyFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "empty_folders.json");
public readonly string FileSystemExpandedFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "expanded_folders.json"); public readonly string FileSystemExpandedFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "expanded_folders.json");
public readonly string FileSystemLockedNodes = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "locked_nodes.json"); public readonly string FileSystemLockedNodes = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "locked_nodes.json");
public readonly string FileSystemSelectedNodes = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "selected_nodes.json");
public readonly string CrashHandlerExe = public readonly string CrashHandlerExe =
Path.Combine(pi.AssemblyLocation.DirectoryName!, "Penumbra.CrashHandler.exe"); Path.Combine(pi.AssemblyLocation.DirectoryName!, "Penumbra.CrashHandler.exe");

View file

@ -8,11 +8,11 @@ using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Materials;
using FilterComboColors = Penumbra.UI.Classes.FilterComboColors;
using MouseWheelType = OtterGui.Widgets.MouseWheelType; using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.Services; namespace Penumbra.Services;
// TODO
//public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) : SimpleFilterCombo<StmKeyType>(SimpleFilterType.Text) //public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) : SimpleFilterCombo<StmKeyType>(SimpleFilterType.Text)
// where TDyePack : unmanaged, IDyePack // where TDyePack : unmanaged, IDyePack
//{ //{

View file

@ -565,7 +565,7 @@ public class ItemSwapTab : IDisposable, ITab
Im.Line.Same(); Im.Line.Same();
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero,
Colors.PressEnterWarningBg); new Rgba32(Colors.PressEnterWarningBg).Color);
if (Im.Item.Hovered()) if (Im.Item.Hovered())
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name)) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name))
.Select(i => i.Name))); .Select(i => i.Name)));
@ -626,7 +626,7 @@ public class ItemSwapTab : IDisposable, ITab
Im.Line.Same(); Im.Line.Same();
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero,
Colors.PressEnterWarningBg); new Rgba32(Colors.PressEnterWarningBg).Color);
if (Im.Item.Hovered()) if (Im.Item.Hovered())
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name)) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name))
.Select(i => i.Name))); .Select(i => i.Name)));
@ -723,7 +723,7 @@ public class ItemSwapTab : IDisposable, ITab
ModelRace.AuRa, ModelRace.AuRa,
ModelRace.Hrothgar, ModelRace.Hrothgar,
], ],
RaceEnumExtensions.ToName); ModelRaceExtensions.ToName);
} }
} }
@ -750,7 +750,7 @@ public class ItemSwapTab : IDisposable, ITab
private static void DrawSwap(Swap swap) private static void DrawSwap(Swap swap)
{ {
var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; var flags = swap.ChildSwaps.Count is 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
using var tree = ImRaii.TreeNode(SwapToString(swap), flags); using var tree = ImRaii.TreeNode(SwapToString(swap), flags);
if (!tree) if (!tree)
return; return;

View file

@ -1,10 +1,6 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Files.StainMapStructs; using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Services; using Penumbra.Services;
@ -30,7 +26,7 @@ public partial class MtrlTab
var framePadding = Im.Style.FramePadding; var framePadding = Im.Style.FramePadding;
var buttonWidth = (Im.ContentRegion.Available.X - itemSpacing * 7.0f) * 0.125f; var buttonWidth = (Im.ContentRegion.Available.X - itemSpacing * 7.0f) * 0.125f;
var frameHeight = Im.Style.FrameHeight; var frameHeight = Im.Style.FrameHeight;
var highlighterSize = ImEx.Icon.CalculateSize(FontAwesomeIcon.Crosshairs.Icon()) + framePadding * 2.0f; var highlighterSize = ImEx.Icon.CalculateSize(LunaStyle.OnHoverIcon) + framePadding * 2.0f;
using var font = Im.Font.PushMono(); using var font = Im.Font.PushMono();
using var alignment = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f)); using var alignment = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f));
@ -56,19 +52,19 @@ public partial class MtrlTab
CtBlendRect( CtBlendRect(
rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 }, rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 },
rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 }, rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 },
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor)), PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor),
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor)) PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor)
); );
CtBlendRect( CtBlendRect(
rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing }, rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing },
rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing }, rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing },
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor)), PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor),
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor)) PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor)
); );
CtBlendRect( CtBlendRect(
rcMin with { X = rcMax.X - frameHeight }, rcMax, rcMin with { X = rcMax.X - frameHeight }, rcMax,
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor)), PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor),
ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor)) PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor)
); );
if (j < 7) if (j < 7)
Im.Line.Same(); Im.Line.Same();
@ -95,95 +91,95 @@ public partial class MtrlTab
var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key;
var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA);
var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
{ {
using (ImUtf8.PushId("RowHeaderA"u8)) using (Im.Id.Push("RowHeaderA"u8))
{ {
retA |= DrawRowHeader(rowAIdx, disabled); retA |= DrawRowHeader(rowAIdx, disabled);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("RowHeaderB"u8)) using (Im.Id.Push("RowHeaderB"u8))
{ {
retB |= DrawRowHeader(rowBIdx, disabled); retB |= DrawRowHeader(rowBIdx, disabled);
} }
} }
DrawHeader(" Colors"u8); DrawHeader(" Colors"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
{ {
using var dis = ImRaii.Disabled(disabled); using var dis = Im.Disabled(disabled);
using (ImUtf8.PushId("ColorsA"u8)) using (Im.Id.Push("ColorsA"u8))
{ {
retA |= DrawColors(table, dyeTable, dyePackA, rowAIdx); retA |= DrawColors(table, dyeTable, dyePackA, rowAIdx);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("ColorsB"u8)) using (Im.Id.Push("ColorsB"u8))
{ {
retB |= DrawColors(table, dyeTable, dyePackB, rowBIdx); retB |= DrawColors(table, dyeTable, dyePackB, rowBIdx);
} }
} }
DrawHeader(" Physical Parameters"u8); DrawHeader(" Physical Parameters"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
{ {
using var dis = ImRaii.Disabled(disabled); using var dis = Im.Disabled(disabled);
using (ImUtf8.PushId("PbrA"u8)) using (Im.Id.Push("PbrA"u8))
{ {
retA |= DrawPbr(table, dyeTable, dyePackA, rowAIdx); retA |= DrawPbr(table, dyeTable, dyePackA, rowAIdx);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("PbrB"u8)) using (Im.Id.Push("PbrB"u8))
{ {
retB |= DrawPbr(table, dyeTable, dyePackB, rowBIdx); retB |= DrawPbr(table, dyeTable, dyePackB, rowBIdx);
} }
} }
DrawHeader(" Sheen Layer Parameters"u8); DrawHeader(" Sheen Layer Parameters"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
{ {
using var dis = ImRaii.Disabled(disabled); using var dis = Im.Disabled(disabled);
using (ImUtf8.PushId("SheenA"u8)) using (Im.Id.Push("SheenA"u8))
{ {
retA |= DrawSheen(table, dyeTable, dyePackA, rowAIdx); retA |= DrawSheen(table, dyeTable, dyePackA, rowAIdx);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("SheenB"u8)) using (Im.Id.Push("SheenB"u8))
{ {
retB |= DrawSheen(table, dyeTable, dyePackB, rowBIdx); retB |= DrawSheen(table, dyeTable, dyePackB, rowBIdx);
} }
} }
DrawHeader(" Pair Blending"u8); DrawHeader(" Pair Blending"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
{ {
using var dis = ImRaii.Disabled(disabled); using var dis = Im.Disabled(disabled);
using (ImUtf8.PushId("BlendingA"u8)) using (Im.Id.Push("BlendingA"u8))
{ {
retA |= DrawBlending(table, dyeTable, dyePackA, rowAIdx); retA |= DrawBlending(table, dyeTable, dyePackA, rowAIdx);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("BlendingB"u8)) using (Im.Id.Push("BlendingB"u8))
{ {
retB |= DrawBlending(table, dyeTable, dyePackB, rowBIdx); retB |= DrawBlending(table, dyeTable, dyePackB, rowBIdx);
} }
} }
DrawHeader(" Material Template"u8); DrawHeader(" Material Template"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
{ {
using var dis = ImRaii.Disabled(disabled); using var dis = Im.Disabled(disabled);
using (ImUtf8.PushId("TemplateA"u8)) using (Im.Id.Push("TemplateA"u8))
{ {
retA |= DrawTemplate(table, dyeTable, dyePackA, rowAIdx); retA |= DrawTemplate(table, dyeTable, dyePackA, rowAIdx);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("TemplateB"u8)) using (Im.Id.Push("TemplateB"u8))
{ {
retB |= DrawTemplate(table, dyeTable, dyePackB, rowBIdx); retB |= DrawTemplate(table, dyeTable, dyePackB, rowBIdx);
} }
@ -192,31 +188,31 @@ public partial class MtrlTab
if (dyeTable != null) if (dyeTable != null)
{ {
DrawHeader(" Dye Properties"u8); DrawHeader(" Dye Properties"u8);
using var columns = ImUtf8.Columns(2, "ColorTable"u8); using var columns = Im.Columns(2, "ColorTable"u8);
using var dis = ImRaii.Disabled(disabled); using var dis = Im.Disabled(disabled);
using (ImUtf8.PushId("DyeA"u8)) using (Im.Id.Push("DyeA"u8))
{ {
retA |= DrawDye(dyeTable, dyePackA, rowAIdx); retA |= DrawDye(dyeTable, dyePackA, rowAIdx);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("DyeB"u8)) using (Im.Id.Push("DyeB"u8))
{ {
retB |= DrawDye(dyeTable, dyePackB, rowBIdx); retB |= DrawDye(dyeTable, dyePackB, rowBIdx);
} }
} }
DrawHeader(" Further Content"u8); DrawHeader(" Further Content"u8);
using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
{ {
using var dis = ImRaii.Disabled(disabled); using var dis = Im.Disabled(disabled);
using (ImUtf8.PushId("FurtherA"u8)) using (Im.Id.Push("FurtherA"u8))
{ {
retA |= DrawFurther(table, dyeTable, dyePackA, rowAIdx); retA |= DrawFurther(table, dyeTable, dyePackA, rowAIdx);
} }
columns.Next(); columns.Next();
using (ImUtf8.PushId("FurtherB"u8)) using (Im.Id.Push("FurtherB"u8))
{ {
retB |= DrawFurther(table, dyeTable, dyePackB, rowBIdx); retB |= DrawFurther(table, dyeTable, dyePackB, rowBIdx);
} }
@ -235,7 +231,7 @@ public partial class MtrlTab
{ {
var headerColor = Im.Style[ImGuiColor.Header]; var headerColor = Im.Style[ImGuiColor.Header];
using var _ = ImGuiColor.HeaderHovered.Push(headerColor).Push(ImGuiColor.HeaderActive, headerColor); using var _ = ImGuiColor.HeaderHovered.Push(headerColor).Push(ImGuiColor.HeaderActive, headerColor);
ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); Im.Tree.Header(label, TreeNodeFlags.Leaf);
} }
private bool DrawRowHeader(int rowIdx, bool disabled) private bool DrawRowHeader(int rowIdx, bool disabled)
@ -276,7 +272,7 @@ public partial class MtrlTab
ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor, ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor,
c => table[rowIdx].SpecularColor = c); c => table[rowIdx].SpecularColor = c);
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor,
@ -287,7 +283,7 @@ public partial class MtrlTab
ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor, ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor,
c => table[rowIdx].EmissiveColor = c); c => table[rowIdx].EmissiveColor = c);
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor,
@ -308,7 +304,7 @@ public partial class MtrlTab
- Im.Style.FrameHeight - Im.Style.FrameHeight
- scalarSize; - scalarSize;
var isRowB = (rowIdx & 1) != 0; var isRowB = (rowIdx & 1) is not 0;
var ret = false; var ret = false;
ref var row = ref table[rowIdx]; ref var row = ref table[rowIdx];
@ -317,7 +313,7 @@ public partial class MtrlTab
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f,
v => table[rowIdx].Anisotropy = v); v => table[rowIdx].Anisotropy = v);
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8,
@ -347,37 +343,37 @@ public partial class MtrlTab
ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f,
v => table[rowIdx].ShaderId = v); v => table[rowIdx].ShaderId = v);
ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f); Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f);
ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false, ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false,
v => table[rowIdx].SphereMapIndex = v); v => table[rowIdx].SphereMapIndex = v);
Im.Line.SameInner(); Im.Line.SameInner();
ImUtf8.Text("Sphere Map"u8); Im.Text("Sphere Map"u8);
if (dyeTable != null) if (dyeTable is not null)
{ {
var textRectMin = ImGui.GetItemRectMin(); var textRectMin = Im.Item.UpperLeftCorner;
var textRectMax = ImGui.GetItemRectMax(); var textRectMax = Im.Item.LowerRightCorner;
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
var cursor = ImGui.GetCursorScreenPos(); var cursor = Im.Cursor.ScreenPosition;
ImGui.SetCursorScreenPos(cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - Im.Style.FrameHeight * 0.5f }); Im.Cursor.ScreenPosition = cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - Im.Style.FrameHeight * 0.5f };
ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex, ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex,
b => dyeTable[rowIdx].SphereMapIndex = b); b => dyeTable[rowIdx].SphereMapIndex = b);
Im.Line.SameInner(); Im.Line.SameInner();
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); Im.Cursor.ScreenPosition = Im.Cursor.ScreenPosition with { Y = cursor.Y };
Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f); Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f);
using var dis = ImRaii.Disabled(); using var dis = Im.Disabled();
CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false,
Nop); Nop);
} }
ImGui.Dummy(new Vector2(64.0f, 0.0f)); Im.Dummy(new Vector2(64.0f, 0.0f));
Im.Line.Same(); Im.Line.Same();
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f,
HalfMaxValue * 100.0f, 1.0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f));
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask, ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask,
@ -387,25 +383,24 @@ public partial class MtrlTab
CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8); CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8);
} }
ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
var leftLineHeight = 64.0f + Im.Style.FramePadding.Y * 2.0f; var leftLineHeight = 64.0f + Im.Style.FramePadding.Y * 2.0f;
var rightLineHeight = 3.0f * Im.Style.FrameHeight + 2.0f * Im.Style.ItemSpacing.Y; var rightLineHeight = 3.0f * Im.Style.FrameHeight + 2.0f * Im.Style.ItemSpacing.Y;
var lineHeight = Math.Max(leftLineHeight, rightLineHeight); var lineHeight = Math.Max(leftLineHeight, rightLineHeight);
var cursorPos = ImGui.GetCursorScreenPos(); var cursorPos = Im.Cursor.ScreenPosition;
ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); Im.Cursor.ScreenPosition = cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f);
Im.Item.SetNextWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); Im.Item.SetNextWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f);
ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false,
v => table[rowIdx].TileIndex = v); v => table[rowIdx].TileIndex = v);
Im.Line.SameInner(); Im.Line.SameInner();
ImUtf8.Text("Tile"u8); Im.Text("Tile"u8);
Im.Line.Same(subColWidth); Im.Line.Same(subColWidth);
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f }); Im.Cursor.ScreenPosition = Im.Cursor.ScreenPosition with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f };
using (ImUtf8.Child("###TileProperties"u8, using (Im.Child.Begin("###TileProperties"u8, Im.ContentRegion.Available with { Y = float.Lerp(rightLineHeight, lineHeight, 0.5f) }))
new Vector2(Im.ContentRegion.Available.X, float.Lerp(rightLineHeight, lineHeight, 0.5f))))
{ {
ImGui.Dummy(new Vector2(scalarSize, 0.0f)); Im.Dummy(new Vector2(scalarSize, 0.0f));
Im.Line.SameInner(); Im.Line.SameInner();
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f, ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f,
@ -414,9 +409,8 @@ public partial class MtrlTab
ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true,
m => table[rowIdx].TileTransform = m); m => table[rowIdx].TileTransform = m);
Im.Line.SameInner(); Im.Line.SameInner();
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() Im.Cursor.ScreenPosition -= new Vector2(0.0f, (Im.Style.FrameHeight + Im.Style.ItemSpacing.Y) * 0.5f);
- new Vector2(0.0f, (Im.Style.FrameHeight + Im.Style.ItemSpacing.Y) * 0.5f)); Im.Text("Tile Transform"u8);
ImUtf8.Text("Tile Transform"u8);
} }
return ret; return ret;
@ -440,7 +434,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f,
1.0f, 1.0f,
v => table[rowIdx].Roughness = (Half)(v * 0.01f)); v => table[rowIdx].Roughness = (Half)(v * 0.01f));
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness, ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness,
@ -455,7 +449,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f,
1.0f, 1.0f,
v => table[rowIdx].Metalness = (Half)(v * 0.01f)); v => table[rowIdx].Metalness = (Half)(v * 0.01f));
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(subColWidth + dyeOffset); Im.Line.Same(subColWidth + dyeOffset);
ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness,
@ -485,7 +479,7 @@ public partial class MtrlTab
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SheenRate = (Half)(v * 0.01f)); v => table[rowIdx].SheenRate = (Half)(v * 0.01f));
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate, ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate,
@ -500,7 +494,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f,
HalfMaxValue * 100.0f, 1.0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f));
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(subColWidth + dyeOffset); Im.Line.Same(subColWidth + dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate,
@ -514,7 +508,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue,
100.0f / HalfEpsilon, 1.0f, 100.0f / HalfEpsilon, 1.0f,
v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); v => table[rowIdx].SheenAperture = (Half)(100.0f / v));
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture, ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture,
@ -545,7 +539,7 @@ public partial class MtrlTab
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar11 = v); v => table[rowIdx].Scalar11 = v);
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Line.Same(dyeOffset); Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3, ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3,
@ -555,7 +549,7 @@ public partial class MtrlTab
CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8); CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8);
} }
ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
@ -594,7 +588,7 @@ public partial class MtrlTab
private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx) private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx)
{ {
var scalarSize = ColorTableScalarSize * Im.Style.GlobalScale; var scalarSize = ColorTableScalarSize * Im.Style.GlobalScale;
var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + Im.Style.FramePadding.X * 2.0f; var applyButtonWidth = Im.Font.CalculateSize("Apply Preview Dye"u8).X + Im.Style.FramePadding.X * 2.0f;
var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth); var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth);
var ret = false; var ret = false;
@ -614,10 +608,10 @@ public partial class MtrlTab
} }
Im.Line.SameInner(); Im.Line.SameInner();
ImUtf8.Text("Dye Template"u8); Im.Text("Dye Template"u8);
Im.Line.Same(Im.ContentRegion.Available.X - applyButtonWidth + Im.Style.ItemSpacing.X); Im.Line.Same(Im.ContentRegion.Available.X - applyButtonWidth + Im.Style.ItemSpacing.X);
using var dis = ImRaii.Disabled(!dyePack.HasValue); using var dis = Im.Disabled(!dyePack.HasValue);
if (ImUtf8.Button("Apply Preview Dye"u8)) if (Im.Button("Apply Preview Dye"u8))
ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [
_stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo1.CurrentSelection.Key,
_stainService.StainCombo2.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key,
@ -626,13 +620,13 @@ public partial class MtrlTab
return ret; return ret;
} }
private static void CenteredTextInRest(string text) private static void CenteredTextInRest(Utf8StringHandler<TextStringHandlerBuffer> text)
=> AlignedTextInRest(text, 0.5f); => AlignedTextInRest(ref text, 0.5f);
private static void AlignedTextInRest(string text, float alignment) private static void AlignedTextInRest(ref Utf8StringHandler<TextStringHandlerBuffer> text, float alignment)
{ {
var width = ImGui.CalcTextSize(text).X; var width = Im.Font.CalculateSize(text).X;
ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + new Vector2((Im.ContentRegion.Available.X - width) * alignment, 0.0f)); Im.Cursor.ScreenPosition += new Vector2((Im.ContentRegion.Available.X - width) * alignment, 0.0f);
Im.Text(text); Im.Text(text);
} }

View file

@ -1,6 +1,5 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility;
using ImSharp; using ImSharp;
using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
@ -26,8 +25,8 @@ public partial class MtrlTab
if (!_shpkLoading && !TextureIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) if (!_shpkLoading && !TextureIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null)
return false; return false;
ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
if (!ImUtf8.CollapsingHeader("Color Table"u8, ImGuiTreeNodeFlags.DefaultOpen)) if (!Im.Tree.Header("Color Table"u8, TreeNodeFlags.DefaultOpen))
return false; return false;
ColorTableCopyAllClipboardButton(); ColorTableCopyAllClipboardButton();
@ -91,7 +90,7 @@ public partial class MtrlTab
{ {
var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection;
var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection;
var tt = dyeId1 == 0 && dyeId2 == 0 var tt = dyeId1 is 0 && dyeId2 is 0
? "Select a preview dye first."u8 ? "Select a preview dye first."u8
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8; : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8;
if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0)) if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0))
@ -303,31 +302,31 @@ public partial class MtrlTab
CancelColorTableHighlight(); CancelColorTableHighlight();
} }
private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, Rgba32 topColor, Rgba32 bottomColor)
{ {
var frameRounding = Im.Style.FrameRounding; var frameRounding = Im.Style.FrameRounding;
var frameThickness = Im.Style.FrameBorderThickness; var frameThickness = Im.Style.FrameBorderThickness;
var borderColor = ImGuiColor.Border.Get(); var borderColor = ImGuiColor.Border.Get();
var drawList = ImGui.GetWindowDrawList(); var drawList = Im.Window.DrawList.Shape;
if (topColor == bottomColor) if (topColor == bottomColor)
{ {
drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); drawList.RectangleFilled(rcMin, rcMax, topColor, frameRounding);
} }
else else
{ {
drawList.AddRectFilled( drawList.RectangleFilled(
rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight); topColor, frameRounding, ImDrawFlagsRectangle.RoundCornersTop);
drawList.AddRectFilledMultiColor( drawList.RectangleMulticolor(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) },
topColor, topColor, bottomColor, bottomColor); topColor, topColor, bottomColor, bottomColor);
drawList.AddRectFilled( drawList.RectangleFilled(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax,
bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); bottomColor, frameRounding, ImDrawFlagsRectangle.RoundCornersTop);
} }
drawList.AddRect(rcMin, rcMax, borderColor.Color, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); drawList.Rectangle(rcMin, rcMax, borderColor.Color, frameRounding, default, frameThickness);
} }
private static bool CtColorPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, HalfColor current, Action<HalfColor> setter, private static bool CtColorPicker(ReadOnlySpan<byte> label, ReadOnlySpan<byte> description, HalfColor current, Action<HalfColor> setter,

View file

@ -77,7 +77,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable
if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8,
"Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8,
new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) new Vector2(-0.1f, 0), false, 0, new Rgba32(Colors.PressEnterWarningBg).Color))
return false; return false;
Mtrl.MigrateToDawntrail(); Mtrl.MigrateToDawntrail();

View file

@ -4,6 +4,7 @@ using Penumbra.GameData.Interop;
using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.ResourceTree; using Penumbra.Interop.ResourceTree;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow.Materials;

View file

@ -91,7 +91,7 @@ public partial class ModEditWindow
if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8,
"Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8,
new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) new Vector2(-0.1f, 0), false, 0, new Rgba32(Colors.PressEnterWarningBg).Color))
return; return;
tab.Mdl.ConvertV5ToV6(); tab.Mdl.ConvertV5ToV6();
@ -198,7 +198,7 @@ public partial class ModEditWindow
var size = new Vector2(Im.ContentRegion.Available.X, 0); var size = new Vector2(Im.ContentRegion.Available.X, 0);
using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle,
borderColor: Colors.RegexWarningBorder); borderColor: new Rgba32(Colors.RegexWarningBorder).Color);
var spaceAvail = Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X - 100; var spaceAvail = Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X - 100;
foreach (var (index, exception) in tab.IoExceptions.Index()) foreach (var (index, exception) in tab.IoExceptions.Index())
@ -304,7 +304,7 @@ public partial class ModEditWindow
private void DrawDocumentationLink(string address) private void DrawDocumentationLink(string address)
{ {
const string text = "Documentation →"; var text = "Documentation →"u8;
var framePadding = Im.Style.FramePadding; var framePadding = Im.Style.FramePadding;
var width = ImGui.CalcTextSize(text).X + framePadding.X * 2; var width = ImGui.CalcTextSize(text).X + framePadding.X * 2;

View file

@ -8,6 +8,7 @@ using Penumbra.Mods;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;

View file

@ -6,6 +6,7 @@ using Penumbra.GameData.Files.ShaderStructs;
using Penumbra.GameData.Interop; using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Materials;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;

View file

@ -56,8 +56,8 @@ public partial class ModEditWindow
{ {
TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8, TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8,
"Can import game paths as well as your own files."u8, Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); "Can import game paths as well as your own files."u8, Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath);
if (_textureSelectCombo.Draw("##combo", if (_textureSelectCombo.Draw("##combo"u8,
"Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, "Select the textures included in this mod on your drive or the ones they replace from the game files."u8, tex.Path,
Mod.ModPath.FullName.Length + 1, out var newPath) Mod.ModPath.FullName.Length + 1, out var newPath)
&& newPath != tex.Path) && newPath != tex.Path)
tex.Load(_textures, newPath); tex.Load(_textures, newPath);
@ -199,7 +199,7 @@ public partial class ModEditWindow
case TaskStatus.WaitingForActivation: case TaskStatus.WaitingForActivation:
case TaskStatus.WaitingToRun: case TaskStatus.WaitingToRun:
case TaskStatus.Running: case TaskStatus.Running:
ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg); ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, new Rgba32(Colors.PressEnterWarningBg).Color);
break; break;
case TaskStatus.Canceled: case TaskStatus.Canceled:
case TaskStatus.Faulted: case TaskStatus.Faulted:

View file

@ -407,9 +407,9 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
var width = ImGui.CalcTextSize("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN ").X; var width = ImGui.CalcTextSize("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN ").X;
table.SetupColumn("file"u8, TableColumnFlags.WidthStretch); table.SetupColumn("file"u8, TableColumnFlags.WidthStretch);
table.SetupColumn("size", TableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN "u8).X); table.SetupColumn("size"u8, TableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN "u8).X);
table.SetupColumn("hash"u8, TableColumnFlags.WidthFixed, table.SetupColumn("hash"u8, TableColumnFlags.WidthFixed,
ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X); ImGui.GetWindowWidth() > 2 * width ? width : Im.Font.CalculateSize("NNNNNNNN... "u8).X);
foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1)) foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1))
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();

View file

@ -11,6 +11,7 @@ using Penumbra.Mods.Editor;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Materials;
using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.AdvancedWindow.Meta;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;

View file

@ -1,17 +1,12 @@
using Dalamud.Interface.Utility;
using Dalamud.Bindings.ImGui;
using ImSharp; using ImSharp;
using OtterGui; using Luna;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Luna.IUiService public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : IUiService
{ {
private string _newModName = string.Empty; private string _newModName = string.Empty;
@ -20,25 +15,24 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
if (modMerger.MergeFromMod == null) if (modMerger.MergeFromMod == null)
return; return;
using var tab = ImRaii.TabItem("Merge Mods"); using var tab = Im.TabBar.BeginItem("Merge Mods"u8);
if (!tab) if (!tab)
return; return;
ImGui.Dummy(Vector2.One); Im.Dummy(Vector2.Zero);
var size = 550 * Im.Style.GlobalScale; var size = 550 * Im.Style.GlobalScale;
DrawMergeInto(size); DrawMergeInto(size);
Im.Line.Same(); Im.Line.Same();
DrawMergeIntoDesc(); DrawMergeIntoDesc();
ImGui.Dummy(Vector2.One); Im.Dummy(Vector2.Zero);
Im.Separator(); Im.Separator();
ImGui.Dummy(Vector2.One); Im.Dummy(Vector2.Zero);
DrawSplitOff(size); DrawSplitOff(size);
Im.Line.Same(); Im.Line.Same();
DrawSplitOffDesc(); DrawSplitOffDesc();
DrawError(); DrawError();
DrawWarnings(); DrawWarnings();
} }
@ -47,13 +41,13 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
{ {
using var bigGroup = Im.Group(); using var bigGroup = Im.Group();
var minComboSize = 300 * Im.Style.GlobalScale; var minComboSize = 300 * Im.Style.GlobalScale;
var textSize = ImUtf8.CalcTextSize($"Merge {modMerger.MergeFromMod!.Name} into ").X; var textSize = Im.Font.CalculateSize($"Merge {modMerger.MergeFromMod!.Name} into ").X;
ImGui.AlignTextToFramePadding(); Im.Cursor.FrameAlign();
using (Im.Group()) using (Im.Group())
{ {
ImUtf8.Text("Merge "u8); Im.Text("Merge "u8);
Im.Line.Same(0, 0); Im.Line.Same(0, 0);
if (size - textSize < minComboSize) if (size - textSize < minComboSize)
{ {
@ -66,56 +60,56 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
} }
Im.Line.Same(0, 0); Im.Line.Same(0, 0);
ImUtf8.Text(" into"u8); Im.Text(" into"u8);
} }
Im.Line.Same(); Im.Line.Same();
DrawCombo(size - ImGui.GetItemRectSize().X - Im.Style.ItemSpacing.X); DrawCombo(size - Im.Item.Size.X - Im.Style.ItemSpacing.X);
using (Im.Group()) using (Im.Group())
{ {
using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions); using var disabled = Im.Disabled(modMerger.MergeFromMod.HasOptions);
var buttonWidth = (size - Im.Style.ItemSpacing.X) / 2; var buttonWidth = (size - Im.Style.ItemSpacing.X) / 2;
var group = modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == modMerger.OptionGroupName); var group = modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == modMerger.OptionGroupName);
var color = group != null || modMerger.OptionGroupName.Length is 0 && modMerger.OptionName.Length is 0 var color = group != null || modMerger.OptionGroupName.Length is 0 && modMerger.OptionName.Length is 0
? Colors.PressEnterWarningBg ? Colors.PressEnterWarningBg
: Colors.DiscordColor; : LunaStyle.DiscordColor;
using var style = ImStyleBorder.Frame.Push(color); using var style = ImStyleBorder.Frame.Push(color);
Im.Item.SetNextWidth(buttonWidth); Im.Item.SetNextWidth(buttonWidth);
ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref modMerger.OptionGroupName, 64); Im.Input.Text("##optionGroupInput"u8, ref modMerger.OptionGroupName, "Target Option Group"u8);
ImGuiUtil.HoverTooltip( Im.Tooltip.OnHover(
"The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n" "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n"u8
+ "A red border indicates an existing option group, a blue border indicates a new one."); + "A red border indicates an existing option group, a blue border indicates a new one."u8);
Im.Line.Same(); Im.Line.Same();
color = color == Colors.DiscordColor color = color == LunaStyle.DiscordColor
? Colors.DiscordColor ? LunaStyle.DiscordColor
: group == null || group.Options.Any(o => o.Name == modMerger.OptionName) : group == null || group.Options.Any(o => o.Name == modMerger.OptionName)
? Colors.PressEnterWarningBg ? Colors.PressEnterWarningBg
: Colors.DiscordColor; : LunaStyle.DiscordColor;
style.Push(ImGuiColor.Border, color); style.Push(ImGuiColor.Border, color);
Im.Item.SetNextWidth(buttonWidth); Im.Item.SetNextWidth(buttonWidth);
ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref modMerger.OptionName, 64); Im.Input.Text("##optionInput"u8, ref modMerger.OptionName, "Target Option Name"u8);
ImGuiUtil.HoverTooltip( Im.Tooltip.OnHover(
"The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n" "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n"u8
+ "A red border indicates an existing option, a blue border indicates a new one."); + "A red border indicates an existing option, a blue border indicates a new one."u8);
} }
if (modMerger.MergeFromMod.HasOptions) if (modMerger.MergeFromMod.HasOptions)
Im.Tooltip.OnHover("You can only specify a target option if the source mod has no true options itself."u8, Im.Tooltip.OnHover("You can only specify a target option if the source mod has no true options itself."u8,
HoveredFlags.AllowWhenDisabled); HoveredFlags.AllowWhenDisabled);
if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0), if (ImEx.Button("Merge"u8, new Vector2(size, 0),
modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !modMerger.CanMerge)) modMerger.CanMerge ? StringU8.Empty : "Please select a target mod different from the current mod."u8, !modMerger.CanMerge))
modMerger.Merge(); modMerger.Merge();
} }
private void DrawMergeIntoDesc() private void DrawMergeIntoDesc()
{ {
ImGuiUtil.TextWrapped(modMerger.MergeFromMod!.HasOptions Im.TextWrapped(modMerger.MergeFromMod!.HasOptions
? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break." ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break."u8
: "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."); : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."u8);
} }
private void DrawCombo(float width) private void DrawCombo(float width)
@ -129,37 +123,37 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
{ {
using var group = Im.Group(); using var group = Im.Group();
Im.Item.SetNextWidth(size); Im.Item.SetNextWidth(size);
ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); Im.Input.Text("##newModInput"u8, ref _newModName, "New Mod Name..."u8);
ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique."); Im.Tooltip.OnHover("Choose a name for the newly created mod. This does not need to be unique."u8);
var tt = _newModName.Length == 0 var tt = _newModName.Length == 0
? "Please enter a name for the newly created mod first." ? "Please enter a name for the newly created mod first."u8
: modMerger.SelectedOptions.Count == 0 : modMerger.SelectedOptions.Count == 0
? "Please select at least one option to split off." ? "Please select at least one option to split off."u8
: string.Empty; : StringU8.Empty;
var buttonText = if (ImEx.Button(
$"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff"; $"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff",
if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0)) new Vector2(size, 0), tt, tt.Length > 0))
modMerger.SplitIntoMod(_newModName); modMerger.SplitIntoMod(_newModName);
ImGui.Dummy(Vector2.One); Im.Dummy(Vector2.One);
var buttonSize = new Vector2((size - 2 * Im.Style.ItemSpacing.X) / 3, 0); var buttonSize = new Vector2((size - 2 * Im.Style.ItemSpacing.X) / 3, 0);
if (ImGui.Button("Select All", buttonSize)) if (Im.Button("Select All"u8, buttonSize))
modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers); modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers);
Im.Line.Same(); Im.Line.Same();
if (ImGui.Button("Unselect All", buttonSize)) if (Im.Button("Unselect All"u8, buttonSize))
modMerger.SelectedOptions.Clear(); modMerger.SelectedOptions.Clear();
Im.Line.Same(); Im.Line.Same();
if (ImGui.Button("Invert Selection", buttonSize)) if (Im.Button("Invert Selection"u8, buttonSize))
modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers); modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers);
DrawOptionTable(size); DrawOptionTable(size);
} }
private void DrawSplitOffDesc() private static void DrawSplitOffDesc()
{ {
ImGuiUtil.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n" Im.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n"u8
+ "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n" + "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n"u8
+ "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n" + "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n"u8
+ "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty."); + "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty."u8);
} }
private void DrawOptionTable(float size) private void DrawOptionTable(float size)
@ -186,48 +180,47 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
table.SetupColumn("#Files"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale); table.SetupColumn("#Files"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale);
table.SetupColumn("#Swaps"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale); table.SetupColumn("#Swaps"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale);
table.SetupColumn("#Manips"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale); table.SetupColumn("#Manips"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale);
ImGui.TableHeadersRow(); table.HeaderRow();
foreach (var (idx, option) in options.Index()) foreach (var (idx, option) in options.Index())
{ {
using var id = ImRaii.PushId(idx); using var id = Im.Id.Push(idx);
var selected = modMerger.SelectedOptions.Contains(option); var selected = modMerger.SelectedOptions.Contains(option);
ImGui.TableNextColumn(); table.NextColumn();
if (ImGui.Checkbox("##check", ref selected)) if (Im.Checkbox("##check"u8, ref selected))
Handle(option, selected); Handle(option, selected);
if (option.Group is not { } group) if (option.Group is not { } group)
{ {
ImGuiUtil.DrawTableColumn(option.GetFullName()); table.DrawColumn(option.GetFullName());
ImGui.TableNextColumn(); table.NextColumn();
} }
else else
{ {
ImGuiUtil.DrawTableColumn(option.GetName()); table.DrawColumn(option.GetName());
table.NextColumn();
ImGui.TableNextColumn(); Im.Selectable(group.Name);
ImGui.Selectable(group.Name, false); using var popup = Im.Popup.BeginContextItem("##groupContext"u8);
if (ImGui.BeginPopupContextItem("##groupContext")) if (popup)
{ {
if (ImGui.MenuItem("Select All")) if (Im.Menu.Item("Select All"u8))
// ReSharper disable once PossibleMultipleEnumeration // ReSharper disable once PossibleMultipleEnumeration
foreach (var opt in group.DataContainers) foreach (var opt in group.DataContainers)
Handle(opt, true); Handle(opt, true);
if (ImGui.MenuItem("Unselect All")) if (Im.Menu.Item("Unselect All"u8))
// ReSharper disable once PossibleMultipleEnumeration // ReSharper disable once PossibleMultipleEnumeration
foreach (var opt in group.DataContainers) foreach (var opt in group.DataContainers)
Handle(opt, false); Handle(opt, false);
ImGui.EndPopup();
} }
} }
ImGui.TableNextColumn(); table.NextColumn();
ImGuiUtil.RightAlign(option.Files.Count.ToString(), 3 * Im.Style.GlobalScale); ImEx.TextRightAligned($"{option.Files.Count}", 3 * Im.Style.GlobalScale);
ImGui.TableNextColumn(); table.NextColumn();
ImGuiUtil.RightAlign(option.FileSwaps.Count.ToString(), 3 * Im.Style.GlobalScale); ImEx.TextRightAligned($"{option.FileSwaps.Count}", 3 * Im.Style.GlobalScale);
ImGui.TableNextColumn(); table.NextColumn();
ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * Im.Style.GlobalScale); ImEx.TextRightAligned($"{option.Manipulations.Count}", 3 * Im.Style.GlobalScale);
continue; continue;
void Handle(IModDataContainer option2, bool selected2) void Handle(IModDataContainer option2, bool selected2)
@ -246,15 +239,15 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
return; return;
Im.Separator(); Im.Separator();
ImGui.Dummy(Vector2.One); Im.Dummy(Vector2.One);
using var color = ImGuiColor.Text.Push(Colors.TutorialBorder); using var color = ImGuiColor.Text.Push(Colors.TutorialBorder);
foreach (var warning in modMerger.Warnings.SkipLast(1)) foreach (var warning in modMerger.Warnings.SkipLast(1))
{ {
ImGuiUtil.TextWrapped(warning); Im.TextWrapped(warning);
Im.Separator(); Im.Separator();
} }
ImGuiUtil.TextWrapped(modMerger.Warnings[^1]); Im.TextWrapped(modMerger.Warnings[^1]);
} }
private void DrawError() private void DrawError()
@ -263,8 +256,8 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
return; return;
Im.Separator(); Im.Separator();
ImGui.Dummy(Vector2.One); Im.Dummy(Vector2.One);
using var color = ImGuiColor.Text.Push(Colors.RegexWarningBorder); using var color = ImGuiColor.Text.Push(Colors.RegexWarningBorder);
ImGuiUtil.TextWrapped(modMerger.Error.ToString()); Im.TextWrapped(modMerger.Error.ToString());
} }
} }

View file

@ -17,7 +17,7 @@ public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window)
protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight,
ImGuiComboFlags flags) ImGuiComboFlags flags)
{ {
_border.PushBorder(ImStyleBorder.Frame, ColorId.FolderLine.Value()); _border.Push(ImStyleBorder.Frame, ColorId.FolderLine.Value());
base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags);
_border.Dispose(); _border.Dispose();
} }

View file

@ -60,7 +60,7 @@ public class ResourceTreeViewer(
if (!_task.IsCompleted) if (!_task.IsCompleted)
{ {
Im.Line.New(); Im.Line.New();
Im.Text("Calculating character list..."); Im.Text("Calculating character list..."u8);
} }
else if (_task.Exception != null) else if (_task.Exception != null)
{ {

View file

@ -3,6 +3,7 @@ using Luna;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceTree; using Penumbra.Interop.ResourceTree;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;

View file

@ -9,10 +9,9 @@ using Luna;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes;
using MouseButton = Penumbra.Api.Enums.MouseButton; using MouseButton = Penumbra.Api.Enums.MouseButton;
namespace Penumbra.UI; namespace Penumbra.UI.Classes;
public class ChangedItemDrawer : IDisposable, IUiService public class ChangedItemDrawer : IDisposable, IUiService
{ {
@ -119,7 +118,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
var ret = leftClicked ? MouseButton.Left : MouseButton.None; var ret = leftClicked ? MouseButton.Left : MouseButton.None;
ret = Im.Item.RightClicked() ? MouseButton.Right : ret; ret = Im.Item.RightClicked() ? MouseButton.Right : ret;
ret = Im.Item.MiddleClicked() ? MouseButton.Middle : ret; ret = Im.Item.MiddleClicked() ? MouseButton.Middle : ret;
if (ret != MouseButton.None) if (ret is not MouseButton.None)
_communicator.ChangedItemClick.Invoke(new ChangedItemClick.Arguments(ret, data)); _communicator.ChangedItemClick.Invoke(new ChangedItemClick.Arguments(ret, data));
if (!Im.Item.Hovered()) if (!Im.Item.Hovered())
return; return;
@ -139,7 +138,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
public static void DrawModelData(IIdentifiedObjectData data, float height) public static void DrawModelData(IIdentifiedObjectData data, float height)
{ {
var additionalData = data.AdditionalData; var additionalData = data.AdditionalData;
if (additionalData.Length == 0) if (additionalData.Length is 0)
return; return;
Im.Line.Same(); Im.Line.Same();
@ -151,7 +150,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
/// <summary> Draw the model information, right-justified. </summary> /// <summary> Draw the model information, right-justified. </summary>
public static void DrawModelData(ReadOnlySpan<byte> text, float height) public static void DrawModelData(ReadOnlySpan<byte> text, float height)
{ {
if (text.Length == 0) if (text.Length is 0)
return; return;
Im.Line.Same(); Im.Line.Same();

View file

@ -1,7 +1,7 @@
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
namespace Penumbra.UI; namespace Penumbra.UI.Classes;
[Flags] [Flags]
public enum ChangedItemIconFlag : uint public enum ChangedItemIconFlag : uint

View file

@ -19,7 +19,7 @@ public class CollectionSelectHeader(
CollectionResolver resolver, CollectionResolver resolver,
Configuration config, Configuration config,
CollectionCombo combo) CollectionCombo combo)
: IUiService : IHeader
{ {
private readonly ActiveCollections _activeCollections = collectionManager.Active; private readonly ActiveCollections _activeCollections = collectionManager.Active;
private static readonly AwesomeIcon Icon = FontAwesomeIcon.Stopwatch; private static readonly AwesomeIcon Icon = FontAwesomeIcon.Stopwatch;
@ -161,4 +161,30 @@ public class CollectionSelectHeader(
_activeCollections.SetCollection(collection!, CollectionType.Current); _activeCollections.SetCollection(collection!, CollectionType.Current);
Im.Line.Same(); Im.Line.Same();
} }
public bool Collapsed
=> false;
public void Draw(Vector2 size)
{
using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero);
DrawTemporaryCheckbox();
Im.Line.Same();
var comboWidth = (size.X - Im.Style.FrameHeight) / 4f;
var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f);
using (var _ = Im.Group())
{
DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1);
DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2);
DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3);
DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4);
combo.Draw("##collectionSelector"u8, comboWidth, ColorId.SelectedCollection.Value());
}
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
if (!_activeCollections.CurrentCollectionInUse)
ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg);
}
} }

View file

@ -1,5 +1,4 @@
using ImSharp; using ImSharp;
using OtterGui.Custom;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.Classes;
@ -41,17 +40,13 @@ public enum ColorId : short
public static class Colors public static class Colors
{ {
// These are written as 0xAABBGGRR. // These are written as 0xAABBGGRR.
public const uint PressEnterWarningBg = 0xFF202080; public static readonly Vector4 PressEnterWarningBg = new(0.5f, 0.125f, 0.125f, 1);
public const uint RegexWarningBorder = 0xFF0000B0; public static readonly Vector4 RegexWarningBorder = new(0.7f, 0, 0, 1);
public const uint MetaInfoText = 0xAAFFFFFF; public static readonly Vector4 MetaInfoText = new(1, 1, 1, 2f / 3);
public const uint RedTableBgTint = 0x40000080; public const uint RedTableBgTint = 0x40000080;
public const uint DiscordColor = CustomGui.DiscordColor; public const uint FilterActive = 0x807070FF;
public const uint FilterActive = 0x807070FF; public const uint TutorialMarker = 0xFF20FFFF;
public const uint TutorialMarker = 0xFF20FFFF; public const uint TutorialBorder = 0xD00000FF;
public const uint TutorialBorder = 0xD00000FF;
public const uint ReniColorButton = CustomGui.ReniColorButton;
public const uint ReniColorHovered = CustomGui.ReniColorHovered;
public const uint ReniColorActive = CustomGui.ReniColorActive;
public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) public static (uint DefaultColor, string Name, string Description) Data(this ColorId color)
=> color switch => color switch

View file

@ -9,10 +9,10 @@ public static class Combos
{ {
// Different combos to use with enums. // Different combos to use with enums.
public static bool Race(string label, ModelRace current, out ModelRace race, float unscaledWidth = 100) public static bool Race(string label, ModelRace current, out ModelRace race, float unscaledWidth = 100)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1); => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out race, ModelRaceExtensions.ToName, 1);
public static bool Gender(string label, Gender current, out Gender gender, float unscaledWidth = 120) public static bool Gender(string label, Gender current, out Gender gender, float unscaledWidth = 120)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, RaceEnumExtensions.ToName, 1); => ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, GenderExtensions.ToName, 1);
public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots,
@ -27,7 +27,7 @@ public static class Combos
EquipSlotExtensions.ToName); EquipSlotExtensions.ToName);
public static bool SubRace(string label, SubRace current, out SubRace subRace, float unscaledWidth = 150) public static bool SubRace(string label, SubRace current, out SubRace subRace, float unscaledWidth = 150)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1); => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out subRace, SubRaceExtensions.ToName, 1);
public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute, float unscaledWidth = 200) public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute, float unscaledWidth = 200)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out attribute, => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out attribute,

View file

@ -6,7 +6,7 @@ using Luna;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.UI; namespace Penumbra.UI.Classes;
public class FileDialogService : IDisposable, IUiService public class FileDialogService : IDisposable, IUiService
{ {

View file

@ -1,8 +1,7 @@
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.UI.Classes;
namespace Penumbra.UI; namespace Penumbra.UI.Classes;
public class IncognitoService(TutorialService tutorial, Configuration config) : IUiService public class IncognitoService(TutorialService tutorial, Configuration config) : IUiService
{ {

View file

@ -1,8 +1,10 @@
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.Mods;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager;
namespace Penumbra.Mods.Manager; namespace Penumbra.UI.Classes;
public class ModCombo(ModStorage modStorage) : SimpleFilterCombo<Mod>(SimpleFilterType.Regex), IUiService public class ModCombo(ModStorage modStorage) : SimpleFilterCombo<Mod>(SimpleFilterType.Regex), IUiService
{ {

View file

@ -0,0 +1,118 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Widgets;
namespace Penumbra.UI.Classes;
public class FilterComboColors : FilterComboCache<KeyValuePair<byte, (string Name, uint Color, bool Gloss)>>
{
private readonly float _comboWidth;
private readonly ImRaii.Color _color = new();
private Vector2 _buttonSize;
private uint _currentColor;
private bool _currentGloss;
protected override int UpdateCurrentSelected(int currentSelected)
{
if (CurrentSelection.Value.Color != _currentColor)
{
CurrentSelectionIdx = Items.IndexOf(c => c.Value.Color == _currentColor);
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
return base.UpdateCurrentSelected(CurrentSelectionIdx);
}
return currentSelected;
}
public FilterComboColors(float comboWidth, MouseWheelType allowMouseWheel,
Func<IReadOnlyList<KeyValuePair<byte, (string Name, uint Color, bool Gloss)>>> colors,
Logger log)
: base(colors, allowMouseWheel, log)
{
_comboWidth = comboWidth;
SearchByParts = true;
}
protected override float GetFilterWidth()
{
// Hack to not color the filter frame.
_color.Pop();
return _buttonSize.X + ImGui.GetStyle().ScrollbarSize;
}
protected override void DrawList(float width, float itemHeight)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.WindowPadding, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
_buttonSize = new Vector2(_comboWidth * ImGuiHelpers.GlobalScale, 0);
if (ImGui.GetScrollMaxY() > 0)
_buttonSize.X += ImGui.GetStyle().ScrollbarSize;
base.DrawList(width, itemHeight);
}
protected override string ToString(KeyValuePair<byte, (string Name, uint Color, bool Gloss)> obj)
=> obj.Value.Name;
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var (_, (name, color, gloss)) = Items[globalIdx];
// Push the stain color to type and if it is too bright, turn the text color black.
var contrastColor = ImGuiUtil.ContrastColorBw(color);
using var colors = ImRaii.PushColor(ImGuiCol.Button, color, color != 0)
.Push(ImGuiCol.Text, contrastColor);
var ret = ImGui.Button(name, _buttonSize);
if (selected)
{
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0xFF2020D0, 0, ImDrawFlags.None,
ImGuiHelpers.GlobalScale);
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin() + new Vector2(ImGuiHelpers.GlobalScale),
ImGui.GetItemRectMax() - new Vector2(ImGuiHelpers.GlobalScale), contrastColor, 0, ImDrawFlags.None, ImGuiHelpers.GlobalScale);
}
if (gloss)
ImGui.GetWindowDrawList().AddRectFilledMultiColor(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0x50FFFFFF, 0x50000000,
0x50FFFFFF, 0x50000000);
return ret;
}
public virtual bool Draw(string label, uint color, string name, bool found, bool gloss, float previewWidth,
MouseWheelType mouseWheel = MouseWheelType.Control)
{
_currentColor = color;
_currentGloss = gloss;
var preview = found && ImGui.CalcTextSize(name).X <= previewWidth ? name : string.Empty;
AllowMouseWheel = mouseWheel;
_color.Push(ImGuiCol.FrameBg, color, found && color != 0)
.Push(ImGuiCol.Text, ImGuiUtil.ContrastColorBw(color), preview.Length > 0);
var change = Draw(label, preview, found ? name : string.Empty, previewWidth, ImGui.GetFrameHeight(), ImGuiComboFlags.NoArrowButton);
return change;
}
protected override void PostCombo(float previewWidth)
{
_color.Dispose();
if (_currentGloss)
{
var min = ImGui.GetItemRectMin();
ImGui.GetWindowDrawList().AddRectFilledMultiColor(min, new Vector2(min.X + previewWidth, ImGui.GetItemRectMax().Y), 0x50FFFFFF,
0x50000000, 0x50FFFFFF, 0x50000000);
}
}
protected override void OnMouseWheel(string preview, ref int index, int steps)
{
UpdateCurrentSelected(0);
base.OnMouseWheel(preview, ref index, steps);
}
public bool Draw(string label, uint color, string name, bool found, bool gloss,
MouseWheelType mouseWheel = MouseWheelType.Control)
=> Draw(label, color, name, found, gloss, ImGui.GetFrameHeight(), mouseWheel);
}

View file

@ -0,0 +1,148 @@
using ImSharp;
namespace Penumbra.UI.Classes;
/// <summary> List of currently available tutorials. </summary>
public enum BasicTutorialSteps
{
GeneralTooltips,
ModDirectory,
EnableMods,
Deprecated1,
GeneralSettings,
Collections,
EditingCollections,
CurrentCollection,
SimpleAssignments,
IndividualAssignments,
GroupAssignments,
CollectionDetails,
Incognito,
Deprecated2,
Mods,
ModImport,
AdvancedHelp,
ModFilters,
CollectionSelectors,
Redrawing,
EnablingMods,
Priority,
ModOptions,
Fin,
Deprecated3,
Faq1,
Faq2,
Favorites,
Tags,
}
/// <summary> Service for the in-game tutorial. </summary>
public class TutorialService(EphemeralConfig config) : Luna.IUiService
{
private readonly Luna.Tutorial _tutorial = new Luna.Tutorial
{
BorderColor = new Rgba32(Colors.TutorialBorder).ToVector(),
HighlightColor = new Rgba32(Colors.TutorialMarker).ToVector(),
PopupLabel = new StringU8("Settings Tutorial"u8),
}
.Register("General Tooltips"u8, "This symbol gives you further information about whatever setting it appears next to.\n\n"u8
+ "Hover over them when you are unsure what something does or how to do something."u8)
.Register("Initial Setup, Step 1: Mod Directory"u8,
"The first step is to set up your mod directory, which is where your mods are extracted to.\n\n"u8
+ "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n"u8
+ "The folder should be an empty folder no other applications write to."u8)
.Register("Initial Setup, Step 2: Enable Mods"u8, "Do not forget to enable your mods in case they are not."u8)
.Deprecated()
.Register("General Settings"u8, "Look through all of these settings before starting, they might help you a lot!\n\n"u8
+ "If you do not know what some of these do yet, return to this later!"u8)
.Register("Initial Setup, Step 3: Collections"u8, "Collections are lists of settings for your installed mods.\n\n"u8
+ "This is our next stop!\n\n"u8
+ "Go here after setting up your root folder to continue the tutorial!"u8)
.Register("Initial Setup, Step 4: Managing Collections"u8,
"On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n"u8
+ "There will always be one collection called \"Default\" that can not be deleted."u8)
.Register("Initial Setup, Step 5: Selected Collection"u8,
"The Selected Collection is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n"u8
+ "We should already have the collection named \"Default\" selected, and for our simple setup, we do not need to do anything here.\n\n"u8)
.Register("Initial Setup, Step 6: Simple Assignments"u8,
"Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n"u8
+ "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n"u8
+ "If you are just starting, you can see that the \"Default\" collection is currently assigned to Default and Interface.\n"u8
+ "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons."u8)
.Register("Individual Assignments"u8,
"In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target."u8)
.Register("Group Assignments"u8,
"In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age."u8)
.Register("Collection Details"u8,
"In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n"u8
+ "Inheritance can be used to make one collection take the settings of another as long as it does not setup the mod in question itself."u8)
.Register("Incognito Mode"u8,
"This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n"u8
+ "and all displayed individual character names to their initials and world, in case you want to share screenshots.\n"u8
+ "It is strongly recommended to not show your characters name in public screenshots when using Penumbra."u8)
.Deprecated()
.Register("Initial Setup, Step 7: Mods"u8, "Our last stop is the Mods tab, where you can import and setup your mods.\n\n"u8
+ "Please go there after verifying that your Selected Collection and Default Collection are setup to your liking."u8)
.Register("Initial Setup, Step 8: Mod Import"u8,
"Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n"u8
+ "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n"u8
+ "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress."u8)
.Register("Advanced Help"u8, "Click this button to get detailed information on what you can do in the mod selector.\n\n"u8
+ "Import and select a mod now to continue."u8)
.Register("Mod Filters"u8, "You can filter the available mods by name, author, changed items or various attributes here."u8)
.Register("Collection Selectors"u8, "This row provides shortcuts to set your Selected Collection.\n\n"u8
+ "The first button sets it to your Base Collection (if any).\n\n"u8
+ "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n"u8
+ "The third is a regular collection selector to let you choose among all your collections."u8)
.Register("Redrawing"u8,
"Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n"u8
+ "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n"u8
+ "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too."u8)
.Register("Initial Setup, Step 9: Enabling Mods"u8,
"Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n"u8
+ "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance."u8)
.Register("Initial Setup, Step 10: Priority"u8,
"If two enabled mods in one collection change the same files, there is a conflict.\n\n"u8
+ "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n"u8
+ "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible."u8)
.Register("Mod Options"u8, "Many mods have options themselves. You can also choose those here.\n\n"u8
+ "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately."u8)
.Register("Initial Setup - Fin"u8, "Now you should have all information to get Penumbra running and working!\n\n"u8
+ "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page."u8)
.Deprecated()
.Register("FAQ 1"u8,
"It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices."u8)
.Register("FAQ 2"u8, "Penumbra can change the skin material a mod uses. This is under advanced editing."u8)
.Register("Favorites"u8,
"You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections."u8)
.Register("Tags"u8,
"Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'."u8)
.EnsureSize(Enum.GetValues<BasicTutorialSteps>().Length);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OpenTutorial(BasicTutorialSteps step)
=> _tutorial.Open((int)step, config.TutorialStep, v =>
{
config.TutorialStep = v;
config.Save();
});
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SkipTutorial(BasicTutorialSteps step)
=> _tutorial.Skip((int)step, config.TutorialStep, v =>
{
config.TutorialStep = v;
config.Save();
});
/// <summary> Update the current tutorial step if tutorials have changed since last update. </summary>
public void UpdateTutorialStep()
{
var tutorial = _tutorial.CurrentEnabledId(config.TutorialStep);
if (tutorial != config.TutorialStep)
{
config.TutorialStep = tutorial;
config.Save();
}
}
}

View file

@ -272,7 +272,7 @@ public sealed class CollectionPanel(
if (!context) if (!context)
return; return;
using (ImGuiColor.Text.Push(Colors.DiscordColor)) using (ImGuiColor.Text.Push(LunaStyle.DiscordColor))
{ {
if (Im.Menu.Item("Use no mods."u8)) if (Im.Menu.Item("Use no mods."u8))
_active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier));
@ -349,7 +349,7 @@ public sealed class CollectionPanel(
return; return;
using var target = Im.DragDrop.Target(); using var target = Im.DragDrop.Target();
if (!target || !target.IsDropping("DragIndividual")) if (!target || !target.IsDropping("DragIndividual"u8))
return; return;
var currentIdx = _active.Individuals.Index(id); var currentIdx = _active.Individuals.Index(id);
@ -376,7 +376,7 @@ public sealed class CollectionPanel(
break; break;
case CollectionType.MalePlayerCharacter: case CollectionType.MalePlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8) ImEx.TextMultiColored("Overruled by "u8)
.Then("Male Racial Player"u8, Colors.DiscordColor) .Then("Male Racial Player"u8, LunaStyle.DiscordColor)
.Then(", "u8) .Then(", "u8)
.Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color) .Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color)
.Then(", or "u8) .Then(", or "u8)
@ -386,7 +386,7 @@ public sealed class CollectionPanel(
break; break;
case CollectionType.FemalePlayerCharacter: case CollectionType.FemalePlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8) ImEx.TextMultiColored("Overruled by "u8)
.Then("Female Racial Player"u8, Colors.ReniColorActive) .Then("Female Racial Player"u8, LunaStyle.ReniColorActive)
.Then(", "u8) .Then(", "u8)
.Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color) .Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color)
.Then(", or "u8) .Then(", or "u8)
@ -396,24 +396,24 @@ public sealed class CollectionPanel(
break; break;
case CollectionType.MaleNonPlayerCharacter: case CollectionType.MaleNonPlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8) ImEx.TextMultiColored("Overruled by "u8)
.Then("Male Racial NPC"u8, Colors.DiscordColor) .Then("Male Racial NPC"u8, LunaStyle.DiscordColor)
.Then(", "u8) .Then(", "u8)
.Then("Children"u8, ColorId.FolderLine.Value().Color) .Then("Children"u8, ColorId.FolderLine.Value().Color)
.Then(", "u8) .Then(", "u8)
.Then("Elderly"u8, Colors.MetaInfoText) .Then("Elderly"u8, Colors.MetaInfoText)
.Then(", or "u8) .Then(", or "u8)
.Then("Individual "u8, ColorId.NewMod.Value().Color) .Then("Individual "u8, ColorId.NewMod.Value().Color)
.Then("Assignments.") .Then("Assignments."u8)
.End(); .End();
break; break;
case CollectionType.FemaleNonPlayerCharacter: case CollectionType.FemaleNonPlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8) ImEx.TextMultiColored("Overruled by "u8)
.Then("Female Racial NPC"u8, Colors.ReniColorActive) .Then("Female Racial NPC"u8, LunaStyle.ReniColorActive)
.Then(", "u8) .Then(", "u8)
.Then("Children"u8, ColorId.FolderLine.Value().Color) .Then("Children"u8, ColorId.FolderLine.Value().Color)
.Then(", "u8) .Then(", "u8)
.Then("Elderly"u8, Colors.MetaInfoText) .Then("Elderly"u8, Colors.MetaInfoText)
.Then(", or ") .Then(", or "u8)
.Then("Individual "u8, ColorId.NewMod.Value().Color) .Then("Individual "u8, ColorId.NewMod.Value().Color)
.Then("Assignments."u8) .Then("Assignments."u8)
.End(); .End();

View file

@ -1,144 +1,142 @@
using ImSharp; using ImSharp;
using OtterGui; using OtterGui;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab; namespace Penumbra.UI.CollectionTab;
public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposable public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposable
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage; private readonly CollectionStorage _storage;
private readonly ActiveCollections _active; private readonly ActiveCollections _active;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly IncognitoService _incognito; private readonly IncognitoService _incognito;
private ModCollection? _dragging; private ModCollection? _dragging;
public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active,
TutorialService tutorial, IncognitoService incognito) TutorialService tutorial, IncognitoService incognito)
: base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
{ {
_config = config; _config = config;
_communicator = communicator; _communicator = communicator;
_storage = storage; _storage = storage;
_active = active; _active = active;
_tutorial = tutorial; _tutorial = tutorial;
_incognito = incognito; _incognito = incognito;
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector); _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector);
// Set items. // Set items.
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty)); OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty));
// Set selection. // Set selection.
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty)); OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty));
} }
protected override bool OnDelete(int idx) protected override bool OnDelete(int idx)
{ {
if (idx < 0 || idx >= Items.Count) if (idx < 0 || idx >= Items.Count)
return false; return false;
// Always return false since we handle the selection update ourselves. // Always return false since we handle the selection update ourselves.
_storage.RemoveCollection(Items[idx]); _storage.RemoveCollection(Items[idx]);
return false; return false;
} }
protected override bool DeleteButtonEnabled() protected override bool DeleteButtonEnabled()
=> _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive(); => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
protected override string DeleteButtonTooltip() protected override string DeleteButtonTooltip()
=> _storage.DefaultNamed == Current => _storage.DefaultNamed == Current
? $"The selected collection {Name(Current)} can not be deleted." ? $"The selected collection {Name(Current)} can not be deleted."
: $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete."; : $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete.";
protected override bool OnAdd(string name) protected override bool OnAdd(string name)
=> _storage.AddCollection(name, null); => _storage.AddCollection(name, null);
protected override bool OnDuplicate(string name, int idx) protected override bool OnDuplicate(string name, int idx)
{ {
if (idx < 0 || idx >= Items.Count) if (idx < 0 || idx >= Items.Count)
return false; return false;
return _storage.AddCollection(name, Items[idx]); return _storage.AddCollection(name, Items[idx]);
} }
protected override bool Filtered(int idx) protected override bool Filtered(int idx)
=> !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); => !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
private const string PayloadString = "Collection"; protected override bool OnDraw(int idx)
{
protected override bool OnDraw(int idx) using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
{ var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx);
using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value()); using var source = Im.DragDrop.Source();
var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx);
using var source = Im.DragDrop.Source(); if (idx == CurrentIdx)
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
if (idx == CurrentIdx)
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); if (source)
{
if (source) _dragging = Items[idx];
{ source.SetPayload("Collection"u8);
_dragging = Items[idx]; Im.Text($"Assigning {Name(_dragging)} to...");
source.SetPayload(PayloadString); }
Im.Text($"Assigning {Name(_dragging)} to...");
} if (ret)
_active.SetCollection(Items[idx], CollectionType.Current);
if (ret)
_active.SetCollection(Items[idx], CollectionType.Current); return ret;
}
return ret;
} public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
{
public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier) using var target = Im.DragDrop.Target();
{ if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8))
using var target = Im.DragDrop.Target(); return;
if (!target.Success || _dragging is null || !target.IsDropping(PayloadString))
return; _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
_dragging = null;
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); }
_dragging = null;
} public void Dispose()
{
public void Dispose() _communicator.CollectionChange.Unsubscribe(OnCollectionChange);
{ }
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
} private string Name(ModCollection collection)
=> _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name;
private string Name(ModCollection collection)
=> _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name; public void RestoreCollections()
{
public void RestoreCollections() Items.Clear();
{ Items.Add(_storage.DefaultNamed);
Items.Clear(); foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
Items.Add(_storage.DefaultNamed); Items.Add(c);
foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed)) SetFilterDirty();
Items.Add(c); SetCurrent(_active.Current);
SetFilterDirty(); }
SetCurrent(_active.Current);
} private void OnCollectionChange(in CollectionChange.Arguments arguments)
{
private void OnCollectionChange(in CollectionChange.Arguments arguments) switch (arguments.Type)
{ {
switch (arguments.Type) case CollectionType.Temporary: return;
{ case CollectionType.Current:
case CollectionType.Temporary: return; if (arguments.NewCollection is not null)
case CollectionType.Current: SetCurrent(arguments.NewCollection);
if (arguments.NewCollection is not null) SetFilterDirty();
SetCurrent(arguments.NewCollection); return;
SetFilterDirty(); case CollectionType.Inactive:
return; RestoreCollections();
case CollectionType.Inactive: SetFilterDirty();
RestoreCollections(); return;
SetFilterDirty(); default:
return; SetFilterDirty();
default: return;
SetFilterDirty(); }
return; }
} }
}
}

View file

@ -221,7 +221,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService
var tt = inheritance switch var tt = inheritance switch
{ {
InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.", InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.",
InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", InheritanceManager.ValidInheritance.Valid => $"Let the Selected Collection inherit from this collection.",
InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.", InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.",
InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.", InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.",
InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.",
@ -307,7 +307,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService
} }
Im.Tooltip.OnHover( Im.Tooltip.OnHover(
$"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one.{(withDelete ? "\nControl + Shift + Right-Click to remove this inheritance."u8 : StringU8.Empty)}"); $"Control + Right-Click to switch the Selected Collection to this one.{(withDelete ? "\nControl + Shift + Right-Click to remove this inheritance."u8 : StringU8.Empty)}");
} }
private string Name(ModCollection collection) private string Name(ModCollection collection)

View file

@ -0,0 +1,45 @@
using System.Collections.Frozen;
using Dalamud.Interface.DragDrop;
using ImSharp;
using Luna;
using Penumbra.Mods.Manager;
namespace Penumbra.UI;
public sealed class GlobalModImporter : IRequiredService, IDisposable
{
public const string DragDropId = "ModDragDrop";
private readonly DragDropManager _dragDropManager;
private readonly ModImportManager _importManager;
/// <summary> All default extensions for valid mod imports. </summary>
public static readonly FrozenSet<string> ValidModExtensions = FrozenSet.Create(StringComparer.OrdinalIgnoreCase,
".ttmp", ".ttmp2", ".pmp", ".pcp", ".zip", ".rar", ".7z"
);
public GlobalModImporter(DragDropManager dragDropManager, ModImportManager modImportManager)
{
_dragDropManager = dragDropManager;
_importManager = modImportManager;
dragDropManager.AddSource(DragDropId, ValidExtension, DragTooltip);
dragDropManager.AddTarget(DragDropId, ImportFiles);
}
public void Dispose()
{
_dragDropManager.RemoveSource(DragDropId);
_dragDropManager.RemoveTarget(DragDropId);
}
private void ImportFiles(IReadOnlyList<string> files, IReadOnlyList<string> _)
=> _importManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f))));
private static bool ValidExtension(IDragDropManager manager)
=> manager.Extensions.Any(ValidModExtensions.Contains);
private static bool DragTooltip(IDragDropManager manager)
{
Im.Text($"Dragging mods for import:\n\t{StringU8.Join("\n\t"u8, manager.Files.Select(Path.GetFileName))}");
return true;
}
}

View file

@ -10,21 +10,21 @@ namespace Penumbra.UI;
/// </summary> /// </summary>
public class LaunchButton : IDisposable, Luna.IUiService public class LaunchButton : IDisposable, Luna.IUiService
{ {
private readonly ConfigWindow _configWindow; private readonly MainWindow.MainWindow _mainWindow;
private readonly IUiBuilder _uiBuilder; private readonly IUiBuilder _uiBuilder;
private readonly ITitleScreenMenu _title; private readonly ITitleScreenMenu _title;
private readonly string _fileName; private readonly string _fileName;
private readonly ITextureProvider _textureProvider; private readonly ITextureProvider _textureProvider;
private IReadOnlyTitleScreenMenuEntry? _entry; private IReadOnlyTitleScreenMenuEntry? _entry;
/// <summary> /// <summary>
/// Register the launch button to be created on the next draw event. /// Register the launch button to be created on the next draw event.
/// </summary> /// </summary>
public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui, ITextureProvider textureProvider) public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, MainWindow.MainWindow ui, ITextureProvider textureProvider)
{ {
_uiBuilder = pi.UiBuilder; _uiBuilder = pi.UiBuilder;
_configWindow = ui; _mainWindow = ui;
_textureProvider = textureProvider; _textureProvider = textureProvider;
_title = title; _title = title;
_entry = null; _entry = null;
@ -59,5 +59,5 @@ public class LaunchButton : IDisposable, Luna.IUiService
} }
private void OnTriggered() private void OnTriggered()
=> _configWindow.Toggle(); => _mainWindow.Toggle();
} }

View file

@ -0,0 +1,191 @@
using ImSharp;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.MainWindow;
public class UiState : ISavable, IService
{
public string ChangedItemTabNameFilter = string.Empty;
public string ChangedItemTabModFilter = string.Empty;
public ChangedItemIconFlag ChangedItemTabCategoryFilter = ChangedItemFlagExtensions.DefaultFlags;
public string ToFilePath(FilenameService fileNames)
=> "uiState";
public void Save(StreamWriter writer)
{ }
}
public sealed class ChangedItemsTab(
CollectionManager collectionManager,
CollectionSelectHeader collectionHeader,
ChangedItemDrawer drawer,
CommunicatorService communicator)
: ITab<TabType>
{
public ReadOnlySpan<byte> Label
=> "Changed Items"u8;
public TabType Identifier
=> TabType.ChangedItems;
private Vector2 _buttonSize;
private readonly ChangedItemFilter _filter = new(drawer, new UiState());
private sealed class ChangedItemFilter(ChangedItemDrawer drawer, UiState uiState) : IFilter<Item>
{
public bool WouldBeVisible(in Item item, int globalIndex)
=> drawer.FilterChangedItem(item.Name, item.Data, uiState.ChangedItemTabNameFilter)
&& (uiState.ChangedItemTabModFilter.Length is 0
|| item.Mods.Any(m => m.Name.Contains(uiState.ChangedItemTabModFilter, StringComparison.OrdinalIgnoreCase)));
public event Action? FilterChanged;
public bool DrawFilter(ReadOnlySpan<byte> label, Vector2 availableRegion)
{
var varWidth = Im.ContentRegion.Available.X
- 450 * Im.Style.GlobalScale
- Im.Style.ItemSpacing.X;
Im.Item.SetNextWidth(450 * Im.Style.GlobalScale);
var ret = Im.Input.Text("##changedItemsFilter"u8, ref uiState.ChangedItemTabNameFilter, "Filter Item..."u8);
Im.Line.Same();
Im.Item.SetNextWidth(varWidth);
ret |= Im.Input.Text("##changedItemsModFilter"u8, ref uiState.ChangedItemTabModFilter, "Filter Mods..."u8);
if (!ret)
return false;
FilterChanged?.Invoke();
return true;
}
public void Clear()
{
uiState.ChangedItemTabModFilter = string.Empty;
uiState.ChangedItemTabNameFilter = string.Empty;
uiState.ChangedItemTabCategoryFilter = ChangedItemFlagExtensions.DefaultFlags;
FilterChanged?.Invoke();
}
}
private readonly record struct Item(string Label, IIdentifiedObjectData Data, SingleArray<IMod> Mods)
{
public readonly string Name = Data.ToName(Label);
public readonly StringU8 ItemName = new(Data.ToName(Label));
public readonly StringU8 Mod = Mods.Count > 0 ? new StringU8(Mods[0].Name) : StringU8.Empty;
public readonly StringU8 ModelData = new(Data.AdditionalData);
public readonly ChangedItemIconFlag CategoryIcon = Data.GetIcon().ToFlag();
public readonly StringU8 Tooltip = Mods.Count > 1
? new StringU8($"Other mods affecting this item:\n{StringU8.Join((byte)'\n', Mods.Skip(1).Select(m => m.Name))}")
: StringU8.Empty;
}
private sealed class Cache : BasicFilterCache<Item>
{
private readonly ActiveCollections _collections;
private readonly CollectionChange _collectionChange;
public Cache(ActiveCollections collections, CommunicatorService communicator, IFilter<Item> filter)
: base(filter)
{
_collections = collections;
_collectionChange = communicator.CollectionChange;
_collectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ChangedItemsTabCache);
}
private void OnCollectionChange(in CollectionChange.Arguments arguments)
=> FilterDirty = true;
protected override IEnumerable<Item> GetItems()
=> _collections.Current.ChangedItems.Select(kvp => new Item(kvp.Key, kvp.Value.Item2, kvp.Value.Item1));
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_collectionChange.Unsubscribe(OnCollectionChange);
}
}
public void DrawContent()
{
collectionHeader.Draw(true);
drawer.DrawTypeFilter();
_filter.DrawFilter("##Filter"u8, Im.ContentRegion.Available);
using var child = Im.Child.Begin("##changedItemsChild"u8, Im.ContentRegion.Available);
if (!child)
return;
_buttonSize = new Vector2(Im.Style.ItemSpacing.Y + Im.Style.FrameHeight);
using var style = ImStyleDouble.CellPadding.Push(Vector2.Zero)
.Push(ImStyleDouble.ItemSpacing, Vector2.Zero)
.Push(ImStyleDouble.FramePadding, Vector2.Zero)
.Push(ImStyleDouble.SelectableTextAlign, new Vector2(0.01f, 0.5f));
using var table = Im.Table.Begin("##changedItems"u8, 3, TableFlags.RowBackground, Im.ContentRegion.Available);
if (!table)
return;
var varWidth = Im.ContentRegion.Available.X
- 450 * Im.Style.GlobalScale
- Im.Style.ItemSpacing.X;
const TableColumnFlags flags = TableColumnFlags.NoResize | TableColumnFlags.WidthFixed;
table.SetupColumn("items"u8, flags, 450 * Im.Style.GlobalScale);
table.SetupColumn("mods"u8, flags, varWidth - 140 * Im.Style.GlobalScale);
table.SetupColumn("id"u8, flags, 140 * Im.Style.GlobalScale);
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current,
() => new Cache(collectionManager.Active, communicator, _filter));
using var clipper = new Im.ListClipper(cache.Count, _buttonSize.Y);
foreach (var (idx, item) in clipper.Iterate(cache).Index())
{
using var id = Im.Id.Push(idx);
DrawChangedItemColumn(table, item);
}
}
/// <summary> Draw a full column for a changed item. </summary>
private void DrawChangedItemColumn(in Im.TableDisposable table, in Item item)
{
table.NextColumn();
drawer.DrawCategoryIcon(item.CategoryIcon, _buttonSize.Y);
Im.Line.NoSpacing();
var clicked = Im.Selectable(item.ItemName, false, SelectableFlags.None, _buttonSize with { X = 0 });
drawer.ChangedItemHandling(item.Data, clicked);
table.NextColumn();
DrawModColumn(item);
table.NextColumn();
ChangedItemDrawer.DrawModelData(item.ModelData, _buttonSize.Y);
}
private void DrawModColumn(in Item item)
{
if (item.Mods.Count <= 0)
return;
if (Im.Selectable(item.Mod, false, SelectableFlags.None, _buttonSize with { X = 0 })
&& Im.Io.KeyControl
&& item.Mods[0] is Mod mod)
communicator.SelectTab.Invoke(new SelectTab.Arguments(TabType.Mods, mod));
if (!Im.Item.Hovered())
return;
using var _ = Im.Tooltip.Begin();
Im.Text("Hold Control and click to jump to mod.\n"u8);
if (!item.Tooltip.IsEmpty)
Im.Text(item.Tooltip);
}
}

View file

@ -0,0 +1,179 @@
using Dalamud.Interface;
using ImSharp;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.MainWindow;
public sealed class EffectiveTab(
CollectionManager collectionManager,
CollectionSelectHeader collectionHeader,
CommunicatorService communicatorService)
: ITab<TabType>
{
public ReadOnlySpan<byte> Label
=> "Effective Changes"u8;
public void DrawContent()
{
collectionHeader.Draw(true);
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(collectionManager, communicatorService, _filter));
cache.Draw();
}
public TabType Identifier
=> TabType.EffectiveChanges;
private readonly PairFilter<Item> _filter = new(new GamePathFilter(), new FullPathFilter());
private sealed class Cache : BasicFilterCache<Item>, IPanel
{
private readonly CollectionManager _collectionManager;
private readonly CommunicatorService _communicator;
private float _arrowSize;
private float _gamePathSize;
private static readonly AwesomeIcon Arrow = FontAwesomeIcon.LongArrowAltLeft;
private new PairFilter<Item> Filter
=> (PairFilter<Item>)base.Filter;
public override void Update()
{
if (FontDirty)
{
_arrowSize = ImEx.Icon.CalculateSize(Arrow).X;
_gamePathSize = 450 * Im.Style.GlobalScale;
Dirty &= ~IManagedCache.DirtyFlags.Font;
}
base.Update();
}
public Cache(CollectionManager collectionManager, CommunicatorService communicator, IFilter<Item> filter)
: base(filter)
{
_collectionManager = collectionManager;
_communicator = communicator;
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.EffectiveChangesCache);
_communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.EffectiveChangesCache);
_communicator.ResolvedMetaChanged.Subscribe(OnResolvedMetaChange, ResolvedMetaChanged.Priority.EffectiveChangesCache);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
_communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange);
_communicator.ResolvedMetaChanged.Unsubscribe(OnResolvedMetaChange);
}
private void OnResolvedFileChange(in ResolvedFileChanged.Arguments arguments)
=> Dirty |= IManagedCache.DirtyFlags.Custom;
private void OnResolvedMetaChange(in ResolvedMetaChanged.Arguments arguments)
=> Dirty |= IManagedCache.DirtyFlags.Custom;
private void OnCollectionChange(in CollectionChange.Arguments arguments)
{
if (arguments.Type is CollectionType.Current)
Dirty |= IManagedCache.DirtyFlags.Custom;
}
protected override IEnumerable<Item> GetItems()
=> _collectionManager.Active.Current.Cache is null
? []
: _collectionManager.Active.Current.Cache.ResolvedFiles.Select(f => new Item(f.Value.Mod, f.Key.Path.Span, f.Value.Path))
.OrderBy(i => i.GamePath.Utf16)
.Concat(_collectionManager.Active.Current.Cache.Meta.IdentifierSources.Select(s => new Item(s.Item2, s.Item1))
.OrderBy(i => i.GamePath.Utf16));
public ReadOnlySpan<byte> Id
=> "EC"u8;
public void Draw()
{
DrawFilters();
DrawTable();
}
private void DrawFilters()
{
using var style = ImStyleSingle.FrameRounding.Push(0).PushX(ImStyleDouble.ItemSpacing, 0);
Filter.Filter1.DrawFilter("Filter game path..."u8, new Vector2(_gamePathSize + Im.Style.CellPadding.X, Im.Style.FrameHeight));
Im.Line.Same(0, _arrowSize + 2 * Im.Style.CellPadding.X);
Filter.Filter2.DrawFilter("Filter file path..."u8, Im.ContentRegion.Available with { Y = Im.Style.FrameHeight });
}
private void DrawTable()
{
using var table = Im.Table.Begin("t"u8, 3, TableFlags.RowBackground | TableFlags.ScrollY, Im.ContentRegion.Available);
if (!table)
return;
table.SetupColumn("gp"u8, TableColumnFlags.WidthFixed, _gamePathSize);
table.SetupColumn("a"u8, TableColumnFlags.WidthFixed, _arrowSize);
table.SetupColumn("fp"u8, TableColumnFlags.WidthStretch);
using var clipper = new Im.ListClipper(Count, Im.Style.TextHeightWithSpacing);
foreach (var item in clipper.Iterate(this))
{
table.NextColumn();
ImEx.CopyOnClickSelectable(item.GamePath.Utf8);
table.NextColumn();
ImEx.Icon.Draw(Arrow);
table.NextColumn();
ImEx.CopyOnClickSelectable(item.FilePath.InternalName.Span);
if (!item.IsMeta)
Im.Tooltip.OnHover($"\nChanged by {item.Mod.Name}.");
}
}
}
private sealed class GamePathFilter : RegexFilterBase<Item>
{
protected override string ToFilterString(in Item item, int globalIndex)
=> item.GamePath.Utf16;
}
private sealed class FullPathFilter : RegexFilterBase<Item>
{
protected override string ToFilterString(in Item item, int globalIndex)
=> item.FilePath.FullName;
}
private sealed class Item
{
public IMod Mod;
public StringPair GamePath;
public FullPath FilePath;
public bool IsMeta;
public Item(IMod mod, ReadOnlySpan<byte> gamePath, FullPath filePath)
{
Mod = mod;
GamePath = new StringPair(gamePath);
FilePath = filePath;
IsMeta = false;
}
public Item(IMod mod, IMetaIdentifier identifier)
{
Mod = mod;
GamePath = new StringPair($"{identifier}");
FilePath = new FullPath(mod.Name);
IsMeta = true;
}
}
}

View file

@ -1,21 +1,24 @@
using Luna; using Luna;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Tabs;
using Penumbra.UI.Tabs.Debug; using Penumbra.UI.Tabs.Debug;
using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher; using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher;
namespace Penumbra.UI.Tabs; namespace Penumbra.UI.MainWindow;
public sealed class MainTabBar : TabBar<TabType>, IDisposable public sealed class MainTabBar : TabBar<TabType>, IDisposable
{ {
public readonly ModsTab Mods; public readonly Tabs.ModsTab Mods;
private readonly EphemeralConfig _config; private readonly EphemeralConfig _config;
private readonly SelectTab _selectTab; private readonly SelectTab _selectTab;
public MainTabBar(Logger log, public MainTabBar(Logger log,
SettingsTab settings, SettingsTab settings,
ModsTab mods, Tabs.ModsTab mods,
ModTab mods2,
CollectionsTab collections, CollectionsTab collections,
ChangedItemsTab changedItems, ChangedItemsTab changedItems,
EffectiveTab effectiveChanges, EffectiveTab effectiveChanges,
@ -24,7 +27,7 @@ public sealed class MainTabBar : TabBar<TabType>, IDisposable
Watcher watcher, Watcher watcher,
OnScreenTab onScreen, OnScreenTab onScreen,
MessagesTab messages, EphemeralConfig config, CommunicatorService communicator) MessagesTab messages, EphemeralConfig config, CommunicatorService communicator)
: base(nameof(MainTabBar), log, settings, collections, mods, changedItems, effectiveChanges, onScreen, : base(nameof(MainTabBar), log, settings, collections, mods, mods2, changedItems, effectiveChanges, onScreen,
resources, watcher, debug, messages) resources, watcher, debug, messages)
{ {
Mods = mods; Mods = mods;

View file

@ -6,9 +6,9 @@ using Penumbra.UI.Classes;
using Penumbra.UI.Tabs; using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType; using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra.UI; namespace Penumbra.UI.MainWindow;
public sealed class ConfigWindow : Window public sealed class MainWindow : Window
{ {
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _config; private readonly Configuration _config;
@ -17,7 +17,7 @@ public sealed class ConfigWindow : Window
private MainTabBar _configTabs = null!; private MainTabBar _configTabs = null!;
private string? _lastException; private string? _lastException;
public ConfigWindow(IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, public MainWindow(IDalamudPluginInterface pi, Configuration config, ValidityChecker checker,
TutorialService tutorial) TutorialService tutorial)
: base(GetLabel(checker)) : base(GetLabel(checker))
{ {

View file

@ -2,7 +2,7 @@ using Luna;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.Tabs; namespace Penumbra.UI.MainWindow;
public sealed class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab<TabType> public sealed class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab<TabType>
{ {

View file

@ -1,6 +1,4 @@
using Dalamud.Bindings.ImGui;
using ImSharp; using ImSharp;
using OtterGui.Text;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -34,7 +32,7 @@ public class AddGroupDrawer : Luna.IUiService
public void Draw(Mod mod, float width) public void Draw(Mod mod, float width)
{ {
var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); var buttonWidth = new Vector2((width - Im.Style.ItemInnerSpacing.X) / 2, 0);
DrawBasicGroups(mod, width, buttonWidth); DrawBasicGroups(mod, width, buttonWidth);
DrawImcData(mod, buttonWidth); DrawImcData(mod, buttonWidth);
} }
@ -42,7 +40,7 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawBasicGroups(Mod mod, float width, Vector2 buttonWidth) private void DrawBasicGroups(Mod mod, float width, Vector2 buttonWidth)
{ {
Im.Item.SetNextWidth(width); Im.Item.SetNextWidth(width);
if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) if (Im.Input.Text("##name"u8, ref _groupName, "Enter New Name..."u8))
_groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false);
DrawSingleGroupButton(mod, buttonWidth); DrawSingleGroupButton(mod, buttonWidth);
@ -53,10 +51,9 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawSingleGroupButton(Mod mod, Vector2 width) private void DrawSingleGroupButton(Mod mod, Vector2 width)
{ {
if (!ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid if (!ImEx.Button("Add Single Group"u8, width, _groupNameValid
? "Add a new single selection option group to this mod."u8 ? "Add a new single selection option group to this mod."u8
: "Can not add a new group of this name."u8, : "Can not add a new group of this name."u8, !_groupNameValid))
width, !_groupNameValid))
return; return;
_modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName);
@ -66,10 +63,9 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawMultiGroupButton(Mod mod, Vector2 width) private void DrawMultiGroupButton(Mod mod, Vector2 width)
{ {
if (!ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid if (!ImEx.Button("Add Multi Group"u8, width, _groupNameValid
? "Add a new multi selection option group to this mod."u8 ? "Add a new multi selection option group to this mod."u8
: "Can not add a new group of this name."u8, : "Can not add a new group of this name."u8, !_groupNameValid))
width, !_groupNameValid))
return; return;
_modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName);
@ -79,10 +75,9 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawCombiningGroupButton(Mod mod, Vector2 width) private void DrawCombiningGroupButton(Mod mod, Vector2 width)
{ {
if (!ImUtf8.ButtonEx("Add Combining Group"u8, _groupNameValid if (!ImEx.Button("Add Combining Group"u8, width, _groupNameValid
? "Add a new combining option group to this mod."u8 ? "Add a new combining option group to this mod."u8
: "Can not add a new group of this name."u8, : "Can not add a new group of this name."u8, !_groupNameValid))
width, !_groupNameValid))
return; return;
_modManager.OptionEditor.AddModGroup(mod, GroupType.Combining, _groupName); _modManager.OptionEditor.AddModGroup(mod, GroupType.Combining, _groupName);
@ -103,7 +98,7 @@ public class AddGroupDrawer : Luna.IUiService
} }
else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman)
{ {
var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / Im.Style.GlobalScale) / 2; var quarterWidth = (width - Im.Style.ItemInnerSpacing.X / Im.Style.GlobalScale) / 2;
change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width);
Im.Line.SameInner(); Im.Line.SameInner();
change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, quarterWidth);
@ -130,12 +125,11 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawImcButton(Mod mod, Vector2 width) private void DrawImcButton(Mod mod, Vector2 width)
{ {
if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid if (ImEx.Button("Add IMC Group"u8, width, !_groupNameValid
? "Can not add a new group of this name."u8 ? "Can not add a new group of this name."u8
: _entryInvalid : _entryInvalid
? "The associated IMC entry is invalid."u8 ? "The associated IMC entry is invalid."u8
: "Add a new multi selection option group to this mod."u8, : "Add a new multi selection option group to this mod."u8, !_groupNameValid || _entryInvalid))
width, !_groupNameValid || _entryInvalid))
{ {
_modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcIdentifier, _defaultEntry); _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcIdentifier, _defaultEntry);
_groupName = string.Empty; _groupName = string.Empty;
@ -148,7 +142,7 @@ public class AddGroupDrawer : Luna.IUiService
var text = _imcFileExists var text = _imcFileExists
? "IMC Entry Does Not Exist"u8 ? "IMC Entry Does Not Exist"u8
: "IMC File Does Not Exist"u8; : "IMC File Does Not Exist"u8;
ImUtf8.TextFramed(text, Colors.PressEnterWarningBg, width); ImEx.TextFramed(text, width, Colors.PressEnterWarningBg);
} }
} }

View file

@ -1,112 +1,95 @@
using Dalamud.Interface; using ImSharp;
using Dalamud.Bindings.ImGui; using Luna;
using ImSharp; using Penumbra.Mods.Groups;
using OtterGui; using Penumbra.Mods.SubMods;
using OtterGui.Raii;
using OtterGui.Text; namespace Penumbra.UI.ModsTab.Groups;
using Penumbra.Mods.Groups;
using Penumbra.Mods.SubMods; public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer
{
namespace Penumbra.UI.ModsTab.Groups; public void Draw()
{
public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer foreach (var (optionIdx, option) in group.OptionData.Index())
{ {
public void Draw() using var id = Im.Id.Push(optionIdx);
{ editor.DrawOptionPosition(group, option, optionIdx);
foreach (var (optionIdx, option) in group.OptionData.Index())
{ Im.Line.SameInner();
using var id = ImUtf8.PushId(optionIdx); editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
editor.DrawOptionPosition(group, option, optionIdx);
Im.Line.SameInner();
Im.Line.SameInner(); editor.DrawOptionName(option);
editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
Im.Line.SameInner();
Im.Line.SameInner(); editor.DrawOptionDescription(option);
editor.DrawOptionName(option);
Im.Line.SameInner();
Im.Line.SameInner(); editor.DrawOptionDelete(option);
editor.DrawOptionDescription(option); }
Im.Line.SameInner(); DrawNewOption();
editor.DrawOptionDelete(option); DrawContainerNames();
} }
DrawNewOption(); private void DrawNewOption()
DrawContainerNames(); {
} var count = group.OptionData.Count;
if (count >= IModGroup.MaxCombiningOptions)
private void DrawNewOption() return;
{
var count = group.OptionData.Count; var name = editor.DrawNewOptionBase(group, count);
if (count >= IModGroup.MaxCombiningOptions)
return; var validName = name.Length > 0;
if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, validName
var name = editor.DrawNewOptionBase(group, count); ? "Add a new option to this group."u8
: "Please enter a name for the new option."u8, !validName))
var validName = name.Length > 0; {
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name);
? "Add a new option to this group."u8 editor.NewOptionName = null;
: "Please enter a name for the new option."u8, default, !validName)) }
{ }
editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name);
editor.NewOptionName = null; private void DrawContainerNames()
} {
} if (ImEx.Button("Edit Container Names"u8, new Vector2(400 * Im.Style.GlobalScale, 0),
"Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8))
private unsafe void DrawContainerNames() Im.Popup.Open("names"u8);
{
if (ImUtf8.ButtonEx("Edit Container Names"u8, var sizeX = group.OptionData.Count * (Im.Style.ItemInnerSpacing.X + Im.Style.FrameHeight) + 300 * Im.Style.GlobalScale;
"Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8, Im.Window.SetNextSize(new Vector2(sizeX,
new Vector2(400 * Im.Style.GlobalScale, 0))) Im.Style.FrameHeightWithSpacing * Math.Min(16, group.Data.Count) + 200 * Im.Style.GlobalScale));
ImUtf8.OpenPopup("DataContainerNames"u8); using var popup = Im.Popup.Begin("names"u8);
if (!popup)
var sizeX = group.OptionData.Count * (Im.Style.ItemInnerSpacing.X + Im.Style.FrameHeight) + 300 * Im.Style.GlobalScale; return;
ImGui.SetNextWindowSize(new Vector2(sizeX, Im.Style.FrameHeightWithSpacing * Math.Min(16, group.Data.Count) + 200 * Im.Style.GlobalScale));
using var popup = ImUtf8.Popup("DataContainerNames"u8); foreach (var option in group.OptionData)
if (!popup) {
return; ImEx.RotatedText(option.Name, true);
Im.Line.SameInner();
foreach (var option in group.OptionData) }
{
ImUtf8.RotatedText(option.Name, true); Im.Line.New();
Im.Line.SameInner(); Im.Separator();
} using var child = Im.Child.Begin("##Child"u8, Im.ContentRegion.Available);
Im.ListClipper.Draw(group.Data, DrawRow, Im.Style.FrameHeightWithSpacing);
Im.Line.New(); }
Im.Separator();
using var child = ImUtf8.Child("##Child"u8, Im.ContentRegion.Available); private void DrawRow(CombinedDataContainer container, int index)
ImGuiClip.ClippedDraw(group.Data, DrawRow, Im.Style.FrameHeightWithSpacing); {
} using var id = Im.Id.Push(index);
using (Im.Disabled())
private void DrawRow(CombinedDataContainer container, int index) {
{ for (var i = 0; i < group.OptionData.Count; ++i)
using var id = ImUtf8.PushId(index); {
using (ImRaii.Disabled()) id.Push(i);
{ var check = (index & (1 << i)) != 0;
for (var i = 0; i < group.OptionData.Count; ++i) Im.Checkbox(""u8, ref check);
{ Im.Line.SameInner();
id.Push(i); id.Pop();
var check = (index & (1 << i)) != 0; }
ImUtf8.Checkbox(""u8, ref check); }
Im.Line.SameInner();
id.Pop(); if (ImEx.InputOnDeactivation.Text("##Nothing"u8, container.Name, out string newName, "Optional Display Name..."u8))
} editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, newName);
} }
}
var name = editor.CombiningDisplayIndex == index ? editor.CombiningDisplayName ?? container.Name : container.Name;
if (ImUtf8.InputText("##Nothing"u8, ref name, "Optional Display Name..."u8))
{
editor.CombiningDisplayIndex = index;
editor.CombiningDisplayName = name;
}
if (ImGui.IsItemDeactivatedAfterEdit())
editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, name);
if (ImGui.IsItemDeactivated())
{
editor.CombiningDisplayIndex = -1;
editor.CombiningDisplayName = null;
}
}
}

View file

@ -1,187 +1,189 @@
using Dalamud.Interface; using ImSharp;
using Dalamud.Bindings.ImGui; using Luna;
using ImSharp; using Penumbra.GameData.Structs;
using OtterGui.Raii; using Penumbra.Meta;
using OtterGui.Text; using Penumbra.Mods.Groups;
using OtterGuiInternal.Utility; using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.GameData.Structs; using Penumbra.Mods.SubMods;
using Penumbra.Meta; using Penumbra.UI.AdvancedWindow.Meta;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager.OptionEditor; namespace Penumbra.UI.ModsTab.Groups;
using Penumbra.Mods.SubMods;
using Penumbra.UI.AdvancedWindow.Meta; public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer
{
namespace Penumbra.UI.ModsTab.Groups; public void Draw()
{
public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer var identifier = group.Identifier;
{ var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry;
public void Draw() var entry = group.DefaultEntry;
{ var changes = false;
var identifier = group.Identifier;
var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; var width = editor.AvailableWidth.X
var entry = group.DefaultEntry; - 3 * Im.Style.ItemInnerSpacing.X
var changes = false; - Im.Style.ItemSpacing.X
- Im.Font.CalculateSize("All Variants"u8).X
var width = editor.AvailableWidth.X - 3 * ImUtf8.ItemInnerSpacing.X - Im.Style.ItemSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X - ImUtf8.CalcTextSize("Only Attributes"u8).X - 2 * ImUtf8.FrameHeight; - Im.Font.CalculateSize("Only Attributes"u8).X
ImEx.TextFramed(identifier.ToString(), new Vector2(width, 0), Rgba32.Transparent); - 2 * Im.Style.FrameHeight;
ImEx.TextFramed(identifier.ToString(), new Vector2(width, 0), Rgba32.Transparent);
Im.Line.SameInner();
var allVariants = group.AllVariants; Im.Line.SameInner();
if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) var allVariants = group.AllVariants;
editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); if (Im.Checkbox("All Variants"u8, ref allVariants))
Im.Tooltip.OnHover("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants);
Im.Tooltip.OnHover("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8);
Im.Line.Same();
var onlyAttributes = group.OnlyAttributes; Im.Line.Same();
if (ImUtf8.Checkbox("Only Attributes"u8, ref onlyAttributes)) var onlyAttributes = group.OnlyAttributes;
editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes); if (Im.Checkbox("Only Attributes"u8, ref onlyAttributes))
Im.Tooltip.OnHover("Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8); editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes);
Im.Tooltip.OnHover(
using (ImUtf8.Group()) "Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8);
{
ImUtf8.TextFrameAligned("Material ID"u8); using (Im.Group())
ImUtf8.TextFrameAligned("VFX ID"u8); {
ImUtf8.TextFrameAligned("Decal ID"u8); ImEx.TextFrameAligned("Material ID"u8);
} ImEx.TextFrameAligned("VFX ID"u8);
ImEx.TextFrameAligned("Decal ID"u8);
Im.Line.Same(); }
using (ImUtf8.Group())
{ Im.Line.Same();
changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true); using (Im.Group())
changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true); {
changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true); changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true);
} changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true);
changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true);
Im.Line.Same(0, editor.PriorityWidth); }
using (ImUtf8.Group())
{ Im.Line.Same(0, editor.PriorityWidth);
ImUtf8.TextFrameAligned("Material Animation ID"u8); using (Im.Group())
ImUtf8.TextFrameAligned("Sound ID"u8); {
ImUtf8.TextFrameAligned("Can Be Disabled"u8); ImEx.TextFrameAligned("Material Animation ID"u8);
} ImEx.TextFrameAligned("Sound ID"u8);
ImEx.TextFrameAligned("Can Be Disabled"u8);
Im.Line.Same(); }
using (ImUtf8.Group()) Im.Line.Same();
{
changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); using (Im.Group())
changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true); {
var canBeDisabled = group.CanBeDisabled; changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true);
if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true);
editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); var canBeDisabled = group.CanBeDisabled;
} if (Im.Checkbox("##disabled"u8, ref canBeDisabled))
editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled);
if (changes) }
editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry);
if (changes)
ImGui.Dummy(Vector2.Zero); editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry);
DrawOptions();
var attributeCache = new ImcAttributeCache(group); Im.Dummy(Vector2.Zero);
DrawNewOption(attributeCache); DrawOptions();
ImGui.Dummy(Vector2.Zero); var attributeCache = new ImcAttributeCache(group);
DrawNewOption(attributeCache);
Im.Dummy(Vector2.Zero);
using (ImUtf8.Group())
{
ImUtf8.TextFrameAligned("Default Attributes"u8); using (Im.Group())
foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) {
ImUtf8.TextFrameAligned(option.Name); ImEx.TextFrameAligned("Default Attributes"u8);
} foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod))
ImEx.TextFrameAligned(option.Name);
Im.Line.SameInner(); }
using (ImUtf8.Group())
{ Im.Line.SameInner();
DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); using (Im.Group())
foreach (var (idx, option) in group.OptionData.Index().Where(o => !o.Item.IsDisableSubMod)) {
{ DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group);
using var id = ImUtf8.PushId(idx); foreach (var (idx, option) in group.OptionData.Index().Where(o => !o.Item.IsDisableSubMod))
DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option, {
group.DefaultEntry.AttributeMask); using var id = Im.Id.Push(idx);
} DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option,
} group.DefaultEntry.AttributeMask);
} }
}
private void DrawOptions() }
{
foreach (var (optionIdx, option) in group.OptionData.Index()) private void DrawOptions()
{ {
using var id = ImRaii.PushId(optionIdx); foreach (var (optionIdx, option) in group.OptionData.Index())
editor.DrawOptionPosition(group, option, optionIdx); {
using var id = Im.Id.Push(optionIdx);
Im.Line.SameInner(); editor.DrawOptionPosition(group, option, optionIdx);
editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
Im.Line.SameInner();
Im.Line.SameInner(); editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
editor.DrawOptionName(option);
Im.Line.SameInner();
Im.Line.SameInner(); editor.DrawOptionName(option);
editor.DrawOptionDescription(option);
Im.Line.SameInner();
if (!option.IsDisableSubMod) editor.DrawOptionDescription(option);
{
Im.Line.SameInner(); if (!option.IsDisableSubMod)
editor.DrawOptionDelete(option); {
} Im.Line.SameInner();
} editor.DrawOptionDelete(option);
} }
}
private void DrawNewOption(in ImcAttributeCache cache) }
{
var dis = cache.LowestUnsetMask == 0; private void DrawNewOption(in ImcAttributeCache cache)
var name = editor.DrawNewOptionBase(group, group.Options.Count); {
var validName = name.Length > 0; var dis = cache.LowestUnsetMask is 0;
var tt = dis var name = editor.DrawNewOptionBase(group, group.Options.Count);
? "No Free Attribute Slots for New Options..."u8 var validName = name.Length > 0;
: validName var tt = dis
? "Add a new option to this group."u8 ? "No Free Attribute Slots for New Options..."u8
: "Please enter a name for the new option."u8; : validName
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, default, !validName || dis)) ? "Add a new option to this group."u8
{ : "Please enter a name for the new option."u8;
editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, tt, !validName || dis))
editor.NewOptionName = null; {
} editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name);
} editor.NewOptionName = null;
}
private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data, }
ushort? defaultMask = null)
{ private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data,
for (var i = 0; i < ImcEntry.NumAttributes; ++i) ushort? defaultMask = null)
{ {
using var id = ImRaii.PushId(i); for (var i = 0; i < ImcEntry.NumAttributes; ++i)
var flag = 1 << i; {
var value = (mask & flag) != 0; using var id = Im.Id.Push(i);
var inDefault = defaultMask.HasValue && (defaultMask & flag) != 0; var flag = 1 << i;
using (ImRaii.Disabled(defaultMask != null && !cache.CanChange(i))) var value = (mask & flag) is not 0;
{ var inDefault = defaultMask.HasValue && (defaultMask & flag) is not 0;
if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : ImUtf8.Checkbox(""u8, ref value)) using (Im.Disabled(defaultMask is not null && !cache.CanChange(i)))
{ {
if (data is ImcModGroup g) if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : Im.Checkbox(""u8, ref value))
editor.ChangeDefaultAttribute(g, cache, i, value); {
else if (data is ImcModGroup g)
editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); editor.ChangeDefaultAttribute(g, cache, i, value);
} else
} editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value);
}
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1)); }
if (i != 9)
Im.Line.SameInner(); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1));
} if (i != 9)
} Im.Line.SameInner();
}
private sealed class NegativeCheckbox : OtterGui.Text.Widget.MultiStateCheckbox<bool> }
{
public static readonly NegativeCheckbox Instance = new(); private sealed class NegativeCheckbox : OtterGui.Text.Widget.MultiStateCheckbox<bool>
{
protected override void RenderSymbol(bool value, Vector2 position, float size) public static readonly NegativeCheckbox Instance = new();
{
if (value) protected override void RenderSymbol(bool value, Vector2 position, float size)
SymbolHelpers.RenderCross(ImGui.GetWindowDrawList(), position, ImGuiColor.CheckMark.Get().Color, size); {
} if (value)
Im.Render.Cross(Im.Window.DrawList, position, ImGuiColor.CheckMark.Get(), size);
protected override bool NextValue(bool value) }
=> !value;
protected override bool NextValue(bool value)
protected override bool PreviousValue(bool value) => !value;
=> !value;
} protected override bool PreviousValue(bool value)
} => !value;
}
}

View file

@ -1,370 +1,343 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Bindings.ImGui; using ImSharp;
using ImSharp; using Luna;
using Luna; using OtterGui.Raii;
using OtterGui; using Penumbra.Meta;
using OtterGui.Raii; using Penumbra.Mods;
using OtterGui.Text; using Penumbra.Mods.Groups;
using OtterGui.Text.EndObjects; using Penumbra.Mods.Manager;
using Penumbra.Meta; using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods; using Penumbra.Mods.Settings;
using Penumbra.Mods.Groups; using Penumbra.Mods.SubMods;
using Penumbra.Mods.Manager; using Penumbra.Services;
using Penumbra.Mods.Manager.OptionEditor; using Penumbra.UI.Classes;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab.Groups;
using Penumbra.Services;
using Penumbra.UI.Classes; public sealed class ModGroupEditDrawer(
ModManager modManager,
namespace Penumbra.UI.ModsTab.Groups; Configuration config,
FilenameService filenames,
public sealed class ModGroupEditDrawer( DescriptionEditPopup descriptionPopup,
ModManager modManager, ImcChecker imcChecker) : IUiService
Configuration config, {
FilenameService filenames, private static ReadOnlySpan<byte> AcrossGroupsLabel
DescriptionEditPopup descriptionPopup, => "##DragOptionAcross"u8;
ImcChecker imcChecker) : IUiService
{ private static ReadOnlySpan<byte> InsideGroupLabel
private static ReadOnlySpan<byte> AcrossGroupsLabel => "##DragOptionInside"u8;
=> "##DragOptionAcross"u8;
internal readonly ImcChecker ImcChecker = imcChecker;
private static ReadOnlySpan<byte> InsideGroupLabel internal readonly ModManager ModManager = modManager;
=> "##DragOptionInside"u8; internal readonly Queue<Action> ActionQueue = new();
internal readonly ImcChecker ImcChecker = imcChecker; internal Vector2 OptionIdxSelectable;
internal readonly ModManager ModManager = modManager; internal Vector2 AvailableWidth;
internal readonly Queue<Action> ActionQueue = new(); internal float PriorityWidth;
internal Vector2 OptionIdxSelectable; internal string? NewOptionName;
internal Vector2 AvailableWidth; private IModGroup? _newOptionGroup;
internal float PriorityWidth;
private Vector2 _buttonSize;
internal string? NewOptionName; private float _groupNameWidth;
private IModGroup? _newOptionGroup; private float _optionNameWidth;
private float _spacing;
private Vector2 _buttonSize; private bool _deleteEnabled;
private float _groupNameWidth;
private float _optionNameWidth; private string? _currentGroupName;
private float _spacing; private IModGroup? _currentGroupEdited;
private bool _deleteEnabled; private bool _isGroupNameValid = true;
private string? _currentGroupName; private IModGroup? _dragDropGroup;
private ModPriority? _currentGroupPriority; private IModOption? _dragDropOption;
private IModGroup? _currentGroupEdited; private bool _draggingAcross;
private bool _isGroupNameValid = true;
public void Draw(Mod mod)
private IModGroup? _dragDropGroup; {
private IModOption? _dragDropOption; PrepareStyle();
private bool _draggingAcross;
using var id = Im.Id.Push("ge"u8);
internal string? CombiningDisplayName; foreach (var (groupIdx, group) in mod.Groups.Index())
internal int CombiningDisplayIndex; DrawGroup(group, groupIdx);
public void Draw(Mod mod) while (ActionQueue.TryDequeue(out var action))
{ action.Invoke();
PrepareStyle(); }
using var id = ImUtf8.PushId("##GroupEdit"u8); private void DrawGroup(IModGroup group, int idx)
foreach (var (groupIdx, group) in mod.Groups.Index()) {
DrawGroup(group, groupIdx); using var id = Im.Id.Push(idx);
using var frame = ImRaii.FramedGroup($"Group #{idx + 1}");
while (ActionQueue.TryDequeue(out var action)) DrawGroupNameRow(group, idx);
action.Invoke(); group.EditDrawer(this).Draw();
} }
private void DrawGroup(IModGroup group, int idx) private void DrawGroupNameRow(IModGroup group, int idx)
{ {
using var id = ImUtf8.PushId(idx); DrawGroupName(group);
using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); Im.Line.SameInner();
DrawGroupNameRow(group, idx); DrawGroupMoveButtons(group, idx);
group.EditDrawer(this).Draw(); Im.Line.SameInner();
} DrawGroupOpenFile(group, idx);
Im.Line.SameInner();
private void DrawGroupNameRow(IModGroup group, int idx) DrawGroupDescription(group);
{ Im.Line.SameInner();
DrawGroupName(group); DrawGroupDelete(group);
Im.Line.SameInner(); Im.Line.SameInner();
DrawGroupMoveButtons(group, idx); DrawGroupPriority(group);
Im.Line.SameInner(); }
DrawGroupOpenFile(group, idx);
Im.Line.SameInner(); private void DrawGroupName(IModGroup group)
DrawGroupDescription(group); {
Im.Line.SameInner(); var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name;
DrawGroupDelete(group); Im.Item.SetNextWidth(_groupNameWidth);
Im.Line.SameInner(); using var border = ImStyleBorder.Frame.Push(Colors.RegexWarningBorder, Im.Style.GlobalScale * 2, !_isGroupNameValid);
DrawGroupPriority(group); if (Im.Input.Text("##GroupName"u8, ref text))
} {
_currentGroupEdited = group;
private void DrawGroupName(IModGroup group) _currentGroupName = text;
{ _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false);
var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; }
Im.Item.SetNextWidth(_groupNameWidth);
using var border = ImRaii.PushFrameBorder(Im.Style.GlobalScale * 2, Colors.RegexWarningBorder, !_isGroupNameValid); if (Im.Item.Deactivated)
if (ImUtf8.InputText("##GroupName"u8, ref text)) {
{ if (_currentGroupName != null && _isGroupNameValid)
_currentGroupEdited = group; ModManager.OptionEditor.RenameModGroup(group, _currentGroupName);
_currentGroupName = text; _currentGroupName = null;
_isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); _currentGroupEdited = null;
} _isGroupNameValid = true;
}
if (ImGui.IsItemDeactivated())
{ var tt = _isGroupNameValid
if (_currentGroupName != null && _isGroupNameValid) ? "Change the Group name."u8
ModManager.OptionEditor.RenameModGroup(group, _currentGroupName); : "Current name can not be used for this group."u8;
_currentGroupName = null; Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, tt);
_currentGroupEdited = null; }
_isGroupNameValid = true;
} private void DrawGroupDelete(IModGroup group)
{
var tt = _isGroupNameValid if (ImEx.Icon.Button(LunaStyle.DeleteIcon, !_deleteEnabled))
? "Change the Group name."u8 ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group));
: "Current name can not be used for this group."u8;
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, tt); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Delete this option group."u8);
} if (!_deleteEnabled)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteModModifier} while clicking to delete.");
private void DrawGroupDelete(IModGroup group) }
{
if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) private void DrawGroupPriority(IModGroup group)
ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group)); {
Im.Item.SetNextWidth(PriorityWidth);
if (_deleteEnabled) if (ImEx.InputOnDeactivation.Scalar("##GroupPriority"u8, group.Priority.Value, out var newPriority))
Im.Tooltip.OnHover("Delete this option group."u8); ModManager.OptionEditor.ChangeGroupPriority(group, new ModPriority(newPriority));
else Im.Tooltip.OnHover("Group Priority"u8);
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, }
$"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete.");
} [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DrawGroupDescription(IModGroup group)
private void DrawGroupPriority(IModGroup group) {
{ if (ImEx.Icon.Button(LunaStyle.EditIcon, "Edit group description."u8))
var priority = _currentGroupEdited == group descriptionPopup.Open(group);
? (_currentGroupPriority ?? group.Priority).Value }
: group.Priority.Value;
Im.Item.SetNextWidth(PriorityWidth); private void DrawGroupMoveButtons(IModGroup group, int idx)
if (ImGui.InputInt("##GroupPriority", ref priority)) {
{ var isFirst = idx is 0;
_currentGroupEdited = group; if (ImEx.Icon.Button(FontAwesomeIcon.ArrowUp.Icon(), isFirst))
_currentGroupPriority = new ModPriority(priority); ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1));
}
if (isFirst)
if (ImGui.IsItemDeactivated()) Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8);
{ else
if (_currentGroupPriority.HasValue) Im.Tooltip.OnHover($"Move this group up to group {idx}.");
ModManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value);
_currentGroupEdited = null;
_currentGroupPriority = null; Im.Line.SameInner();
} var isLast = idx == group.Mod.Groups.Count - 1;
if (ImEx.Icon.Button(FontAwesomeIcon.ArrowDown.Icon(), isLast))
ImGuiUtil.HoverTooltip("Group Priority"); ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1));
}
if (isLast)
[MethodImpl(MethodImplOptions.AggressiveInlining)] Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8);
private void DrawGroupDescription(IModGroup group) else
{ Im.Tooltip.OnHover($"Move this group down to group {idx + 2}.");
if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) }
descriptionPopup.Open(group);
} private void DrawGroupOpenFile(IModGroup group, int idx)
{
private void DrawGroupMoveButtons(IModGroup group, int idx) var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport);
{ var fileExists = File.Exists(fileName);
var isFirst = idx == 0; if (ImEx.Icon.Button(LunaStyle.OpenExternalIcon, !fileExists))
if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) try
ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1)); {
Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true });
if (isFirst) }
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); catch (Exception e)
else {
Im.Tooltip.OnHover($"Move this group up to group {idx}."); Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error);
}
Im.Line.SameInner(); if (fileExists)
var isLast = idx == group.Mod.Groups.Count - 1; Im.Tooltip.OnHover($"Open the {group.Name} json file in the text editor of your choice.");
if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) else
ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1)); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist.");
}
if (isLast)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8);
else [MethodImpl(MethodImplOptions.AggressiveInlining)]
Im.Tooltip.OnHover($"Move this group down to group {idx + 2}."); internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx)
} {
Im.Cursor.FrameAlign();
private void DrawGroupOpenFile(IModGroup group, int idx) Im.Selectable($"Option #{optionIdx + 1}", size: OptionIdxSelectable);
{ Target(group, optionIdx);
var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); Source(option);
var fileExists = File.Exists(fileName); }
if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists))
try [MethodImpl(MethodImplOptions.AggressiveInlining)]
{ internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx)
Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); {
} var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx;
catch (Exception e) if (Im.RadioButton("##default"u8, isDefaultOption))
{ ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx));
Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); Im.Tooltip.OnHover($"Set {option.Name} as the default choice for this group.");
} }
if (fileExists) [MethodImpl(MethodImplOptions.AggressiveInlining)]
Im.Tooltip.OnHover($"Open the {group.Name} json file in the text editor of your choice."); internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx)
else {
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx);
} if (Im.Checkbox("##default"u8, ref isDefaultOption))
ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
Im.Tooltip.OnHover($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group.");
[MethodImpl(MethodImplOptions.AggressiveInlining)] }
internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx)
{ [MethodImpl(MethodImplOptions.AggressiveInlining)]
ImGui.AlignTextToFramePadding(); internal void DrawOptionDescription(IModOption option)
ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: OptionIdxSelectable); {
Target(group, optionIdx); if (ImEx.Icon.Button(LunaStyle.EditIcon, "Edit option description."u8))
Source(option); descriptionPopup.Open(option);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) internal void DrawOptionPriority(MultiSubMod option)
{ {
var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; Im.Item.SetNextWidth(PriorityWidth);
if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) if (ImEx.InputOnDeactivation.Scalar("##Priority"u8, option.Priority.Value, out var newValue))
ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(newValue));
Im.Tooltip.OnHover($"Set {option.Name} as the default choice for this group."); Im.Tooltip.OnHover("Option priority inside the mod."u8);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) internal void DrawOptionName(IModOption option)
{ {
var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); Im.Item.SetNextWidth(_optionNameWidth);
if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) if (ImEx.InputOnDeactivation.Text("##Name"u8, option.Name, out string newName))
ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ModManager.OptionEditor.RenameOption(option, newName);
Im.Tooltip.OnHover($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(MethodImplOptions.AggressiveInlining)] internal void DrawOptionDelete(IModOption option)
internal void DrawOptionDescription(IModOption option) {
{ if (ImEx.Icon.Button(LunaStyle.DeleteIcon, !_deleteEnabled))
if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option));
descriptionPopup.Open(option);
} if (_deleteEnabled)
Im.Tooltip.OnHover("Delete this option."u8);
[MethodImpl(MethodImplOptions.AggressiveInlining)] else
internal void DrawOptionPriority(MultiSubMod option) Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
{ $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete.");
var priority = option.Priority.Value; }
Im.Item.SetNextWidth(PriorityWidth);
if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) [MethodImpl(MethodImplOptions.AggressiveInlining)]
ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); internal string DrawNewOptionBase(IModGroup group, int count)
Im.Tooltip.OnHover("Option priority inside the mod."u8); {
} Im.Cursor.FrameAlign();
Im.Selectable($"Option #{count + 1}", size: OptionIdxSelectable);
[MethodImpl(MethodImplOptions.AggressiveInlining)] Target(group, count);
internal void DrawOptionName(IModOption option)
{ Im.Line.SameInner();
var name = option.Name; Im.FrameDummy();
Im.Item.SetNextWidth(_optionNameWidth);
if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) Im.Line.SameInner();
ModManager.OptionEditor.RenameOption(option, name); Im.Item.SetNextWidth(_optionNameWidth);
} var newName = _newOptionGroup == group
? NewOptionName ?? string.Empty
[MethodImpl(MethodImplOptions.AggressiveInlining)] : string.Empty;
internal void DrawOptionDelete(IModOption option) if (Im.Input.Text("##newOption"u8, ref newName, "Add new option..."u8))
{ {
if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) NewOptionName = newName;
ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option)); _newOptionGroup = group;
}
if (_deleteEnabled)
Im.Tooltip.OnHover("Delete this option."u8); Im.Line.SameInner();
else return newName;
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, }
$"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete.");
} [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Source(IModOption option)
[MethodImpl(MethodImplOptions.AggressiveInlining)] {
internal string DrawNewOptionBase(IModGroup group, int count) using var source = Im.DragDrop.Source();
{ if (!source)
ImGui.AlignTextToFramePadding(); return;
ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable);
Target(group, count); var across = option.Group is ITexToolsGroup;
Im.Line.SameInner(); if (!source.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel))
ImUtf8.IconDummy(); {
_dragDropGroup = option.Group;
Im.Line.SameInner(); _dragDropOption = option;
Im.Item.SetNextWidth(_optionNameWidth); _draggingAcross = across;
var newName = _newOptionGroup == group }
? NewOptionName ?? string.Empty
: string.Empty; Im.Text($"Dragging option {option.Name} from group {option.Group.Name}...");
if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) }
{
NewOptionName = newName; private void Target(IModGroup group, int optionIdx)
_newOptionGroup = group; {
} if (_dragDropGroup != group
&& (!_draggingAcross || _dragDropGroup is not null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))
Im.Line.SameInner(); return;
return newName;
} using var target = Im.DragDrop.Target();
if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel))
[MethodImpl(MethodImplOptions.AggressiveInlining)] return;
private void Source(IModOption option)
{ if (_dragDropGroup is not null && _dragDropOption is not null)
using var source = ImUtf8.DragDropSource(); {
if (!source) if (_dragDropGroup == group)
return; {
var sourceOption = _dragDropOption;
var across = option.Group is ITexToolsGroup; ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx));
}
if (!DragDropSource.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel)) else
{ {
_dragDropGroup = option.Group; // Move from one group to another by deleting, then adding, then moving the option.
_dragDropOption = option; var sourceOption = _dragDropOption;
_draggingAcross = across; ActionQueue.Enqueue(() =>
} {
ModManager.OptionEditor.DeleteOption(sourceOption);
ImUtf8.Text($"Dragging option {option.Name} from group {option.Group.Name}..."); if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption)
} ModManager.OptionEditor.MoveOption(newOption, optionIdx);
});
private void Target(IModGroup group, int optionIdx) }
{ }
if (_dragDropGroup != group
&& (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))) _dragDropGroup = null;
return; _dragDropOption = null;
_draggingAcross = false;
using var target = ImUtf8.DragDropTarget(); }
if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel))
return; [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PrepareStyle()
if (_dragDropGroup != null && _dragDropOption != null) {
{ var totalWidth = 400f * Im.Style.GlobalScale;
if (_dragDropGroup == group) _buttonSize = new Vector2(Im.Style.FrameHeight);
{ PriorityWidth = 50 * Im.Style.GlobalScale;
var sourceOption = _dragDropOption; AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0);
ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx)); _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing);
} _spacing = Im.Style.ItemInnerSpacing.X;
else OptionIdxSelectable = Im.Font.CalculateSize("Option #88."u8);
{ _optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing;
// Move from one group to another by deleting, then adding, then moving the option. _deleteEnabled = config.DeleteModModifier.IsActive();
var sourceOption = _dragDropOption; }
ActionQueue.Enqueue(() => }
{
ModManager.OptionEditor.DeleteOption(sourceOption);
if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption)
ModManager.OptionEditor.MoveOption(newOption, optionIdx);
});
}
}
_dragDropGroup = null;
_dragDropOption = null;
_draggingAcross = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PrepareStyle()
{
var totalWidth = 400f * Im.Style.GlobalScale;
_buttonSize = new Vector2(ImUtf8.FrameHeight);
PriorityWidth = 50 * Im.Style.GlobalScale;
AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0);
_groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing);
_spacing = Im.Style.ItemInnerSpacing.X;
OptionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8);
_optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing;
_deleteEnabled = config.DeleteModModifier.IsActive();
}
}

View file

@ -1,68 +1,65 @@
using Dalamud.Interface; using ImSharp;
using Dalamud.Bindings.ImGui; using Luna;
using ImSharp; using Penumbra.Mods.Groups;
using OtterGui.Raii;
using OtterGui.Text; namespace Penumbra.UI.ModsTab.Groups;
using Penumbra.Mods.Groups;
public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer
namespace Penumbra.UI.ModsTab.Groups; {
public void Draw()
public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer {
{ foreach (var (optionIdx, option) in group.OptionData.Index())
public void Draw() {
{ using var id = Im.Id.Push(optionIdx);
foreach (var (optionIdx, option) in group.OptionData.Index()) editor.DrawOptionPosition(group, option, optionIdx);
{
using var id = ImRaii.PushId(optionIdx); Im.Line.SameInner();
editor.DrawOptionPosition(group, option, optionIdx); editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx);
Im.Line.SameInner(); Im.Line.SameInner();
editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx); editor.DrawOptionName(option);
Im.Line.SameInner(); Im.Line.SameInner();
editor.DrawOptionName(option); editor.DrawOptionDescription(option);
Im.Line.SameInner(); Im.Line.SameInner();
editor.DrawOptionDescription(option); editor.DrawOptionDelete(option);
Im.Line.SameInner(); Im.Line.SameInner();
editor.DrawOptionDelete(option); Im.Dummy(new Vector2(editor.PriorityWidth, 0));
}
Im.Line.SameInner();
ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); DrawNewOption();
} DrawConvertButton();
}
DrawNewOption();
DrawConvertButton(); private void DrawConvertButton()
} {
var convertible = group.Options.Count <= IModGroup.MaxMultiOptions;
private void DrawConvertButton() var g = group;
{ var e = editor.ModManager.OptionEditor.SingleEditor;
var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; if (ImEx.Button("Convert to Multi Group"u8, editor.AvailableWidth, !convertible))
var g = group; editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g));
var e = editor.ModManager.OptionEditor.SingleEditor; if (!convertible)
if (ImUtf8.ButtonEx("Convert to Multi Group", editor.AvailableWidth, !convertible)) Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g)); "Can not convert to multi group since maximum number of options is exceeded."u8);
if (!convertible) }
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
"Can not convert to multi group since maximum number of options is exceeded."u8); private void DrawNewOption()
} {
var count = group.Options.Count;
private void DrawNewOption() if (count >= int.MaxValue)
{ return;
var count = group.Options.Count;
if (count >= int.MaxValue) var name = editor.DrawNewOptionBase(group, count);
return;
var validName = name.Length > 0;
var name = editor.DrawNewOptionBase(group, count); if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, validName
? "Add a new option to this group."u8
var validName = name.Length > 0; : "Please enter a name for the new option."u8, !validName))
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName {
? "Add a new option to this group."u8 editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name);
: "Please enter a name for the new option."u8, default, !validName)) editor.NewOptionName = null;
{ }
editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name); }
editor.NewOptionName = null; }
}
}
}

View file

@ -184,7 +184,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
// Customization points. // Customization points.
public override ISortMode<Mod> SortMode public override ISortMode<Mod> SortMode
=> _config.SortMode; => ISortMode<Mod>.FoldersFirst;
protected override uint ExpandedFolderColor protected override uint ExpandedFolderColor
=> ColorId.FolderExpanded.Value().Color; => ColorId.FolderExpanded.Value().Color;

View file

@ -31,7 +31,7 @@ public static class ModFilterExtensions
{ {
public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 22) - 1); public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 22) - 1);
public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs = public static readonly IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs =
[ [
(ModFilter.Enabled, ModFilter.Disabled, "Enabled"), (ModFilter.Enabled, ModFilter.Disabled, "Enabled"),
(ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"), (ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"),
@ -43,7 +43,7 @@ public static class ModFilterExtensions
(ModFilter.Temporary, ModFilter.NotTemporary, "Temporary"), (ModFilter.Temporary, ModFilter.NotTemporary, "Temporary"),
]; ];
public static IReadOnlyList<IReadOnlyList<(ModFilter Filter, string Name)>> Groups = public static readonly IReadOnlyList<IReadOnlyList<(ModFilter Filter, string Name)>> Groups =
[ [
[ [
(ModFilter.NoConflict, "Has No Conflicts"), (ModFilter.NoConflict, "Has No Conflicts"),

View file

@ -1,74 +1,78 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using ImSharp; using ImSharp;
using Penumbra.Mods; using Luna;
using Penumbra.Services; using Penumbra.Mods;
using Penumbra.Services;
namespace Penumbra.UI.ModsTab;
namespace Penumbra.UI.ModsTab;
public class ModPanel : IDisposable, Luna.IUiService
{ public class ModPanel : IDisposable, IPanel
private readonly MultiModPanel _multiModPanel; {
private readonly ModSelection _selection; private readonly MultiModPanel _multiModPanel;
private readonly ModPanelHeader _header; private readonly ModSelection _selection;
private readonly ModPanelTabBar _tabs; private readonly ModPanelHeader _header;
private bool _resetCursor; private readonly ModPanelTabBar _tabs;
private bool _resetCursor;
public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModPanelTabBar tabs,
MultiModPanel multiModPanel, CommunicatorService communicator) public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModPanelTabBar tabs,
{ MultiModPanel multiModPanel, CommunicatorService communicator)
_selection = selection; {
_tabs = tabs; _selection = selection;
_multiModPanel = multiModPanel; _tabs = tabs;
_header = new ModPanelHeader(pi, communicator); _multiModPanel = multiModPanel;
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel); _header = new ModPanelHeader(pi, communicator);
OnSelectionChange(new ModSelection.Arguments(null, _selection.Mod)); _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel);
} OnSelectionChange(new ModSelection.Arguments(null, _selection.Mod));
}
public void Draw()
{ public ReadOnlySpan<byte> Id
if (!_valid) => "MP"u8;
{
_multiModPanel.Draw(); public void Draw()
return; {
} if (!_valid)
{
if (_resetCursor) _multiModPanel.Draw();
{ return;
_resetCursor = false; }
Im.Scroll.X = 0;
} if (_resetCursor)
{
_header.Draw(); _resetCursor = false;
Im.Cursor.X += Im.Scroll.X; Im.Scroll.X = 0;
using var child = Im.Child.Begin("Tabs"u8, }
Im.ContentRegion.Available with { X = Im.Window.MaximumContentRegion.X - Im.Window.MinimumContentRegion.X });
if (child) _header.Draw();
_tabs.Draw(_mod); Im.Cursor.X += Im.Scroll.X;
} using var child = Im.Child.Begin("Tabs"u8,
Im.ContentRegion.Available with { X = Im.Window.MaximumContentRegion.X - Im.Window.MinimumContentRegion.X });
public void Dispose() if (child)
{ _tabs.Draw(_mod);
_selection.Unsubscribe(OnSelectionChange); }
_header.Dispose();
} public void Dispose()
{
private bool _valid; _selection.Unsubscribe(OnSelectionChange);
private Mod _mod = null!; _header.Dispose();
}
private void OnSelectionChange(in ModSelection.Arguments arguments)
{ private bool _valid;
_resetCursor = true; private Mod _mod = null!;
if (arguments.NewSelection is null || _selection.Mod is null)
{ private void OnSelectionChange(in ModSelection.Arguments arguments)
_valid = false; {
} _resetCursor = true;
else if (arguments.NewSelection is null || _selection.Mod is null)
{ {
_valid = true; _valid = false;
_mod = arguments.NewSelection; }
_header.ChangeMod(_mod); else
_tabs.Settings.Reset(); {
_tabs.Edit.Reset(); _valid = true;
} _mod = arguments.NewSelection;
} _header.ChangeMod(_mod);
} _tabs.Settings.Reset();
_tabs.Edit.Reset();
}
}
}

View file

@ -1,138 +1,137 @@
using Dalamud.Bindings.ImGui; using ImSharp;
using ImSharp; using Luna;
using Luna; using Penumbra.Collections;
using Penumbra.Collections; using Penumbra.Collections.Manager;
using Penumbra.Collections.Manager; using Penumbra.Mods;
using Penumbra.Mods; using Penumbra.UI.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab;
public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab<ModPanelTab>
{
private enum ModState
{
Enabled,
Disabled,
Unconfigured,
}
private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = [];
public ReadOnlySpan<byte> Label
=> "Collections"u8;
public ModPanelTab Identifier namespace Penumbra.UI.ModsTab;
public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab<ModPanelTab>
{
private enum ModState
{
Enabled,
Disabled,
Unconfigured,
}
private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = [];
public ReadOnlySpan<byte> Label
=> "Collections"u8;
public ModPanelTab Identifier
=> ModPanelTab.Collections; => ModPanelTab.Collections;
public void DrawContent() public void DrawContent()
{ {
var (direct, inherited) = CountUsage(selector.Selected!); var (direct, inherited) = CountUsage(selector.Selected!);
Im.Line.New(); Im.Line.New();
switch (direct) switch (direct)
{ {
case 1: Im.Text("This Mod is directly configured in 1 collection."u8); break; case 1: Im.Text("This Mod is directly configured in 1 collection."u8); break;
case 0: Im.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); break; case 0: Im.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); break;
default: Im.Text($"This Mod is directly configured in {direct} collections."); break; default: Im.Text($"This Mod is directly configured in {direct} collections."); break;
} }
if (inherited > 0) if (inherited > 0)
Im.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); Im.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance.");
Im.Line.New(); Im.Line.New();
Im.Separator(); Im.Separator();
Im.Line.New(); Im.Line.New();
using var table = Im.Table.Begin("##modCollections"u8, 3, TableFlags.SizingFixedFit | TableFlags.RowBackground); using var table = Im.Table.Begin("##modCollections"u8, 3, TableFlags.SizingFixedFit | TableFlags.RowBackground);
if (!table) if (!table)
return; return;
var size = Im.Font.CalculateSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale; var size = Im.Font.CalculateSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale;
var collectionSize = 200 * Im.Style.GlobalScale; var collectionSize = 200 * Im.Style.GlobalScale;
table.SetupColumn("Collection"u8, TableColumnFlags.WidthFixed, collectionSize); table.SetupColumn("Collection"u8, TableColumnFlags.WidthFixed, collectionSize);
table.SetupColumn("State"u8, TableColumnFlags.WidthFixed, size); table.SetupColumn("State"u8, TableColumnFlags.WidthFixed, size);
table.SetupColumn("Inherited From"u8, TableColumnFlags.WidthFixed, collectionSize); table.SetupColumn("Inherited From"u8, TableColumnFlags.WidthFixed, collectionSize);
table.HeaderRow();
ImGui.TableHeadersRow();
foreach (var (idx, (collection, parent, color, state)) in _cache.Index()) foreach (var (idx, (collection, parent, color, state)) in _cache.Index())
{ {
using var id = Im.Id.Push(idx); using var id = Im.Id.Push(idx);
table.DrawColumn(collection.Identity.Name); table.DrawColumn(collection.Identity.Name);
table.NextColumn(); table.NextColumn();
Im.Text(ToText(state), color); Im.Text(ToText(state), color);
using (var context = Im.Popup.BeginContextItem("Context"u8)) using (var context = Im.Popup.BeginContextItem("Context"u8))
{ {
if (context) if (context)
{ {
Im.Text(collection.Identity.Name); Im.Text(collection.Identity.Name);
Im.Separator(); Im.Separator();
using (Im.Disabled(state is ModState.Enabled && parent == collection)) using (Im.Disabled(state is ModState.Enabled && parent == collection))
{ {
if (Im.Menu.Item("Enable"u8)) if (Im.Menu.Item("Enable"u8))
{ {
if (parent != collection) if (parent != collection)
manager.Editor.SetModInheritance(collection, selector.Selected!, false); manager.Editor.SetModInheritance(collection, selector.Selected!, false);
manager.Editor.SetModState(collection, selector.Selected!, true); manager.Editor.SetModState(collection, selector.Selected!, true);
} }
} }
using (Im.Disabled(state is ModState.Disabled && parent == collection)) using (Im.Disabled(state is ModState.Disabled && parent == collection))
{ {
if (Im.Menu.Item("Disable"u8)) if (Im.Menu.Item("Disable"u8))
{ {
if (parent != collection) if (parent != collection)
manager.Editor.SetModInheritance(collection, selector.Selected!, false); manager.Editor.SetModInheritance(collection, selector.Selected!, false);
manager.Editor.SetModState(collection, selector.Selected!, false); manager.Editor.SetModState(collection, selector.Selected!, false);
} }
} }
using (Im.Disabled(parent != collection)) using (Im.Disabled(parent != collection))
{ {
if (Im.Menu.Item("Inherit"u8)) if (Im.Menu.Item("Inherit"u8))
manager.Editor.SetModInheritance(collection, selector.Selected!, true); manager.Editor.SetModInheritance(collection, selector.Selected!, true);
} }
} }
} }
table.DrawColumn(parent == collection ? StringU8.Empty : parent.Identity.Name); table.DrawColumn(parent == collection ? StringU8.Empty : parent.Identity.Name);
} }
} }
private static ReadOnlySpan<byte> ToText(ModState state) private static ReadOnlySpan<byte> ToText(ModState state)
=> state switch => state switch
{ {
ModState.Unconfigured => "Unconfigured"u8, ModState.Unconfigured => "Unconfigured"u8,
ModState.Enabled => "Enabled"u8, ModState.Enabled => "Enabled"u8,
ModState.Disabled => "Disabled"u8, ModState.Disabled => "Disabled"u8,
_ => "Unknown"u8, _ => "Unknown"u8,
}; };
private (int Direct, int Inherited) CountUsage(Mod mod) private (int Direct, int Inherited) CountUsage(Mod mod)
{ {
_cache.Clear(); _cache.Clear();
var undefined = ColorId.UndefinedMod.Value(); var undefined = ColorId.UndefinedMod.Value();
var enabled = ColorId.EnabledMod.Value(); var enabled = ColorId.EnabledMod.Value();
var inherited = ColorId.InheritedMod.Value(); var inherited = ColorId.InheritedMod.Value();
var disabled = ColorId.DisabledMod.Value(); var disabled = ColorId.DisabledMod.Value();
var disInherited = ColorId.InheritedDisabledMod.Value(); var disInherited = ColorId.InheritedDisabledMod.Value();
var directCount = 0; var directCount = 0;
var inheritedCount = 0; var inheritedCount = 0;
foreach (var collection in manager.Storage) foreach (var collection in manager.Storage)
{ {
var (settings, parent) = collection.GetInheritedSettings(mod.Index); var (settings, parent) = collection.GetInheritedSettings(mod.Index);
var (color, text) = settings == null var (color, text) = settings == null
? (undefined, ModState.Unconfigured) ? (undefined, ModState.Unconfigured)
: settings.Enabled : settings.Enabled
? (parent == collection ? enabled : inherited, ModState.Enabled) ? (parent == collection ? enabled : inherited, ModState.Enabled)
: (parent == collection ? disabled : disInherited, ModState.Disabled); : (parent == collection ? disabled : disInherited, ModState.Disabled);
_cache.Add((collection, parent, color.Color, text)); _cache.Add((collection, parent, color.Color, text));
if (color == enabled) if (color == enabled)
++directCount; ++directCount;
else if (color == inherited) else if (color == inherited)
++inheritedCount; ++inheritedCount;
} }
return (directCount, inheritedCount); return (directCount, inheritedCount);
} }
} }

View file

@ -1,58 +1,58 @@
using Dalamud.Bindings.ImGui; using ImSharp;
using ImSharp; using Luna;
using Luna; using OtterGui.Widgets;
using OtterGui.Widgets; using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager; using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab;
public class ModPanelDescriptionTab(
ModFileSystemSelector selector,
TutorialService tutorial,
ModManager modManager,
PredefinedTagManager predefinedTagsConfig)
: ITab<ModPanelTab>
{
private readonly TagButtons _localTags = new();
private readonly TagButtons _modTags = new();
public ReadOnlySpan<byte> Label
=> "Description"u8;
public ModPanelTab Identifier namespace Penumbra.UI.ModsTab;
public class ModPanelDescriptionTab(
ModFileSystemSelector selector,
TutorialService tutorial,
ModManager modManager,
PredefinedTagManager predefinedTagsConfig)
: ITab<ModPanelTab>
{
private readonly TagButtons _localTags = new();
private readonly TagButtons _modTags = new();
public ReadOnlySpan<byte> Label
=> "Description"u8;
public ModPanelTab Identifier
=> ModPanelTab.Description; => ModPanelTab.Description;
public void DrawContent() public void DrawContent()
{ {
using var child = Im.Child.Begin("##description"u8); using var child = Im.Child.Begin("##description"u8);
if (!child) if (!child)
return; return;
Im.ScaledDummy(2, 2); Im.ScaledDummy(2, 2);
Im.ScaledDummy(2, 2); Im.ScaledDummy(2, 2);
var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled
? (true, Im.Style.FrameHeight + Im.Style.WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? Im.Style.ScrollbarSize : 0)) ? (true, Im.Style.FrameHeight + Im.Style.WindowPadding.X + (Im.Scroll.MaximumY > 0 ? Im.Style.ScrollbarSize : 0))
: (false, 0); : (false, 0);
var tagIdx = _localTags.Draw("Local Tags: ", var tagIdx = _localTags.Draw("Local Tags: ",
"Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n"
+ "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags, + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags,
out var editedTag, rightEndOffset: predefinedTagButtonOffset); out var editedTag, rightEndOffset: predefinedTagButtonOffset);
tutorial.OpenTutorial(BasicTutorialSteps.Tags); tutorial.OpenTutorial(BasicTutorialSteps.Tags);
if (tagIdx >= 0) if (tagIdx >= 0)
modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag); modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag);
if (predefinedTagsEnabled) if (predefinedTagsEnabled)
predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true, predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true,
selector.Selected!); selector.Selected!);
if (selector.Selected!.ModTags.Count > 0) if (selector.Selected!.ModTags.Count > 0)
_modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.",
selector.Selected!.ModTags, out _, false, selector.Selected!.ModTags, out _, false,
ImGui.CalcTextSize("Local ").X - ImGui.CalcTextSize("Mod ").X); Im.Font.CalculateSize("Local "u8).X - Im.Font.CalculateSize("Mod "u8).X);
Im.ScaledDummy(2, 2); Im.ScaledDummy(2, 2);
Im.Separator(); Im.Separator();
Im.TextWrapped(selector.Selected!.Description); Im.TextWrapped(selector.Selected!.Description);
} }
} }

View file

@ -182,7 +182,7 @@ public class ModPanelEditTab(
var tt = fileExists var tt = fileExists
? "Open the metadata json file in the text editor of your choice."u8 ? "Open the metadata json file in the text editor of your choice."u8
: "The metadata json file does not exist."u8; : "The metadata json file does not exist."u8;
using (Im.Id.Push("meta")) using (Im.Id.Push("meta"u8))
{ {
if (ImEx.Icon.Button(LunaStyle.FileExportIcon, tt, !fileExists)) if (ImEx.Icon.Button(LunaStyle.FileExportIcon, tt, !fileExists))
Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true });

View file

@ -1,269 +1,258 @@
using Dalamud.Bindings.ImGui; using Dalamud.Interface.GameFonts;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Plugin;
using Dalamud.Plugin; using ImSharp;
using ImSharp; using Penumbra.Communication;
using OtterGui; using Penumbra.Mods;
using OtterGui.Raii; using Penumbra.Mods.Manager;
using Penumbra.Communication; using Penumbra.Services;
using Penumbra.Mods; using Penumbra.UI.Classes;
using Penumbra.Mods.Manager;
using Penumbra.Services; namespace Penumbra.UI.ModsTab;
using Penumbra.UI.Classes;
public class ModPanelHeader : IDisposable
namespace Penumbra.UI.ModsTab; {
/// <summary> We use a big, nice game font for the title. </summary>
public class ModPanelHeader : IDisposable private readonly IFontHandle _nameFont;
{
/// <summary> We use a big, nice game font for the title. </summary> private readonly CommunicatorService _communicator;
private readonly IFontHandle _nameFont; private float _lastPreSettingsHeight;
private bool _dirty = true;
private readonly CommunicatorService _communicator;
private float _lastPreSettingsHeight; public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator)
private bool _dirty = true; {
_communicator = communicator;
public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator) _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23));
{ _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModPanelHeader);
_communicator = communicator; }
_nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23));
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModPanelHeader); /// <summary>
} /// Draw the header for the current mod,
/// consisting of its name, version, author and website, if they exist.
/// <summary> /// </summary>
/// Draw the header for the current mod, public void Draw()
/// consisting of its name, version, author and website, if they exist. {
/// </summary> UpdateModData();
public void Draw() var height = Im.ContentRegion.Available.Y;
{ var maxHeight = 3 * height / 4;
UpdateModData(); using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers
var height = Im.ContentRegion.Available.Y; ? Im.Child.Begin("HeaderChild"u8, Im.ContentRegion.Available with { Y = maxHeight })
var maxHeight = 3 * height / 4; : default;
using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers using (Im.Group())
? ImRaii.Child("HeaderChild", new Vector2(Im.ContentRegion.Available.X, maxHeight), false) {
: null; var offset = DrawModName();
using (Im.Group()) DrawVersion(offset);
{ DrawSecondRow(offset);
var offset = DrawModName(); }
DrawVersion(offset);
DrawSecondRow(offset); _communicator.PreSettingsTabBarDraw.Invoke(new PreSettingsTabBarDraw.Arguments(_mod, Im.Item.Size.X, _nameWidth));
} _lastPreSettingsHeight = Im.Cursor.Position.Y;
}
_communicator.PreSettingsTabBarDraw.Invoke(new PreSettingsTabBarDraw.Arguments(_mod, ImGui.GetItemRectSize().X, _nameWidth));
_lastPreSettingsHeight = ImGui.GetCursorPosY(); public void ChangeMod(Mod mod)
} {
_mod = mod;
public void ChangeMod(Mod mod) _dirty = true;
{ }
_mod = mod;
_dirty = true; /// <summary>
} /// Update all mod header data. Should someone change frame padding or item spacing,
/// or his default font, this will break, but he will just have to select a different mod to restore.
/// <summary> /// </summary>
/// Update all mod header data. Should someone change frame padding or item spacing, private void UpdateModData()
/// or his default font, this will break, but he will just have to select a different mod to restore. {
/// </summary> if (!_dirty)
private void UpdateModData() return;
{
if (!_dirty) _dirty = false;
return; _lastPreSettingsHeight = 0;
// Name
_dirty = false; var name = $" {_mod.Name} ";
_lastPreSettingsHeight = 0; if (name != _modName)
// Name {
var name = $" {_mod.Name} "; using var f = _nameFont.Push();
if (name != _modName) _modName = name;
{ _modNameWidth = Im.Font.CalculateSize(name).X + 2 * (Im.Style.FramePadding.X + 2 * Im.Style.GlobalScale);
using var f = _nameFont.Push(); }
_modName = name;
_modNameWidth = ImGui.CalcTextSize(name).X + 2 * (Im.Style.FramePadding.X + 2 * Im.Style.GlobalScale); // Author
} if (_mod.Author != _modAuthor)
{
// Author var author = _mod.Author.Length is 0 ? string.Empty : $"by {_mod.Author}";
if (_mod.Author != _modAuthor) _modAuthor = _mod.Author;
{ _modAuthorWidth = Im.Font.CalculateSize(author).X;
var author = _mod.Author.Length is 0 ? string.Empty : $"by {_mod.Author}"; _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X;
_modAuthor = _mod.Author; }
_modAuthorWidth = ImGui.CalcTextSize(author).X;
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X; // Version
} var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty;
if (version != _modVersion)
// Version {
var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; _modVersion = version;
if (version != _modVersion) _modVersionWidth = Im.Font.CalculateSize(version).X;
{ }
_modVersion = version;
_modVersionWidth = ImGui.CalcTextSize(version).X; // Website
} if (_modWebsite != _mod.Website)
{
// Website _modWebsite = _mod.Website;
if (_modWebsite != _mod.Website) _websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult)
{ && (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp);
_modWebsite = _mod.Website; _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}";
_websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult) _modWebsiteButtonWidth = _websiteValid
&& (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp); ? Im.Font.CalculateSize(_modWebsiteButton).X + 2 * Im.Style.FramePadding.X
_modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; : Im.Font.CalculateSize(_modWebsiteButton).X;
_modWebsiteButtonWidth = _websiteValid _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X;
? ImGui.CalcTextSize(_modWebsiteButton).X + 2 * Im.Style.FramePadding.X }
: ImGui.CalcTextSize(_modWebsiteButton).X; }
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X;
} public void Dispose()
} {
_nameFont.Dispose();
public void Dispose() _communicator.ModDataChanged.Unsubscribe(OnModDataChange);
{ }
_nameFont.Dispose();
_communicator.ModDataChanged.Unsubscribe(OnModDataChange); // Header data.
} private Mod _mod = null!;
private string _modName = string.Empty;
// Header data. private string _modAuthor = string.Empty;
private Mod _mod = null!; private string _modVersion = string.Empty;
private string _modName = string.Empty; private string _modWebsite = string.Empty;
private string _modAuthor = string.Empty; private string _modWebsiteButton = string.Empty;
private string _modVersion = string.Empty; private bool _websiteValid;
private string _modWebsite = string.Empty;
private string _modWebsiteButton = string.Empty; private float _modNameWidth;
private bool _websiteValid; private float _modAuthorWidth;
private float _modVersionWidth;
private float _modNameWidth; private float _modWebsiteButtonWidth;
private float _modAuthorWidth; private float _secondRowWidth;
private float _modVersionWidth;
private float _modWebsiteButtonWidth; private float _nameWidth;
private float _secondRowWidth;
/// <summary>
private float _nameWidth; /// Draw the mod name in the game font with a 2px border, centered,
/// with at least the width of the version space to each side.
/// <summary> /// </summary>
/// Draw the mod name in the game font with a 2px border, centered, private float DrawModName()
/// with at least the width of the version space to each side. {
/// </summary> var decidingWidth = Math.Max(_secondRowWidth, Im.Window.Width);
private float DrawModName() var offsetWidth = (decidingWidth - _modNameWidth) / 2;
{ var offsetVersion = _modVersion.Length > 0
var decidingWidth = Math.Max(_secondRowWidth, ImGui.GetWindowWidth()); ? _modVersionWidth + Im.Style.ItemSpacing.X + Im.Style.WindowPadding.X
var offsetWidth = (decidingWidth - _modNameWidth) / 2; : 0;
var offsetVersion = _modVersion.Length > 0 var offset = Math.Max(offsetWidth, offsetVersion);
? _modVersionWidth + Im.Style.ItemSpacing.X + Im.Style.WindowPadding.X if (offset > 0)
: 0; Im.Cursor.X = offset;
var offset = Math.Max(offsetWidth, offsetVersion);
if (offset > 0) using var style = ImStyleBorder.Frame.Push(Colors.MetaInfoText, 2 * Im.Style.GlobalScale);
{ using var f = _nameFont.Push();
ImGui.SetCursorPosX(offset); ImEx.TextFramed(_modName, Vector2.Zero, 0);
} _nameWidth = Im.Item.Size.X;
return offset;
using var style = ImStyleBorder.Frame.Push(Colors.MetaInfoText, 2 * Im.Style.GlobalScale); }
using var f = _nameFont.Push();
ImGuiUtil.DrawTextButton(_modName, Vector2.Zero, 0); /// <summary> Draw the version in the top-right corner. </summary>
_nameWidth = ImGui.GetItemRectSize().X; private void DrawVersion(float offset)
return offset; {
} var oldPos = Im.Cursor.Position;
Im.Cursor.Position = new Vector2(2 * offset + _modNameWidth - _modVersionWidth - Im.Style.WindowPadding.X,
/// <summary> Draw the version in the top-right corner. </summary> Im.Style.FramePadding.Y);
private void DrawVersion(float offset) Im.Text(_modVersion, Colors.MetaInfoText);
{ Im.Cursor.Position = oldPos;
var oldPos = ImGui.GetCursorPos(); }
ImGui.SetCursorPos(new Vector2(2 * offset + _modNameWidth - _modVersionWidth - Im.Style.WindowPadding.X,
Im.Style.FramePadding.Y)); /// <summary>
ImGuiUtil.TextColored(Colors.MetaInfoText, _modVersion); /// Draw author and website if they exist. The website is a button if it is valid.
ImGui.SetCursorPos(oldPos); /// Usually, author begins at the left boundary of the name,
} /// and website ends at the right boundary of the name.
/// If their combined width is larger than the name, they are combined-centered.
/// <summary> /// </summary>
/// Draw author and website if they exist. The website is a button if it is valid. private void DrawSecondRow(float offset)
/// Usually, author begins at the left boundary of the name, {
/// and website ends at the right boundary of the name. if (_modAuthor.Length == 0)
/// If their combined width is larger than the name, they are combined-centered. {
/// </summary> if (_modWebsiteButton.Length == 0)
private void DrawSecondRow(float offset) {
{ Im.Line.New();
if (_modAuthor.Length == 0) return;
{ }
if (_modWebsiteButton.Length == 0)
{ offset += (_modNameWidth - _modWebsiteButtonWidth) / 2;
Im.Line.New(); Im.Cursor.X = offset;
return; DrawWebsite();
} }
else if (_modWebsiteButton.Length == 0)
offset += (_modNameWidth - _modWebsiteButtonWidth) / 2; {
ImGui.SetCursorPosX(offset); offset += (_modNameWidth - _modAuthorWidth) / 2;
DrawWebsite(); Im.Cursor.X = offset;
} DrawAuthor();
else if (_modWebsiteButton.Length == 0) }
{ else if (_secondRowWidth < _modNameWidth)
offset += (_modNameWidth - _modAuthorWidth) / 2; {
ImGui.SetCursorPosX(offset); Im.Cursor.X = offset;
DrawAuthor(); DrawAuthor();
} Im.Line.Same(offset + _modNameWidth - _modWebsiteButtonWidth);
else if (_secondRowWidth < _modNameWidth) DrawWebsite();
{ }
ImGui.SetCursorPosX(offset); else
DrawAuthor(); {
Im.Line.Same(offset + _modNameWidth - _modWebsiteButtonWidth); offset -= (_secondRowWidth - _modNameWidth) / 2;
DrawWebsite(); if (offset > 0)
} Im.Cursor.X = offset;
else
{ DrawAuthor();
offset -= (_secondRowWidth - _modNameWidth) / 2; Im.Line.Same();
if (offset > 0) DrawWebsite();
{ }
ImGui.SetCursorPosX(offset); }
}
/// <summary> Draw the author text. </summary>
DrawAuthor(); private void DrawAuthor()
Im.Line.Same(); {
DrawWebsite(); Im.Text("by "u8, Colors.MetaInfoText);
} Im.Line.NoSpacing();
} Im.Text(_modAuthor);
}
/// <summary> Draw the author text. </summary>
private void DrawAuthor() /// <summary>
{ /// Draw either a website button if the source is a valid website address,
using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); /// or a source text if it is not.
ImGuiUtil.TextColored(Colors.MetaInfoText, "by "); /// </summary>
Im.Line.Same(); private void DrawWebsite()
style.Pop(); {
Im.Text(_modAuthor); if (_websiteValid)
} {
if (Im.SmallButton(_modWebsiteButton))
/// <summary> {
/// Draw either a website button if the source is a valid website address, try
/// or a source text if it is not. {
/// </summary> var process = new ProcessStartInfo(_modWebsite)
private void DrawWebsite() {
{ UseShellExecute = true,
if (_websiteValid) };
{ Process.Start(process);
if (ImGui.SmallButton(_modWebsiteButton)) }
{ catch
try {
{ // ignored
var process = new ProcessStartInfo(_modWebsite) }
{ }
UseShellExecute = true,
}; Im.Tooltip.OnHover(_modWebsite);
Process.Start(process); }
} else
catch {
{ Im.Text("from "u8, Colors.MetaInfoText);
// ignored Im.Line.NoSpacing();
} Im.Text(_modWebsite);
} }
}
ImGuiUtil.HoverTooltip(_modWebsite);
} /// <summary> Just update the data when any relevant field changes. </summary>
else private void OnModDataChange(in ModDataChanged.Arguments arguments)
{ {
using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); const ModDataChangeType relevantChanges =
ImGuiUtil.TextColored(Colors.MetaInfoText, "from "); ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version;
Im.Line.Same(); _dirty = (arguments.Type & relevantChanges) is not 0;
style.Pop(); }
Im.Text(_modWebsite); }
}
}
/// <summary> Just update the data when any relevant field changes. </summary>
private void OnModDataChange(in ModDataChanged.Arguments arguments)
{
const ModDataChangeType relevantChanges =
ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version;
_dirty = (arguments.Type & relevantChanges) is not 0;
}
}

View file

@ -1,264 +1,264 @@
using Dalamud.Bindings.ImGui; using ImSharp;
using ImSharp; using Luna;
using Luna; using Penumbra.UI.Classes;
using OtterGui.Raii; using Penumbra.Collections.Manager;
using OtterGui.Text; using Penumbra.Communication;
using Penumbra.UI.Classes; using Penumbra.Mods;
using Penumbra.Collections.Manager; using Penumbra.Mods.Manager;
using Penumbra.Communication; using Penumbra.Services;
using Penumbra.Mods; using Penumbra.Mods.Settings;
using Penumbra.Mods.Manager; using Penumbra.UI.ModsTab.Groups;
using Penumbra.Services;
using Penumbra.Mods.Settings;
using Penumbra.UI.ModsTab.Groups;
namespace Penumbra.UI.ModsTab;
public class ModPanelSettingsTab(
CollectionManager collectionManager,
ModManager modManager,
ModSelection selection,
TutorialService tutorial,
CommunicatorService communicator,
ModGroupDrawer modGroupDrawer,
Configuration config)
: ITab<ModPanelTab>
{
private bool _inherited;
private bool _temporary;
private bool _locked;
private int? _currentPriority;
public ReadOnlySpan<byte> Label
=> "Settings"u8;
public ModPanelTab Identifier namespace Penumbra.UI.ModsTab;
public class ModPanelSettingsTab(
CollectionManager collectionManager,
ModManager modManager,
ModSelection selection,
TutorialService tutorial,
CommunicatorService communicator,
ModGroupDrawer modGroupDrawer,
Configuration config)
: ITab<ModPanelTab>
{
private bool _inherited;
private bool _temporary;
private bool _locked;
private int? _currentPriority;
public ReadOnlySpan<byte> Label
=> "Settings"u8;
public ModPanelTab Identifier
=> ModPanelTab.Settings; => ModPanelTab.Settings;
public void PostTabButton() public void PostTabButton()
=> tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); => tutorial.OpenTutorial(BasicTutorialSteps.ModOptions);
public void Reset() public void Reset()
=> _currentPriority = null; => _currentPriority = null;
public void DrawContent() public void DrawContent()
{ {
using var table = Im.Table.Begin("##settings"u8, 1, TableFlags.ScrollY, Im.ContentRegion.Available); using var table = Im.Table.Begin("##settings"u8, 1, TableFlags.ScrollY, Im.ContentRegion.Available);
if (!table) if (!table)
return; return;
_inherited = selection.Collection != collectionManager.Active.Current; _inherited = selection.Collection != collectionManager.Active.Current;
_temporary = selection.TemporarySettings != null; _temporary = selection.TemporarySettings != null;
_locked = (selection.TemporarySettings?.Lock ?? 0) > 0; _locked = (selection.TemporarySettings?.Lock ?? 0) > 0;
ImGui.TableSetupScrollFreeze(0, 1); table.SetupScrollFreeze(0, 1);
ImGui.TableNextColumn(); table.NextColumn();
DrawTemporaryWarning(); DrawTemporaryWarning();
DrawInheritedWarning(); DrawInheritedWarning();
ImGui.Dummy(Vector2.Zero); Im.Dummy(Vector2.Zero);
communicator.PreSettingsPanelDraw.Invoke(new PreSettingsPanelDraw.Arguments(selection.Mod!)); communicator.PreSettingsPanelDraw.Invoke(new PreSettingsPanelDraw.Arguments(selection.Mod!));
DrawEnabledInput(); DrawEnabledInput();
tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods);
Im.Line.Same(); Im.Line.Same();
DrawPriorityInput(); DrawPriorityInput();
tutorial.OpenTutorial(BasicTutorialSteps.Priority); tutorial.OpenTutorial(BasicTutorialSteps.Priority);
DrawRemoveSettings(); DrawRemoveSettings();
ImGui.TableNextColumn(); table.NextColumn();
communicator.PostEnabledDraw.Invoke(new PostEnabledDraw.Arguments(selection.Mod!)); communicator.PostEnabledDraw.Invoke(new PostEnabledDraw.Arguments(selection.Mod!));
modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings);
UiHelpers.DefaultLineSpace(); UiHelpers.DefaultLineSpace();
communicator.PostSettingsPanelDraw.Invoke(new PostSettingsPanelDraw.Arguments(selection.Mod!)); communicator.PostSettingsPanelDraw.Invoke(new PostSettingsPanelDraw.Arguments(selection.Mod!));
} }
/// <summary> Draw a big tinted bar if the current setting is temporary. </summary> /// <summary> Draw a big tinted bar if the current setting is temporary. </summary>
private void DrawTemporaryWarning() private void DrawTemporaryWarning()
{ {
if (!_temporary) if (!_temporary)
return; return;
using var color = ImGuiColor.Button.Push(Rgba32.TintColor(Im.Style[ImGuiColor.Button], ColorId.TemporaryModSettingsTint.Value().ToVector())); using var color =
var width = new Vector2(Im.ContentRegion.Available.X, 0); ImGuiColor.Button.Push(Rgba32.TintColor(Im.Style[ImGuiColor.Button], ColorId.TemporaryModSettingsTint.Value().ToVector()));
if (ImUtf8.ButtonEx($"These settings are temporarily set by {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", var width = Im.ContentRegion.Available with { Y = 0 };
width, if (ImEx.Button($"These settings are temporarily set by {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}",
_locked)) width, _locked))
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null);
Im.Tooltip.OnHover("Changing settings in temporary settings will not save them across sessions.\n"u8 Im.Tooltip.OnHover("Changing settings in temporary settings will not save them across sessions.\n"u8
+ "You can click this button to remove the temporary settings and return to your normal settings."u8); + "You can click this button to remove the temporary settings and return to your normal settings."u8);
} }
/// <summary> Draw a big red bar if the current setting is inherited. </summary> /// <summary> Draw a big red bar if the current setting is inherited. </summary>
private void DrawInheritedWarning() private void DrawInheritedWarning()
{ {
if (!_inherited) if (!_inherited)
return; return;
using var color = ImGuiColor.Button.Push(Colors.PressEnterWarningBg); using var color = ImGuiColor.Button.Push(Colors.PressEnterWarningBg);
var width = new Vector2(Im.ContentRegion.Available.X, 0); var width = Im.ContentRegion.Available with { Y = 0 };
if (ImUtf8.ButtonEx($"These settings are inherited from {selection.Collection.Identity.Name}.", width, _locked)) if (ImEx.Button($"These settings are inherited from {selection.Collection.Identity.Name}.", width, _locked))
{ {
if (_temporary) if (_temporary)
{ {
selection.TemporarySettings!.ForceInherit = false; selection.TemporarySettings!.ForceInherit = false;
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings);
} }
else else
{ {
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false);
} }
} }
Im.Tooltip.OnHover("You can click this button to copy the current settings to the current selection.\n"u8 Im.Tooltip.OnHover("You can click this button to copy the current settings to the current selection.\n"u8
+ "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."u8); + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."u8);
} }
/// <summary> Draw a checkbox for the enabled status of the mod. </summary> /// <summary> Draw a checkbox for the enabled status of the mod. </summary>
private void DrawEnabledInput() private void DrawEnabledInput()
{ {
var enabled = selection.Settings.Enabled; var enabled = selection.Settings.Enabled;
using var disabled = ImRaii.Disabled(_locked); using var disabled = Im.Disabled(_locked);
if (!ImUtf8.Checkbox("Enabled"u8, ref enabled)) if (!Im.Checkbox("Enabled"u8, ref enabled))
return; return;
modManager.SetKnown(selection.Mod!); modManager.SetKnown(selection.Mod!);
if (_temporary || config.DefaultTemporaryMode) if (_temporary || config.DefaultTemporaryMode)
{ {
var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings);
temporarySettings.ForceInherit = false; temporarySettings.ForceInherit = false;
temporarySettings.Enabled = enabled; temporarySettings.Enabled = enabled;
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, temporarySettings); collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, temporarySettings);
} }
else else
{ {
collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled);
} }
} }
/// <summary> /// <summary>
/// Draw a priority input. /// Draw a priority input.
/// Priority is changed on deactivation of the input box. /// Priority is changed on deactivation of the input box.
/// </summary> /// </summary>
private void DrawPriorityInput() private void DrawPriorityInput()
{ {
using var group = ImUtf8.Group(); using var group = Im.Group();
var settings = selection.Settings; var settings = selection.Settings;
var priority = _currentPriority ?? settings.Priority.Value; var priority = _currentPriority ?? settings.Priority.Value;
Im.Item.SetNextWidth(50 * Im.Style.GlobalScale); Im.Item.SetNextWidth(50 * Im.Style.GlobalScale);
using var disabled = ImRaii.Disabled(_locked); using var disabled = Im.Disabled(_locked);
if (ImUtf8.InputScalar("##Priority"u8, ref priority)) if (Im.Input.Scalar("##Priority"u8, ref priority))
_currentPriority = priority; _currentPriority = priority;
if (new ModPriority(priority).IsHidden) if (new ModPriority(priority).IsHidden)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
$"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); $"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax}).");
if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) if (Im.Item.DeactivatedAfterEdit && _currentPriority.HasValue)
{ {
if (_currentPriority != settings.Priority.Value) if (_currentPriority != settings.Priority.Value)
{ {
if (_temporary || config.DefaultTemporaryMode) if (_temporary || config.DefaultTemporaryMode)
{ {
var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings);
temporarySettings.ForceInherit = false; temporarySettings.ForceInherit = false;
temporarySettings.Priority = new ModPriority(_currentPriority.Value); temporarySettings.Priority = new ModPriority(_currentPriority.Value);
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!,
temporarySettings); temporarySettings);
} }
else else
{ {
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!,
new ModPriority(_currentPriority.Value)); new ModPriority(_currentPriority.Value));
} }
} }
_currentPriority = null; _currentPriority = null;
} }
ImUtf8.LabeledHelpMarker("Priority"u8, "Mods with a higher number here take precedence before Mods with a lower number.\n"u8 var hovered = LunaStyle.DrawHelpMarker();
+ "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."u8); Im.Line.SameInner();
} Im.Text("Priority"u8);
if (hovered || Im.Item.Hovered())
/// <summary> Im.Tooltip.Set("Mods with a higher number here take precedence before Mods with a lower number.\n"u8
/// Draw a button to remove the current settings and inherit them instead + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."u8);
/// in the top-right corner of the window/tab. }
/// </summary>
private void DrawRemoveSettings() /// <summary>
{ /// Draw a button to remove the current settings and inherit them instead
var drawInherited = !_inherited && !selection.Settings.IsEmpty; /// in the top-right corner of the window/tab.
var scroll = ImGui.GetScrollMaxY() > 0 ? Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X : 0; /// </summary>
var buttonSize = ImUtf8.CalcTextSize("Turn Permanent_"u8).X; private void DrawRemoveSettings()
var offset = drawInherited {
? buttonSize + ImUtf8.CalcTextSize("Inherit Settings"u8).X + Im.Style.FramePadding.X * 4 + Im.Style.ItemSpacing.X var drawInherited = !_inherited && !selection.Settings.IsEmpty;
: buttonSize + Im.Style.FramePadding.X * 2; var scroll = Im.Scroll.MaximumY > 0 ? Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X : 0;
Im.Line.Same(ImGui.GetWindowWidth() - offset - scroll); var buttonSize = Im.Font.CalculateSize("Turn Permanent_"u8).X;
var enabled = config.DeleteModModifier.IsActive(); var offset = drawInherited
if (drawInherited) ? buttonSize + Im.Font.CalculateSize("Inherit Settings"u8).X + Im.Style.FramePadding.X * 4 + Im.Style.ItemSpacing.X
{ : buttonSize + Im.Style.FramePadding.X * 2;
var inherit = (enabled, _locked) switch Im.Line.Same(Im.Window.Width - offset - scroll);
{ var enabled = config.DeleteModModifier.IsActive();
(true, false) => ImUtf8.ButtonEx("Inherit Settings"u8, if (drawInherited)
"Remove current settings from this collection so that it can inherit them.\n"u8 {
+ "If no inherited collection has settings for this mod, it will be disabled."u8), var inherit = (enabled, _locked) switch
(false, false) => ImUtf8.ButtonEx("Inherit Settings"u8, {
$"Remove current settings from this collection so that it can inherit them.\nHold {config.DeleteModModifier} to inherit.", (true, false) => ImEx.Button("Inherit Settings"u8,
default, true), "Remove current settings from this collection so that it can inherit them.\n"u8
(_, true) => ImUtf8.ButtonEx("Inherit Settings"u8, + "If no inherited collection has settings for this mod, it will be disabled."u8),
"Remove current settings from this collection so that it can inherit them.\nThe settings are currently locked and can not be changed."u8, (false, false) => ImEx.Button("Inherit Settings"u8, default,
default, true), $"Remove current settings from this collection so that it can inherit them.\nHold {config.DeleteModModifier} to inherit.",
}; true),
if (inherit) (_, true) => ImEx.Button("Inherit Settings"u8, default,
{ "Remove current settings from this collection so that it can inherit them.\nThe settings are currently locked and can not be changed."u8,
if (_temporary || config.DefaultTemporaryMode) true),
{ };
var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); if (inherit)
temporarySettings.ForceInherit = true; {
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, if (_temporary || config.DefaultTemporaryMode)
temporarySettings); {
} var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings);
else temporarySettings.ForceInherit = true;
{ collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!,
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); temporarySettings);
} }
} else
{
Im.Line.Same(); collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true);
} }
}
if (_temporary)
{ Im.Line.Same();
var overwrite = enabled }
? ImUtf8.ButtonEx("Turn Permanent"u8,
"Overwrite the actual settings for this mod in this collection with the current temporary settings."u8, if (_temporary)
new Vector2(buttonSize, 0)) {
: ImUtf8.ButtonEx("Turn Permanent"u8, var overwrite = enabled
$"Overwrite the actual settings for this mod in this collection with the current temporary settings.\nHold {config.DeleteModModifier} to overwrite.", ? ImEx.Button("Turn Permanent"u8, new Vector2(buttonSize, 0),
new Vector2(buttonSize, 0), true); "Overwrite the actual settings for this mod in this collection with the current temporary settings."u8)
if (overwrite) : ImEx.Button("Turn Permanent"u8, new Vector2(buttonSize, 0),
{ $"Overwrite the actual settings for this mod in this collection with the current temporary settings.\nHold {config.DeleteModModifier} to overwrite.",
var settings = collectionManager.Active.Current.GetTempSettings(selection.Mod!.Index)!; true);
if (settings.ForceInherit) if (overwrite)
{ {
collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod, true); var settings = collectionManager.Active.Current.GetTempSettings(selection.Mod!.Index)!;
} if (settings.ForceInherit)
else {
{ collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod, true);
collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod, settings.Enabled); }
collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod, settings.Priority); else
foreach (var (index, setting) in settings.Settings.Index()) {
collectionManager.Editor.SetModSetting(collectionManager.Active.Current, selection.Mod, index, setting); collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod, settings.Enabled);
} collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod, settings.Priority);
foreach (var (index, setting) in settings.Settings.Index())
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod, null); collectionManager.Editor.SetModSetting(collectionManager.Active.Current, selection.Mod, index, setting);
} }
}
else collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod, null);
{ }
var actual = collectionManager.Active.Current.GetActualSettings(selection.Mod!.Index).Settings; }
if (ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) else
collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, {
new TemporaryModSettings(selection.Mod!, actual)); var actual = collectionManager.Active.Current.GetActualSettings(selection.Mod!.Index).Settings;
} if (ImEx.Button("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8))
} collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!,
} new TemporaryModSettings(selection.Mod!, actual));
}
}
}

View file

@ -3,6 +3,7 @@ using Luna;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow;
using Penumbra.UI.Classes;
using ImGuiColor = ImSharp.ImGuiColor; using ImGuiColor = ImSharp.ImGuiColor;
namespace Penumbra.UI.ModsTab; namespace Penumbra.UI.ModsTab;
@ -31,7 +32,7 @@ public class ModPanelTabBar : TabBar<ModPanelTab>
TutorialService tutorial, ModPanelCollectionsTab collections, Logger log) TutorialService tutorial, ModPanelCollectionsTab collections, Logger log)
: base(nameof(ModPanelTabBar), log, settings, description, conflicts, changedItems, collections, edit) : base(nameof(ModPanelTabBar), log, settings, description, conflicts, changedItems, collections, edit)
{ {
Flags = TabBarFlags.NoTooltip; Flags = TabBarFlags.NoTooltip | TabBarFlags.FittingPolicyScroll;
Settings = settings; Settings = settings;
Edit = edit; Edit = edit;
_modManager = modManager; _modManager = modManager;

View file

@ -2,6 +2,7 @@ using OtterGui.Filesystem;
using OtterGui.Filesystem.Selector; using OtterGui.Filesystem.Selector;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab; namespace Penumbra.UI.ModsTab;

View file

@ -1,16 +1,12 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
namespace Penumbra.UI.ModsTab; namespace Penumbra.UI.ModsTab;
public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, PredefinedTagManager tagManager) : Luna.IUiService public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, PredefinedTagManager tagManager) : IUiService
{ {
public void Draw() public void Draw()
{ {
@ -18,7 +14,7 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor,
return; return;
Im.Line.New(); Im.Line.New();
var treeNodePos = ImGui.GetCursorPos(); var treeNodePos = Im.Cursor.Position;
var numLeaves = DrawModList(); var numLeaves = DrawModList();
DrawCounts(treeNodePos, numLeaves); DrawCounts(treeNodePos, numLeaves);
DrawMultiTagger(); DrawMultiTagger();
@ -26,24 +22,24 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor,
private void DrawCounts(Vector2 treeNodePos, int numLeaves) private void DrawCounts(Vector2 treeNodePos, int numLeaves)
{ {
var startPos = ImGui.GetCursorPos(); var startPos = Im.Cursor.Position;
var numFolders = selector.SelectedPaths.Count - numLeaves; var numFolders = selector.SelectedPaths.Count - numLeaves;
var text = (numLeaves, numFolders) switch Utf8StringHandler<TextStringHandlerBuffer> text = (numLeaves, numFolders) switch
{ {
(0, 0) => string.Empty, // should not happen (0, 0) => StringU8.Empty, // should not happen
(> 0, 0) => $"{numLeaves} Mods", (> 0, 0) => $"{numLeaves} Mods",
(0, > 0) => $"{numFolders} Folders", (0, > 0) => $"{numFolders} Folders",
_ => $"{numLeaves} Mods, {numFolders} Folders", _ => $"{numLeaves} Mods, {numFolders} Folders",
}; };
ImGui.SetCursorPos(treeNodePos); Im.Cursor.Position = treeNodePos;
ImUtf8.TextRightAligned(text); ImEx.TextRightAligned(ref text);
ImGui.SetCursorPos(startPos); Im.Cursor.Position = startPos;
} }
private int DrawModList() private int DrawModList()
{ {
using var tree = ImUtf8.TreeNode("Currently Selected Objects###Selected"u8, using var tree = Im.Tree.Node("Currently Selected Objects###Selected"u8,
ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); TreeNodeFlags.DefaultOpen | TreeNodeFlags.NoTreePushOnOpen);
Im.Separator(); Im.Separator();
@ -69,16 +65,16 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor,
foreach (var (fullName, path) in selector.SelectedPaths.Select(p => (p.FullName(), p)) foreach (var (fullName, path) in selector.SelectedPaths.Select(p => (p.FullName(), p))
.OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase))
{ {
using var id = ImRaii.PushId(i++); using var id = Im.Id.Push(i++);
var (icon, text) = path is ModFileSystem.Leaf l var (icon, text) = path is ModFileSystem.Leaf l
? (FontAwesomeIcon.FileCircleMinus, l.Value.Name) ? (FontAwesomeIcon.FileCircleMinus.Icon(), l.Value.Name)
: (FontAwesomeIcon.FolderMinus, string.Empty); : (FontAwesomeIcon.FolderMinus.Icon(), string.Empty);
ImGui.TableNextColumn(); table.NextColumn();
if (ImUtf8.IconButton(icon, "Remove from selection."u8, sizeType)) if (ImEx.Icon.Button(icon, "Remove from selection."u8, false, sizeType))
selector.RemovePathFromMultiSelection(path); selector.RemovePathFromMultiSelection(path);
ImUtf8.DrawFrameColumn(text); table.DrawFrameColumn(text);
ImUtf8.DrawFrameColumn(fullName); table.DrawFrameColumn(fullName);
if (path is ModFileSystem.Leaf) if (path is ModFileSystem.Leaf)
++leaves; ++leaves;
} }
@ -95,7 +91,7 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor,
private void DrawMultiTagger() private void DrawMultiTagger()
{ {
var width = ImEx.ScaledVector(150, 0); var width = ImEx.ScaledVector(150, 0);
ImUtf8.TextFrameAligned("Multi Tagger:"u8); ImEx.TextFrameAligned("Multi Tagger:"u8);
Im.Line.Same(); Im.Line.Same();
var predefinedTagsEnabled = tagManager.Enabled; var predefinedTagsEnabled = tagManager.Enabled;
@ -103,32 +99,32 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor,
? Im.ContentRegion.Available.X - 2 * width.X - 3 * Im.Style.ItemInnerSpacing.X - Im.Style.FrameHeight ? Im.ContentRegion.Available.X - 2 * width.X - 3 * Im.Style.ItemInnerSpacing.X - Im.Style.FrameHeight
: Im.ContentRegion.Available.X - 2 * (width.X + Im.Style.ItemInnerSpacing.X); : Im.ContentRegion.Available.X - 2 * (width.X + Im.Style.ItemInnerSpacing.X);
Im.Item.SetNextWidth(inputWidth); Im.Item.SetNextWidth(inputWidth);
ImUtf8.InputText("##tag"u8, ref _tag, "Local Tag Name..."u8); Im.Input.Text("##tag"u8, ref _tag, "Local Tag Name..."u8);
UpdateTagCache(); UpdateTagCache();
var label = _addMods.Count > 0 Utf8StringHandler<LabelStringHandlerBuffer> label = _addMods.Count > 0
? $"Add to {_addMods.Count} Mods" ? $"Add to {_addMods.Count} Mods"
: "Add"; : "Add";
var tooltip = _addMods.Count == 0 Utf8StringHandler<TextStringHandlerBuffer> tooltip = _addMods.Count is 0
? _tag.Length == 0 ? _tag.Length is 0
? "No tag specified." ? "No tag specified."
: $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data." : $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data."
: $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name))}"; : $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name))}";
Im.Line.SameInner(); Im.Line.SameInner();
if (ImUtf8.ButtonEx(label, tooltip, width, _addMods.Count == 0)) if (ImEx.Button(label, width, tooltip, _addMods.Count is 0))
foreach (var mod in _addMods) foreach (var mod in _addMods)
editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag);
label = _removeMods.Count > 0 label = _removeMods.Count > 0
? $"Remove from {_removeMods.Count} Mods" ? $"Remove from {_removeMods.Count} Mods"
: "Remove"; : "Remove";
tooltip = _removeMods.Count == 0 tooltip = _removeMods.Count is 0
? _tag.Length == 0 ? _tag.Length is 0
? "No tag specified." ? "No tag specified."
: $"No selected mod contains the tag \"{_tag}\" locally." : $"No selected mod contains the tag \"{_tag}\" locally."
: $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name))}"; : $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name))}";
Im.Line.SameInner(); Im.Line.SameInner();
if (ImUtf8.ButtonEx(label, tooltip, width, _removeMods.Count == 0)) if (ImEx.Button(label, width, tooltip, _removeMods.Count is 0))
foreach (var (mod, index) in _removeMods) foreach (var (mod, index) in _removeMods)
editor.ChangeLocalTag(mod, index, string.Empty); editor.ChangeLocalTag(mod, index, string.Empty);

View file

@ -0,0 +1,97 @@
using ImSharp;
using Luna;
namespace Penumbra.UI.ModsTab.Selector;
/// <summary> The button to add a new, empty mod. </summary>
/// <param name="drawer"> The file system drawer. </param>
public sealed class AddNewModButton(ModFileSystemDrawer drawer) : BaseIconButton<AwesomeIcon>
{
/// <inheritdoc/>
public override AwesomeIcon Icon
=> LunaStyle.AddObjectIcon;
/// <inheritdoc/>
public override bool HasTooltip
=> true;
/// <inheritdoc/>
public override void DrawTooltip()
=> Im.Text("Create a new, empty mod of a given name."u8);
/// <inheritdoc/>
public override void OnClick()
=> Im.Popup.Open("Create New Mod"u8);
/// <inheritdoc/>
protected override void PostDraw()
{
if (!InputPopup.OpenName("Create New Mod"u8, out var newModName))
return;
if (drawer.ModManager.Creator.CreateEmptyMod(drawer.ModManager.BasePath, newModName) is { } directory)
drawer.ModManager.AddMod(directory, false);
}
}
/// <summary> The button to import a mod. </summary>
/// <param name="drawer"> The file system drawer. </param>
public sealed class ImportModButton(ModFileSystemDrawer drawer) : BaseIconButton<AwesomeIcon>
{
/// <inheritdoc/>
public override AwesomeIcon Icon
=> LunaStyle.AddObjectIcon;
/// <inheritdoc/>
public override bool HasTooltip
=> true;
/// <inheritdoc/>
public override void DrawTooltip()
=> Im.Text("Create a new, empty mod of a given name."u8);
/// <inheritdoc/>
public override void OnClick()
=> Im.Popup.Open("Create New Mod"u8);
/// <inheritdoc/>
protected override void PostDraw()
{
if (!InputPopup.OpenName("Create New Mod"u8, out var newModName))
return;
if (drawer.ModManager.Creator.CreateEmptyMod(drawer.ModManager.BasePath, newModName) is { } directory)
drawer.ModManager.AddMod(directory, false);
}
}
/// <summary> The button to import a mod. </summary>
/// <param name="drawer"> The file system drawer. </param>
public sealed class DeleteSelectionButton(ModFileSystemDrawer drawer) : BaseIconButton<AwesomeIcon>
{
/// <inheritdoc/>
public override AwesomeIcon Icon
=> LunaStyle.AddObjectIcon;
/// <inheritdoc/>
public override bool HasTooltip
=> true;
/// <inheritdoc/>
public override void DrawTooltip()
=> Im.Text("Create a new, empty mod of a given name."u8);
/// <inheritdoc/>
public override void OnClick()
=> Im.Popup.Open("Create New Mod"u8);
/// <inheritdoc/>
protected override void PostDraw()
{
if (!InputPopup.OpenName("Create New Mod"u8, out var newModName))
return;
if (drawer.ModManager.Creator.CreateEmptyMod(drawer.ModManager.BasePath, newModName) is { } directory)
drawer.ModManager.AddMod(directory, false);
}
}

View file

@ -0,0 +1,22 @@
using Luna;
namespace Penumbra.UI.ModsTab.Selector;
/// <summary> The menu item to clear the default import folder. </summary>
/// <param name="drawer"> The file system drawer. </param>
public sealed class ClearDefaultImportFolderButton(ModFileSystemDrawer drawer) : BaseButton
{
/// <inheritdoc/>
public override ReadOnlySpan<byte> Label
=> "Clear Default Import Folder"u8;
/// <inheritdoc/>
public override void OnClick()
{
if (drawer.Config.DefaultImportFolder.Length is 0)
return;
drawer.Config.DefaultImportFolder = string.Empty;
drawer.Config.Save();
}
}

View file

@ -0,0 +1,16 @@
using Luna;
namespace Penumbra.UI.ModsTab.Selector;
/// <summary> The menu item to clear all temporary settings of the current collection. </summary>
/// <param name="drawer"> The file system drawer. </param>
public sealed class ClearTemporarySettingsButton(ModFileSystemDrawer drawer) : BaseButton
{
/// <inheritdoc/>
public override ReadOnlySpan<byte> Label
=> "Clear Temporary Settings"u8;
/// <inheritdoc/>
public override void OnClick()
=> drawer.CollectionManager.Editor.ClearTemporarySettings(drawer.CollectionManager.Active.Current);
}

View file

@ -0,0 +1,27 @@
using ImSharp;
using Luna;
namespace Penumbra.UI.ModsTab.Selector;
/// <summary> The menu items to set all descendants of a folder enabled or disabled. </summary>
/// <param name="drawer"> The file system drawer. </param>
/// <param name="setTo"> Whether the drawer should enable or disable the descendants. </param>
/// <param name="inherit"> Whether the drawer should inherit all descendants instead of enabling or disabling them. </param>
public sealed class SetDescendantsButton(ModFileSystemDrawer drawer, bool setTo, bool inherit = false) : BaseButton<IFileSystemFolder>
{
private readonly StringU8 _label = new((inherit, setTo) switch
{
(true, true) => "Inherit Descendants"u8,
(true, false) => "Stop Inheriting Descendants"u8,
(_, true) => "Enable Descendants"u8,
(_, false) => "Disable Descendants"u8,
});
/// <inheritdoc/>
public override ReadOnlySpan<byte> Label(in IFileSystemFolder folder)
=> _label;
/// <inheritdoc/>
public override void OnClick(in IFileSystemFolder folder)
=> drawer.SetDescendants(folder, setTo, inherit);
}

View file

@ -0,0 +1,15 @@
using Luna;
namespace Penumbra.UI.ModsTab.Selector;
public sealed class ModFileSystemCache(ModFileSystemDrawer parent)
: FileSystemCache<ModFileSystemCache.ModData>(parent), IService
{
public sealed class ModData : BaseFileSystemNodeCache<ModData>;
public override void Update()
{ }
protected override ModData ConvertNode(in IFileSystemNode node)
=> new();
}

View file

@ -0,0 +1,57 @@
using Luna;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
namespace Penumbra.UI.ModsTab.Selector;
public sealed class ModFileSystemDrawer : FileSystemDrawer<ModFileSystemCache.ModData>
{
public readonly ModManager ModManager;
public readonly CollectionManager CollectionManager;
public readonly Configuration Config;
public ModFileSystemDrawer(ModFileSystem2 fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config)
: base(fileSystem, null)
{
ModManager = modManager;
CollectionManager = collectionManager;
Config = config;
MainContext.AddButton(new ClearTemporarySettingsButton(this), 105);
MainContext.AddButton(new ClearDefaultImportFolderButton(this), 10);
FolderContext.AddButton(new SetDescendantsButton(this, true), 11);
FolderContext.AddButton(new SetDescendantsButton(this, false), 10);
FolderContext.AddButton(new SetDescendantsButton(this, true, true), 6);
FolderContext.AddButton(new SetDescendantsButton(this, false, true), 5);
FolderContext.AddButton(new SetDefaultImportFolderButton(this), -100);
DataContext.AddButton(new ToggleFavoriteButton(this), 10);
Footer.Buttons.AddButton(new AddNewModButton(this), 1000);
}
public override ReadOnlySpan<byte> Id
=> "ModFileSystem"u8;
protected override FileSystemCache<ModFileSystemCache.ModData> CreateCache()
=> new ModFileSystemCache(this);
public void SetDescendants(IFileSystemFolder folder, bool enabled, bool inherit = false)
{
var mods = folder.GetDescendants().OfType<IFileSystemData<Mod>>().Select(l =>
{
// Any mod handled here should not stay new.
ModManager.SetKnown(l.Value);
return l.Value;
});
if (inherit)
CollectionManager.Editor.SetMultipleModInheritances(CollectionManager.Active.Current, mods, enabled);
else
CollectionManager.Editor.SetMultipleModStates(CollectionManager.Active.Current, mods, enabled);
}
}

View file

@ -0,0 +1,22 @@
using Luna;
namespace Penumbra.UI.ModsTab.Selector;
/// <summary> The menu item to set a given folder as default import folder. </summary>
/// <param name="drawer"> The file system drawer. </param>
public sealed class SetDefaultImportFolderButton(ModFileSystemDrawer drawer) : BaseButton<IFileSystemFolder>
{
/// <inheritdoc/>
public override ReadOnlySpan<byte> Label(in IFileSystemFolder _)
=> "Set As Default Import Folder"u8;
/// <inheritdoc/>
public override void OnClick(in IFileSystemFolder folder)
{
if (folder.FullPath == drawer.Config.DefaultImportFolder)
return;
drawer.Config.DefaultImportFolder = folder.FullPath;
drawer.Config.Save();
}
}

View file

@ -0,0 +1,63 @@
using System.Collections.Frozen;
using Luna;
using Penumbra.Mods;
namespace Penumbra.UI.ModsTab.Selector;
public readonly struct ImportDate : ISortMode
{
public static readonly ImportDate Instance = new();
public ReadOnlySpan<byte> Name
=> "Import Date (Older First)"u8;
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
=> f.GetSubFolders().Cast<IFileSystemNode>().Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderBy(l => l.Value.ImportDate));
}
public readonly struct InverseImportDate : ISortMode
{
public static readonly InverseImportDate Instance = new();
public ReadOnlySpan<byte> Name
=> "Import Date (Newer First)"u8;
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
public IEnumerable<IFileSystemNode> GetChildren(IFileSystemFolder f)
=> f.GetSubFolders().Cast<IFileSystemNode>()
.Concat(f.GetLeaves().OfType<IFileSystemData<Mod>>().OrderByDescending(l => l.Value.ImportDate));
}
public static class SortModeExtensions
{
private static readonly FrozenDictionary<string, ISortMode> ValidSortModes = new Dictionary<string, ISortMode>
{
[nameof(ISortMode.FoldersFirst)] = ISortMode.FoldersFirst,
[nameof(ISortMode.Lexicographical)] = ISortMode.Lexicographical,
[nameof(ImportDate)] = ISortMode.ImportDate,
[nameof(InverseImportDate)] = ISortMode.InverseImportDate,
[nameof(ISortMode.InverseFoldersFirst)] = ISortMode.InverseFoldersFirst,
[nameof(ISortMode.InverseLexicographical)] = ISortMode.InverseLexicographical,
[nameof(ISortMode.FoldersLast)] = ISortMode.FoldersLast,
[nameof(ISortMode.InverseFoldersLast)] = ISortMode.InverseFoldersLast,
[nameof(ISortMode.InternalOrder)] = ISortMode.InternalOrder,
[nameof(ISortMode.InverseInternalOrder)] = ISortMode.InverseInternalOrder,
}.ToFrozenDictionary();
extension(ISortMode)
{
public static ISortMode ImportDate
=> ImportDate.Instance;
public static ISortMode InverseImportDate
=> InverseImportDate.Instance;
public static IReadOnlyDictionary<string, ISortMode> Valid
=> ValidSortModes;
}
}

View file

@ -0,0 +1,17 @@
using Luna;
using Penumbra.Mods;
namespace Penumbra.UI.ModsTab.Selector;
/// <summary> The menu item to set toggle a mod's favourite state. </summary>
/// <param name="drawer"> The file system drawer. </param>
public sealed class ToggleFavoriteButton(ModFileSystemDrawer drawer) : BaseButton<IFileSystemData>
{
/// <inheritdoc/>
public override ReadOnlySpan<byte> Label(in IFileSystemData data)
=> ((Mod)data.Value).Favorite ? "Remove Favorite"u8 : "Mark as Favorite"u8;
/// <inheritdoc/>
public override void OnClick(in IFileSystemData data)
=> drawer.ModManager.DataEditor.ChangeModFavorite((Mod)data.Value, !((Mod)data.Value).Favorite);
}

View file

@ -1,4 +1,6 @@
using ImSharp;
using Luna; using Luna;
using Luna.Generators;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Enums; using Penumbra.Enums;
using Penumbra.Interop; using Penumbra.Interop;
@ -9,21 +11,31 @@ using Penumbra.String.Classes;
namespace Penumbra.UI.ResourceWatcher; namespace Penumbra.UI.ResourceWatcher;
[Flags] [Flags]
[NamedEnum(Utf16: false)]
public enum RecordType : byte public enum RecordType : byte
{ {
Request = 0x01, [Name("REQ")]
ResourceLoad = 0x02, Request = 0x01,
FileLoad = 0x04,
Destruction = 0x08, [Name("LOAD")]
ResourceLoad = 0x02,
[Name("FILE")]
FileLoad = 0x04,
[Name("DEST")]
Destruction = 0x08,
[Name("DONE")]
ResourceComplete = 0x10, ResourceComplete = 0x10,
} }
internal unsafe struct Record internal unsafe struct Record()
{ {
public DateTime Time; public DateTime Time;
public CiByteString Path; public StringU8 Path;
public CiByteString OriginalPath; public StringU8 OriginalPath;
public string AssociatedGameObject; public string AssociatedGameObject = string.Empty;
public ModCollection? Collection; public ModCollection? Collection;
public ResourceHandle* Handle; public ResourceHandle* Handle;
public ResourceTypeFlag ResourceType; public ResourceTypeFlag ResourceType;
@ -42,8 +54,8 @@ internal unsafe struct Record
=> new() => new()
{ {
Time = DateTime.UtcNow, Time = DateTime.UtcNow,
Path = path.IsOwned ? path : path.Clone(), Path = new StringU8(path.Span, false),
OriginalPath = CiByteString.Empty, OriginalPath = StringU8.Empty,
Collection = null, Collection = null,
Handle = null, Handle = null,
ResourceType = ResourceExtensions.Type(path).ToFlag(), ResourceType = ResourceExtensions.Type(path).ToFlag(),
@ -63,8 +75,8 @@ internal unsafe struct Record
=> new() => new()
{ {
Time = DateTime.UtcNow, Time = DateTime.UtcNow,
Path = fullPath.InternalName.IsOwned ? fullPath.InternalName : fullPath.InternalName.Clone(), Path = new StringU8(fullPath.InternalName.Span, false),
OriginalPath = path.IsOwned ? path : path.Clone(), OriginalPath = new StringU8(path.Span, false),
Collection = resolve.Valid ? resolve.ModCollection : null, Collection = resolve.Valid ? resolve.ModCollection : null,
Handle = null, Handle = null,
ResourceType = ResourceExtensions.Type(path).ToFlag(), ResourceType = ResourceExtensions.Type(path).ToFlag(),
@ -82,12 +94,12 @@ internal unsafe struct Record
public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject)
{ {
path = path.IsOwned ? path : path.Clone(); var p = new StringU8(path.Span, false);
return new Record return new Record
{ {
Time = DateTime.UtcNow, Time = DateTime.UtcNow,
Path = path, Path = p,
OriginalPath = path, OriginalPath = p,
Collection = collection, Collection = collection,
Handle = handle, Handle = handle,
ResourceType = handle->FileType.ToFlag(), ResourceType = handle->FileType.ToFlag(),
@ -109,8 +121,8 @@ internal unsafe struct Record
=> new() => new()
{ {
Time = DateTime.UtcNow, Time = DateTime.UtcNow,
Path = path.InternalName.IsOwned ? path.InternalName : path.InternalName.Clone(), Path = new StringU8(path.InternalName.Span, false),
OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), OriginalPath = new StringU8(originalPath.Span, false),
Collection = collection, Collection = collection,
Handle = handle, Handle = handle,
ResourceType = handle->FileType.ToFlag(), ResourceType = handle->FileType.ToFlag(),
@ -128,12 +140,12 @@ internal unsafe struct Record
public static Record CreateDestruction(ResourceHandle* handle) public static Record CreateDestruction(ResourceHandle* handle)
{ {
var path = handle->FileName().Clone(); var path = new StringU8(handle->FileName().Span, false);
return new Record return new Record
{ {
Time = DateTime.UtcNow, Time = DateTime.UtcNow,
Path = path, Path = path,
OriginalPath = CiByteString.Empty, OriginalPath = StringU8.Empty,
Collection = null, Collection = null,
Handle = handle, Handle = handle,
ResourceType = handle->FileType.ToFlag(), ResourceType = handle->FileType.ToFlag(),
@ -154,8 +166,8 @@ internal unsafe struct Record
=> new() => new()
{ {
Time = DateTime.UtcNow, Time = DateTime.UtcNow,
Path = path.IsOwned ? path : path.Clone(), Path = new StringU8(path.Span, false),
OriginalPath = CiByteString.Empty, OriginalPath = StringU8.Empty,
Collection = null, Collection = null,
Handle = handle, Handle = handle,
ResourceType = handle->FileType.ToFlag(), ResourceType = handle->FileType.ToFlag(),
@ -171,12 +183,13 @@ internal unsafe struct Record
OsThreadId = ProcessThreadApi.GetCurrentThreadId(), OsThreadId = ProcessThreadApi.GetCurrentThreadId(),
}; };
public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, ReadOnlySpan<byte> additionalData) public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath,
ReadOnlySpan<byte> additionalData)
=> new() => new()
{ {
Time = DateTime.UtcNow, Time = DateTime.UtcNow,
Path = CombinedPath(path, additionalData), Path = CombinedPath(path, additionalData),
OriginalPath = originalPath.Path.IsOwned ? originalPath.Path : originalPath.Path.Clone(), OriginalPath = new StringU8(originalPath.Path.Span, false),
Collection = null, Collection = null,
Handle = handle, Handle = handle,
ResourceType = handle->FileType.ToFlag(), ResourceType = handle->FileType.ToFlag(),
@ -192,16 +205,16 @@ internal unsafe struct Record
OsThreadId = ProcessThreadApi.GetCurrentThreadId(), OsThreadId = ProcessThreadApi.GetCurrentThreadId(),
}; };
private static CiByteString CombinedPath(CiByteString path, ReadOnlySpan<byte> additionalData) private static StringU8 CombinedPath(CiByteString path, ReadOnlySpan<byte> additionalData)
{ {
if (additionalData.Length is 0) if (additionalData.Length is 0)
return path.IsOwned ? path : path.Clone(); return new StringU8(path.Span, false);
fixed (byte* ptr = additionalData) fixed (byte* ptr = additionalData)
{ {
// If a path has additional data and is split, it is always in the form of |{additionalData}|{path}, // If a path has additional data and is split, it is always in the form of |{additionalData}|{path},
// so we can just read from the start of additional data - 1 and sum their length +2 for the pipes. // so we can just read from the start of additional data - 1 and sum their length +2 for the pipes.
return new CiByteString(new ReadOnlySpan<byte>(ptr - 1, additionalData.Length + 2 + path.Length)).Clone(); return new StringU8(new ReadOnlySpan<byte>(ptr - 1, additionalData.Length + 2 + path.Length));
} }
} }
} }

View file

@ -1,7 +1,7 @@
using Dalamud.Bindings.ImGui;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using ImSharp; using ImSharp;
using ImSharp.Containers;
using Luna; using Luna;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
@ -27,7 +27,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
private readonly ResourceLoader _loader; private readonly ResourceLoader _loader;
private readonly ResourceHandleDestructor _destructor; private readonly ResourceHandleDestructor _destructor;
private readonly ActorManager _actors; private readonly ActorManager _actors;
private readonly List<Record> _records = []; private readonly ObservableList<Record> _records = [];
private readonly ConcurrentQueue<Record> _newRecords = []; private readonly ConcurrentQueue<Record> _newRecords = [];
private readonly ResourceWatcherTable _table; private readonly ResourceWatcherTable _table;
private string _logFilter = string.Empty; private string _logFilter = string.Empty;
@ -43,7 +43,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
_resources = resources; _resources = resources;
_destructor = destructor; _destructor = destructor;
_loader = loader; _loader = loader;
_table = new ResourceWatcherTable(config.Ephemeral, _records); _table = new ResourceWatcherTable(new ResourceWatcherConfig(), _records);
_resources.ResourceRequested += OnResourceRequested; _resources.ResourceRequested += OnResourceRequested;
_destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher);
_loader.ResourceLoaded += OnResourceLoaded; _loader.ResourceLoaded += OnResourceLoaded;
@ -71,7 +71,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
? Record.CreateRequest(original.Path, false, _1.Value, _2) ? Record.CreateRequest(original.Path, false, _1.Value, _2)
: Record.CreateRequest(original.Path, false); : Record.CreateRequest(original.Path, false);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record); Enqueue(record);
} }
public unsafe void Dispose() public unsafe void Dispose()
@ -90,7 +90,6 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
{ {
_records.Clear(); _records.Clear();
_newRecords.Clear(); _newRecords.Clear();
_table.Reset();
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -103,7 +102,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
{ {
UpdateRecords(); UpdateRecords();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + Im.Style.TextHeightWithSpacing / 2); Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2;
var isEnabled = _ephemeral.EnableResourceWatcher; var isEnabled = _ephemeral.EnableResourceWatcher;
if (Im.Checkbox("Enable"u8, ref isEnabled)) if (Im.Checkbox("Enable"u8, ref isEnabled))
{ {
@ -138,7 +137,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2; Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2;
_table.Draw(Im.Style.TextHeightWithSpacing); _table.Draw();
} }
private void DrawFilterInput() private void DrawFilterInput()
@ -184,8 +183,8 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
{ {
Im.Item.SetNextWidthScaled(80); Im.Item.SetNextWidthScaled(80);
Im.Input.Scalar("Max. Entries"u8, ref _newMaxEntries); Im.Input.Scalar("Max. Entries"u8, ref _newMaxEntries);
var change = ImGui.IsItemDeactivatedAfterEdit(); var change = Im.Item.DeactivatedAfterEdit;
if (Im.Item.RightClicked() && ImGui.GetIO().KeyCtrl) if (Im.Item.RightClicked() && Im.Io.KeyControl)
{ {
change = true; change = true;
_newMaxEntries = DefaultMaxEntries; _newMaxEntries = DefaultMaxEntries;
@ -193,7 +192,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
var maxEntries = _config.MaxResourceWatcherRecords; var maxEntries = _config.MaxResourceWatcherRecords;
if (maxEntries != DefaultMaxEntries && Im.Item.Hovered()) if (maxEntries != DefaultMaxEntries && Im.Item.Hovered())
ImGui.SetTooltip($"CTRL + Right-Click to reset to default {DefaultMaxEntries}."); Im.Tooltip.Set($"CTRL + Right-Click to reset to default {DefaultMaxEntries}.");
if (!change) if (!change)
return; return;
@ -219,8 +218,6 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
if (_records.Count > _config.MaxResourceWatcherRecords) if (_records.Count > _config.MaxResourceWatcherRecords)
_records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords);
_table.Reset();
} }
@ -235,7 +232,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
var record = Record.CreateRequest(original.Path, sync); var record = Record.CreateRequest(original.Path, sync);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record); Enqueue(record);
} }
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data)
@ -262,7 +259,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data))
: Record.CreateLoad(manipulatedPath.Value, path.Path, handle, data.ModCollection, Name(data)); : Record.CreateLoad(manipulatedPath.Value, path.Path, handle, data.ModCollection, Name(data));
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record); Enqueue(record);
} }
private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original,
@ -280,7 +277,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
var record = Record.CreateResourceComplete(path, resource, original, additionalData); var record = Record.CreateResourceComplete(path, resource, original, additionalData);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record); Enqueue(record);
} }
private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan<byte> _) private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan<byte> _)
@ -294,7 +291,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
var record = Record.CreateFileLoad(path, resource, success, custom); var record = Record.CreateFileLoad(path, resource, success, custom);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record); Enqueue(record);
} }
private unsafe void OnResourceDestroyed(in ResourceHandleDestructor.Arguments arguments) private unsafe void OnResourceDestroyed(in ResourceHandleDestructor.Arguments arguments)
@ -308,7 +305,7 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
var record = Record.CreateDestruction(arguments.ResourceHandle); var record = Record.CreateDestruction(arguments.ResourceHandle);
if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record))
_newRecords.Enqueue(record); Enqueue(record);
} }
public unsafe string Name(ResolveData resolve, string none = "") public unsafe string Name(ResolveData resolve, string none = "")
@ -336,4 +333,15 @@ public sealed class ResourceWatcher : IDisposable, ITab<TabType>
return $"0x{resolve.AssociatedGameObject:X}"; return $"0x{resolve.AssociatedGameObject:X}";
} }
private void Enqueue(Record record)
{
lock (_newRecords)
{
// Discard entries that exceed the number of records.
while (_newRecords.Count >= _config.MaxResourceWatcherRecords)
_newRecords.TryDequeue(out _);
_newRecords.Enqueue(record);
}
}
} }

View file

@ -1,57 +1,110 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using ImSharp; using ImSharp;
using ImSharp.Containers;
using ImSharp.Table;
using Luna; using Luna;
using OtterGui.Table;
using Penumbra.Enums; using Penumbra.Enums;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
namespace Penumbra.UI.ResourceWatcher; namespace Penumbra.UI.ResourceWatcher;
internal sealed class ResourceWatcherTable : Table<Record> public class ResourceWatcherConfig
{ {
public ResourceWatcherTable(EphemeralConfig config, IReadOnlyCollection<Record> records) public int Version = 1;
: base("##records", public bool Enabled = false;
records, public int MaxEntries = 500;
new PathColumn { Label = "Path" }, public bool StoreOnlyMatching = true;
new RecordTypeColumn(config) { Label = "Record" }, public bool WriteToLog = false;
new CollectionColumn { Label = "Collection" }, public string LogFilter = string.Empty;
new ObjectColumn { Label = "Game Object" }, public string PathFilter = string.Empty;
new CustomLoadColumn { Label = "Custom" }, public string CollectionFilter = string.Empty;
new SynchronousLoadColumn { Label = "Sync" }, public string ObjectFilter = string.Empty;
new OriginalPathColumn { Label = "Original Path" }, public string OriginalPathFilter = string.Empty;
new ResourceCategoryColumn(config) { Label = "Category" }, public string ResourceFilter = string.Empty;
new ResourceTypeColumn(config) { Label = "Type" }, public string CrcFilter = string.Empty;
new HandleColumn { Label = "Resource" }, public string RefFilter = string.Empty;
new LoadStateColumn { Label = "State" }, public string ThreadFilter = string.Empty;
new RefCountColumn { Label = "#Ref" }, public RecordType RecordFilter = Enum.GetValues<RecordType>().Or();
new DateColumn { Label = "Time" }, public BoolEnum CustomFilter = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown;
new Crc64Column { Label = "Crc64" }, public BoolEnum SyncFilter = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown;
new OsThreadColumn { Label = "TID" } public ResourceCategoryFlag CategoryFilter = ResourceExtensions.AllResourceCategories;
) public ResourceTypeFlag TypeFilter = ResourceExtensions.AllResourceTypes;
public LoadStateFlag LoadStateFilter = Enum.GetValues<LoadStateFlag>().Or();
public void Save()
{ } { }
}
public void Reset() [Flags]
=> FilterDirty = true; public enum BoolEnum : byte
{
True = 0x01,
False = 0x02,
Unknown = 0x04,
}
private sealed class PathColumn : ColumnString<Record> [Flags]
public enum LoadStateFlag : byte
{
Success = 0x01,
Async = 0x02,
Failed = 0x04,
FailedSub = 0x08,
Unknown = 0x10,
None = 0xFF,
}
internal sealed unsafe class CachedRecord(Record record)
{
public readonly Record Record = record;
public readonly string PathU16 = record.Path.ToString();
public readonly StringU8 TypeName = new(record.RecordType.ToName());
public readonly StringU8 Time = new($"{record.Time.ToLongTimeString()}.{record.Time.Millisecond:D4}");
public readonly StringPair Crc64 = new($"{record.Crc64:X16}");
public readonly StringU8 Collection = record.Collection is null ? StringU8.Empty : new StringU8(record.Collection.Identity.Name);
public readonly StringU8 AssociatedGameObject = new(record.AssociatedGameObject);
public readonly string OriginalPath = record.OriginalPath.ToString();
public readonly StringU8 ResourceCategory = new($"{record.Category}");
public readonly StringU8 ResourceType = new(record.ResourceType.ToString().ToLowerInvariant());
public readonly string HandleU16 = $"0x{(nint)record.Handle:X}";
public readonly SizedStringPair Thread = new($"{record.OsThreadId}");
public readonly SizedStringPair RefCount = new($"{record.RefCount}");
}
internal sealed class ResourceWatcherTable : TableBase<CachedRecord, TableCache<CachedRecord>>
{
private readonly IReadOnlyList<Record> _records;
public bool WouldBeVisible(Record record)
{ {
public override float Width var cached = new CachedRecord(record);
=> 300 * Im.Style.GlobalScale; return Columns.All(c => c.WouldBeVisible(cached, -1));
public override string ToName(Record item)
=> item.Path.ToString();
public override int Compare(Record lhs, Record rhs)
=> lhs.Path.CompareTo(rhs.Path);
public override void DrawColumn(Record item, int _)
=> DrawByteString(item.Path, 280 * Im.Style.GlobalScale);
} }
private static void DrawByteString(CiByteString path, float length) public ResourceWatcherTable(ResourceWatcherConfig config, IReadOnlyList<Record> records)
: base(new StringU8("##records"u8),
new PathColumn(config) { Label = new StringU8("Path"u8) },
new RecordTypeColumn(config) { Label = new StringU8("Record"u8) },
new CollectionColumn(config) { Label = new StringU8("Collection"u8) },
new ObjectColumn(config) { Label = new StringU8("Game Object"u8) },
new CustomLoadColumn(config) { Label = new StringU8("Custom"u8) },
new SynchronousLoadColumn(config) { Label = new StringU8("Sync"u8) },
new OriginalPathColumn(config) { Label = new StringU8("Original Path"u8) },
new ResourceCategoryColumn(config) { Label = new StringU8("Category"u8) },
new ResourceTypeColumn(config) { Label = new StringU8("Type"u8) },
new HandleColumn(config) { Label = new StringU8("Resource"u8) },
new LoadStateColumn(config) { Label = new StringU8("State"u8) },
new RefCountColumn(config) { Label = new StringU8("#Ref"u8) },
new DateColumn { Label = new StringU8("Time"u8) },
new Crc64Column(config) { Label = new StringU8("Crc64"u8) },
new OsThreadColumn(config) { Label = new StringU8("TID"u8) }
)
{
_records = records;
}
private static void DrawByteString(StringU8 path, float length)
{ {
if (path.IsEmpty) if (path.IsEmpty)
return; return;
@ -64,24 +117,24 @@ internal sealed class ResourceWatcherTable : Table<Record>
} }
else else
{ {
var fileName = path.LastIndexOf((byte)'/'); var fileName = path.Span.LastIndexOf((byte)'/');
using (Im.Group()) using (Im.Group())
{ {
CiByteString shortPath; ReadOnlySpan<byte> shortPath;
var icon = FontAwesomeIcon.EllipsisH.Icon(); var icon = FontAwesomeIcon.EllipsisH.Icon();
if (fileName is not -1) if (fileName is not -1)
{ {
using var font = AwesomeIcon.Font.Push(); using var font = AwesomeIcon.Font.Push();
clicked = Im.Selectable(icon.Span); clicked = Im.Selectable(icon.Span);
Im.Line.SameInner(); Im.Line.SameInner();
shortPath = path.Substring(fileName, path.Length - fileName); shortPath = path.Span.Slice(fileName, path.Length - fileName);
} }
else else
{ {
shortPath = path; shortPath = path;
} }
clicked |= Im.Selectable(shortPath.Span, false, SelectableFlags.AllowOverlap); clicked |= Im.Selectable(shortPath, false, SelectableFlags.AllowOverlap);
} }
Im.Tooltip.OnHover(path.Span); Im.Tooltip.OnHover(path.Span);
@ -91,378 +144,496 @@ internal sealed class ResourceWatcherTable : Table<Record>
Im.Clipboard.Set(path.Span); Im.Clipboard.Set(path.Span);
} }
private sealed class RecordTypeColumn : ColumnFlags<RecordType, Record>
{
private readonly EphemeralConfig _config;
public RecordTypeColumn(EphemeralConfig config) private sealed class PathColumn : TextColumn<CachedRecord>
{
private readonly ResourceWatcherConfig _config;
public PathColumn(ResourceWatcherConfig config)
{ {
AllFlags = ResourceWatcher.AllRecords; _config = config;
_config = config; UnscaledWidth = 300;
Filter.Set(config.PathFilter);
Filter.FilterChanged += OnFilterChanged;
} }
public override float Width private void OnFilterChanged()
=> 80 * Im.Style.GlobalScale;
public override bool FilterFunc(Record item)
=> FilterValue.HasFlag(item.RecordType);
public override RecordType FilterValue
=> _config.ResourceWatcherRecordTypes;
protected override void SetValue(RecordType value, bool enable)
{ {
if (enable) _config.PathFilter = Filter.Text;
_config.ResourceWatcherRecordTypes |= value;
else
_config.ResourceWatcherRecordTypes &= ~value;
_config.Save(); _config.Save();
} }
public override void DrawColumn(Record item, int idx) public override void DrawColumn(in CachedRecord item, int globalIndex)
{ {
Im.Text(item.RecordType switch DrawByteString(item.Record.Path, 290 * Im.Style.GlobalScale);
{
RecordType.Request => "REQ"u8,
RecordType.ResourceLoad => "LOAD"u8,
RecordType.FileLoad => "FILE"u8,
RecordType.Destruction => "DEST"u8,
RecordType.ResourceComplete => "DONE"u8,
_ => StringU8.Empty,
});
} }
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.PathU16;
protected override StringU8 DisplayText(in CachedRecord item, int globalIndex)
=> item.Record.Path;
} }
private sealed class DateColumn : Column<Record> private sealed class RecordTypeColumn : FlagColumn<RecordType, CachedRecord>
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 80 * Im.Style.GlobalScale;
public override int Compare(Record lhs, Record rhs) public RecordTypeColumn(ResourceWatcherConfig config)
=> lhs.Time.CompareTo(rhs.Time);
public override void DrawColumn(Record item, int _)
=> Im.Text($"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}");
}
private sealed class Crc64Column : ColumnString<Record>
{
public override float Width
=> UiBuilder.MonoFont.GetCharAdvance('0') * 17;
public override string ToName(Record item)
=> item.Crc64 is not 0 ? $"{item.Crc64:X16}" : string.Empty;
public override unsafe void DrawColumn(Record item, int _)
{ {
using var font = item.Handle is null ? null : Im.Font.PushMono(); _config = config;
Im.Text(ToName(item)); UnscaledWidth = 80;
Filter.LoadValue(config.RecordFilter);
Filter.FilterChanged += OnFilterChanged;
} }
private void OnFilterChanged()
{
_config.RecordFilter = Filter.FilterValue;
_config.Save();
}
protected override StringU8 DisplayString(in CachedRecord item, int globalIndex)
=> item.TypeName;
protected override IReadOnlyList<(RecordType Value, StringU8 Name)> EnumData
=> Enum.GetValues<RecordType>().Select(t => (t, new StringU8(t.ToName()))).ToArray();
protected override RecordType GetValue(in CachedRecord item, int globalIndex)
=> item.Record.RecordType;
}
private sealed class DateColumn : BasicColumn<CachedRecord>
{
public DateColumn()
=> UnscaledWidth = 80;
public override int Compare(in CachedRecord lhs, int lhsGlobalIndex, in CachedRecord rhs, int rhsGlobalIndex)
=> lhs.Record.Time.CompareTo(rhs.Record.Time);
public override void DrawColumn(in CachedRecord item, int globalIndex)
=> Im.Text(item.Time);
}
private sealed class Crc64Column : TextColumn<CachedRecord>
{
private readonly ResourceWatcherConfig _config;
public Crc64Column(ResourceWatcherConfig config)
{
_config = config;
UnscaledWidth = 17 * 8;
Filter.Set(config.CrcFilter);
Filter.FilterChanged += OnFilterChanged;
}
private void OnFilterChanged()
{
_config.CrcFilter = Filter.Text;
_config.Save();
}
public override int Compare(in CachedRecord lhs, int lhsGlobalIndex, in CachedRecord rhs, int rhsGlobalIndex)
=> lhs.Record.Crc64.CompareTo(rhs.Record.Crc64);
public override void DrawColumn(in CachedRecord item, int globalIndex)
{
if (item.Record.Crc64 is 0)
return;
using var font = Im.Font.PushMono();
base.DrawColumn(in item, globalIndex);
}
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.Crc64;
protected override StringU8 DisplayText(in CachedRecord item, int globalIndex)
=> item.Crc64;
} }
private sealed class CollectionColumn : ColumnString<Record> private sealed class CollectionColumn : TextColumn<CachedRecord>
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 80 * Im.Style.GlobalScale;
public override string ToName(Record item) public CollectionColumn(ResourceWatcherConfig config)
=> (item.Collection != null ? item.Collection.Identity.Name : null) ?? string.Empty; {
_config = config;
UnscaledWidth = 80;
Filter.Set(config.CollectionFilter);
Filter.FilterChanged += OnFilterChanged;
}
private void OnFilterChanged()
{
_config.CollectionFilter = Filter.Text;
_config.Save();
}
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.Record.Collection?.Identity.Name ?? string.Empty;
protected override StringU8 DisplayText(in CachedRecord item, int globalIndex)
=> item.Collection;
} }
private sealed class ObjectColumn : ColumnString<Record> private sealed class ObjectColumn : TextColumn<CachedRecord>
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 200 * Im.Style.GlobalScale;
public override string ToName(Record item) public ObjectColumn(ResourceWatcherConfig config)
{
_config = config;
UnscaledWidth = 150;
Filter.Set(config.ObjectFilter);
Filter.FilterChanged += OnFilterChanged;
}
private void OnFilterChanged()
{
_config.ObjectFilter = Filter.Text;
_config.Save();
}
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.Record.AssociatedGameObject;
protected override StringU8 DisplayText(in CachedRecord item, int globalIndex)
=> item.AssociatedGameObject; => item.AssociatedGameObject;
} }
private sealed class OriginalPathColumn : ColumnString<Record> private sealed class OriginalPathColumn : TextColumn<CachedRecord>
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 200 * Im.Style.GlobalScale;
public override string ToName(Record item) public OriginalPathColumn(ResourceWatcherConfig config)
=> item.OriginalPath.ToString();
public override int Compare(Record lhs, Record rhs)
=> lhs.OriginalPath.CompareTo(rhs.OriginalPath);
public override void DrawColumn(Record item, int _)
=> DrawByteString(item.OriginalPath, 190 * Im.Style.GlobalScale);
}
private sealed class ResourceCategoryColumn : ColumnFlags<ResourceCategoryFlag, Record>
{
private readonly EphemeralConfig _config;
public ResourceCategoryColumn(EphemeralConfig config)
{ {
_config = config; _config = config;
AllFlags = ResourceExtensions.AllResourceCategories; UnscaledWidth = 200;
Filter.Set(config.OriginalPathFilter);
Filter.FilterChanged += OnFilterChanged;
} }
public override float Width private void OnFilterChanged()
=> 80 * Im.Style.GlobalScale;
public override bool FilterFunc(Record item)
=> FilterValue.HasFlag(item.Category);
public override ResourceCategoryFlag FilterValue
=> _config.ResourceWatcherResourceCategories;
protected override void SetValue(ResourceCategoryFlag value, bool enable)
{ {
if (enable) _config.OriginalPathFilter = Filter.Text;
_config.ResourceWatcherResourceCategories |= value;
else
_config.ResourceWatcherResourceCategories &= ~value;
_config.Save(); _config.Save();
} }
public override void DrawColumn(Record item, int idx) public override void DrawColumn(in CachedRecord item, int globalIndex)
{ {
Im.Text($"{item.Category}"); DrawByteString(item.Record.OriginalPath, 190 * Im.Style.GlobalScale);
} }
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.OriginalPath;
protected override StringU8 DisplayText(in CachedRecord item, int globalIndex)
=> item.Record.OriginalPath;
} }
private sealed class ResourceTypeColumn : ColumnFlags<ResourceTypeFlag, Record> private sealed class ResourceCategoryColumn : FlagColumn<ResourceCategoryFlag, CachedRecord>
{ {
private readonly EphemeralConfig _config; private readonly ResourceWatcherConfig _config;
public ResourceTypeColumn(EphemeralConfig config) public ResourceCategoryColumn(ResourceWatcherConfig config)
{ {
_config = config; _config = config;
AllFlags = Enum.GetValues<ResourceTypeFlag>().Aggregate((v, f) => v | f); UnscaledWidth = 80;
for (var i = 0; i < Names.Length; ++i) Filter.LoadValue(config.CategoryFilter);
Names[i] = Names[i].ToLowerInvariant(); Filter.FilterChanged += OnFilterChanged;
} }
public override float Width private void OnFilterChanged()
=> 50 * Im.Style.GlobalScale;
public override bool FilterFunc(Record item)
=> FilterValue.HasFlag(item.ResourceType);
public override ResourceTypeFlag FilterValue
=> _config.ResourceWatcherResourceTypes;
protected override void SetValue(ResourceTypeFlag value, bool enable)
{ {
if (enable) _config.CategoryFilter = Filter.FilterValue;
_config.ResourceWatcherResourceTypes |= value;
else
_config.ResourceWatcherResourceTypes &= ~value;
_config.Save(); _config.Save();
} }
public override void DrawColumn(Record item, int idx) protected override StringU8 DisplayString(in CachedRecord item, int globalIndex)
{ => item.ResourceCategory;
Im.Text($"{item.ResourceType.ToString().ToLowerInvariant()}");
} protected override IReadOnlyList<(ResourceCategoryFlag Value, StringU8 Name)> EnumData { get; } =
Enum.GetValues<ResourceCategoryFlag>().Select(r => (r, new StringU8($"{r}"))).ToArray();
protected override ResourceCategoryFlag GetValue(in CachedRecord item, int globalIndex)
=> item.Record.Category;
} }
private sealed class LoadStateColumn : ColumnFlags<LoadStateColumn.LoadStateFlag, Record> private sealed class ResourceTypeColumn : FlagColumn<ResourceTypeFlag, CachedRecord>
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 50 * Im.Style.GlobalScale;
[Flags] public ResourceTypeColumn(ResourceWatcherConfig config)
public enum LoadStateFlag : byte
{ {
Success = 0x01, _config = config;
Async = 0x02, UnscaledWidth = 50;
Failed = 0x04, Filter.LoadValue(config.TypeFilter);
FailedSub = 0x08, Filter.FilterChanged += OnFilterChanged;
Unknown = 0x10,
None = 0xFF,
} }
protected override string[] Names private void OnFilterChanged()
=> new[]
{
"Loaded",
"Loading",
"Failed",
"Dependency Failed",
"Unknown",
"None",
};
public LoadStateColumn()
{ {
AllFlags = Enum.GetValues<LoadStateFlag>().Aggregate((v, f) => v | f); _config.TypeFilter = Filter.FilterValue;
_filterValue = AllFlags; _config.Save();
} }
private LoadStateFlag _filterValue; protected override IReadOnlyList<(ResourceTypeFlag Value, StringU8 Name)> EnumData { get; } =
Enum.GetValues<ResourceTypeFlag>().Select(r => (r, new StringU8(r.ToString().ToLowerInvariant()))).ToArray();
public override LoadStateFlag FilterValue protected override StringU8 DisplayString(in CachedRecord item, int globalIndex)
=> _filterValue; => item.ResourceType;
protected override void SetValue(LoadStateFlag value, bool enable) protected override ResourceTypeFlag GetValue(in CachedRecord item, int globalIndex)
=> item.Record.ResourceType;
}
private sealed class LoadStateColumn : FlagColumn<LoadStateFlag, CachedRecord>
{
private readonly ResourceWatcherConfig _config;
public LoadStateColumn(ResourceWatcherConfig config)
{ {
if (enable) _config = config;
_filterValue |= value; UnscaledWidth = 50;
else Filter.LoadValue(config.LoadStateFilter);
_filterValue &= ~value; Filter.FilterChanged += OnFilterChanged;
} }
public override bool FilterFunc(Record item) private void OnFilterChanged()
=> item.LoadState switch
{
LoadState.None => FilterValue.HasFlag(LoadStateFlag.None),
LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Success),
LoadState.FailedSubResource => FilterValue.HasFlag(LoadStateFlag.FailedSub),
<= LoadState.Constructed => FilterValue.HasFlag(LoadStateFlag.Unknown),
< LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Async),
> LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Failed),
};
public override void DrawColumn(Record item, int _)
{ {
if (item.LoadState == LoadState.None) _config.LoadStateFilter = Filter.FilterValue;
_config.Save();
}
public override void DrawColumn(in CachedRecord item, int globalIndex)
{
if (item.Record.LoadState == LoadState.None)
return; return;
var (icon, color, tt) = item.LoadState switch var (icon, color, tt) = item.Record.LoadState switch
{ {
LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(), LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(),
new StringU8($"Successfully loaded ({(byte)item.LoadState}).")), new StringU8($"Successfully loaded ({(byte)item.Record.LoadState}).")),
LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(), LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(),
new StringU8($"Dependencies failed to load ({(byte)item.LoadState}).")), new StringU8($"Dependencies failed to load ({(byte)item.Record.LoadState}).")),
<= LoadState.Constructed => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), <= LoadState.Constructed => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(),
new StringU8($"Not yet loaded ({(byte)item.LoadState}).")), new StringU8($"Not yet loaded ({(byte)item.Record.LoadState}).")),
< LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), new StringU8($"Loading asynchronously ({(byte)item.LoadState}).")), < LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(),
new StringU8($"Loading asynchronously ({(byte)item.Record.LoadState}).")),
> LoadState.Success => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), > LoadState.Success => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(),
new StringU8($"Failed to load ({(byte)item.LoadState}).")), new StringU8($"Failed to load ({(byte)item.Record.LoadState}).")),
}; };
ImEx.Icon.Draw(icon.Icon(), color); ImEx.Icon.Draw(icon.Icon(), color);
Im.Tooltip.OnHover(tt); Im.Tooltip.OnHover(tt);
} }
public override int Compare(Record lhs, Record rhs) public override int Compare(in CachedRecord lhs, int lhsGlobalIndex, in CachedRecord rhs, int rhsGlobalIndex)
=> lhs.LoadState.CompareTo(rhs.LoadState); => lhs.Record.LoadState.CompareTo(rhs.Record.LoadState);
}
private sealed class HandleColumn : ColumnString<Record> protected override StringU8 DisplayString(in CachedRecord item, int globalIndex)
{ => StringU8.Empty;
public override float Width
=> 120 * Im.Style.GlobalScale;
public override unsafe string ToName(Record item) protected override IReadOnlyList<(LoadStateFlag Value, StringU8 Name)> EnumData { get; }
=> item.Handle == null ? string.Empty : $"0x{(ulong)item.Handle:X}"; =
[
(LoadStateFlag.Success, new StringU8("Loaded"u8)),
(LoadStateFlag.Async, new StringU8("Loading"u8)),
(LoadStateFlag.Failed, new StringU8("Failed"u8)),
(LoadStateFlag.FailedSub, new StringU8("Dependency Failed"u8)),
(LoadStateFlag.Unknown, new StringU8("Unknown"u8)),
(LoadStateFlag.None, new StringU8("None"u8)),
];
public override unsafe void DrawColumn(Record item, int _) protected override LoadStateFlag GetValue(in CachedRecord item, int globalIndex)
{ => item.Record.LoadState switch
using var font = item.Handle is null ? null : Im.Font.PushMono();
ImEx.TextRightAligned(ToName(item));
}
}
[Flags]
private enum BoolEnum : byte
{
True = 0x01,
False = 0x02,
Unknown = 0x04,
}
private class OptBoolColumn : ColumnFlags<BoolEnum, Record>
{
private BoolEnum _filter;
public OptBoolColumn()
{
AllFlags = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown;
_filter = AllFlags;
Flags &= ~ImGuiTableColumnFlags.NoSort;
}
protected bool FilterFunc(OptionalBool b)
=> b.Value switch
{ {
null => _filter.HasFlag(BoolEnum.Unknown), LoadState.None => LoadStateFlag.None,
true => _filter.HasFlag(BoolEnum.True), LoadState.Success => LoadStateFlag.Success,
false => _filter.HasFlag(BoolEnum.False), LoadState.FailedSubResource => LoadStateFlag.FailedSub,
<= LoadState.Constructed => LoadStateFlag.Unknown,
< LoadState.Success => LoadStateFlag.Async,
> LoadState.Success => LoadStateFlag.Failed,
}; };
}
public override BoolEnum FilterValue private sealed class HandleColumn : TextColumn<CachedRecord>
=> _filter; {
private readonly ResourceWatcherConfig _config;
protected override void SetValue(BoolEnum value, bool enable) public HandleColumn(ResourceWatcherConfig config)
{ {
if (enable) _config = config;
_filter |= value; UnscaledWidth = 120;
else Filter.Set(config.ResourceFilter);
_filter &= ~value; Filter.FilterChanged += OnFilterChanged;
} }
protected static void DrawColumn(OptionalBool b) private void OnFilterChanged()
{ {
if (!b.HasValue) _config.ResourceFilter = Filter.Text;
_config.Save();
}
public override unsafe void DrawColumn(in CachedRecord item, int globalIndex)
{
if (item.Record.RecordType is RecordType.Request)
return; return;
ImEx.Icon.Draw(b.Value switch Penumbra.Dynamis.DrawPointer(item.Record.Handle);
{
true => FontAwesomeIcon.Check.Icon(),
_ => FontAwesomeIcon.Times.Icon(),
});
} }
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.HandleU16;
protected override StringU8 DisplayText(in CachedRecord item, int globalIndex)
=> StringU8.Empty;
}
private abstract class OptBoolColumn : FlagColumn<BoolEnum, CachedRecord>
{
protected OptBoolColumn(float width)
{
UnscaledWidth = width;
Flags &= ~TableColumnFlags.NoSort;
}
public override void DrawColumn(in CachedRecord item, int globalIndex)
{
var value = GetValue(item, globalIndex);
if (value is BoolEnum.Unknown)
return;
ImEx.Icon.Draw(value is BoolEnum.True
? FontAwesomeIcon.Check.Icon()
: FontAwesomeIcon.Times.Icon()
);
}
protected override IReadOnlyList<(BoolEnum Value, StringU8 Name)> EnumData { get; } =
[
(BoolEnum.True, new StringU8("True"u8)),
(BoolEnum.False, new StringU8("False"u8)),
(BoolEnum.Unknown, new StringU8("Unknown"u8)),
];
protected override StringU8 DisplayString(in CachedRecord item, int globalIndex)
=> StringU8.Empty;
protected static BoolEnum ToValue(OptionalBool value)
=> value.Value switch
{
true => BoolEnum.True,
false => BoolEnum.False,
null => BoolEnum.Unknown,
};
} }
private sealed class CustomLoadColumn : OptBoolColumn private sealed class CustomLoadColumn : OptBoolColumn
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 60 * Im.Style.GlobalScale;
public override bool FilterFunc(Record item) public CustomLoadColumn(ResourceWatcherConfig config)
=> FilterFunc(item.CustomLoad); : base(60f)
{
_config = config;
Filter.LoadValue(config.CustomFilter);
Filter.FilterChanged += OnFilterChanged;
}
public override void DrawColumn(Record item, int idx) private void OnFilterChanged()
=> DrawColumn(item.CustomLoad); {
_config.CustomFilter = Filter.FilterValue;
_config.Save();
}
protected override BoolEnum GetValue(in CachedRecord item, int globalIndex)
=> ToValue(item.Record.CustomLoad);
} }
private sealed class SynchronousLoadColumn : OptBoolColumn private sealed class SynchronousLoadColumn : OptBoolColumn
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 45 * Im.Style.GlobalScale;
public override bool FilterFunc(Record item) public SynchronousLoadColumn(ResourceWatcherConfig config)
=> FilterFunc(item.Synchronously); : base(45)
{
_config = config;
Filter.LoadValue(config.SyncFilter);
Filter.FilterChanged += OnFilterChanged;
}
public override void DrawColumn(Record item, int idx) private void OnFilterChanged()
=> DrawColumn(item.Synchronously); {
_config.SyncFilter = Filter.FilterValue;
_config.Save();
}
protected override BoolEnum GetValue(in CachedRecord item, int globalIndex)
=> ToValue(item.Record.Synchronously);
} }
private sealed class RefCountColumn : Column<Record> private sealed class RefCountColumn : NumberColumn<uint, CachedRecord>
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 30 * Im.Style.GlobalScale;
public override void DrawColumn(Record item, int _) public RefCountColumn(ResourceWatcherConfig config)
=> ImEx.TextRightAligned($"{item.RefCount}"); {
_config = config;
UnscaledWidth = 60;
Filter.Set(config.RefFilter);
Filter.FilterChanged += OnFilterChanged;
}
public override int Compare(Record lhs, Record rhs) private void OnFilterChanged()
=> lhs.RefCount.CompareTo(rhs.RefCount); {
_config.RefFilter = Filter.Text;
_config.Save();
}
public override uint ToValue(in CachedRecord item, int globalIndex)
=> item.Record.RefCount;
protected override SizedString DisplayNumber(in CachedRecord item, int globalIndex)
=> item.RefCount;
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.RefCount;
} }
private sealed class OsThreadColumn : ColumnString<Record> private sealed class OsThreadColumn : NumberColumn<uint, CachedRecord>
{ {
public override float Width private readonly ResourceWatcherConfig _config;
=> 60 * Im.Style.GlobalScale;
public override string ToName(Record item) public OsThreadColumn(ResourceWatcherConfig config)
=> item.OsThreadId.ToString(); {
_config = config;
UnscaledWidth = 60;
Filter.Set(config.ThreadFilter);
Filter.FilterChanged += OnFilterChanged;
}
public override void DrawColumn(Record item, int _) private void OnFilterChanged()
=> ImEx.TextRightAligned($"{item.OsThreadId}"); {
_config.ThreadFilter = Filter.Text;
_config.Save();
}
public override int Compare(Record lhs, Record rhs) public override uint ToValue(in CachedRecord item, int globalIndex)
=> lhs.OsThreadId.CompareTo(rhs.OsThreadId); => item.Record.OsThreadId;
protected override SizedString DisplayNumber(in CachedRecord item, int globalIndex)
=> item.Thread;
protected override string ComparisonText(in CachedRecord item, int globalIndex)
=> item.Thread;
} }
public override IEnumerable<CachedRecord> GetItems()
=> new CacheListAdapter<Record, CachedRecord>(_records, arg => new CachedRecord(arg));
protected override TableCache<CachedRecord> CreateCache()
=> new(this);
} }

Some files were not shown because too many files have changed in this diff Show more