Current state, all done except for file system selector.
Some checks failed
.NET Build / build (push) Has been cancelled

This commit is contained in:
Ottermandias 2025-12-28 00:06:32 +01:00
parent 97a14db4d5
commit cabcaadde3
41 changed files with 1749 additions and 1511 deletions

2
Luna

@ -1 +1 @@
Subproject commit e52d0dab9fd7f64d108125b79e387052fae2434f Subproject commit d81c788133b8b557febbad0bf74baee9588215eb

View file

@ -1,377 +1,396 @@
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Luna; using Luna;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Collections.Manager; namespace Penumbra.Collections.Manager;
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary> /// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId> public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
{ {
public static readonly LocalCollectionId Zero = new(0); public static readonly LocalCollectionId Zero = new(0);
public static LocalCollectionId operator +(LocalCollectionId left, int right) public static LocalCollectionId operator +(LocalCollectionId left, int right)
=> new(left.Id + right); => new(left.Id + right);
} }
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly SaveService _saveService; private readonly SaveService _saveService;
private readonly ModStorage _modStorage; private readonly ModStorage _modStorage;
public ModCollection Create(string name, int index, ModCollection? duplicate) public ModCollection Create(string name, int index, ModCollection? duplicate)
{ {
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index) var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count); ?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
_collectionsByLocal[CurrentCollectionId] = newCollection; _collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1; CurrentCollectionId += 1;
return newCollection; return newCollection;
} }
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings, public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> inheritances) IReadOnlyList<string> inheritances)
{ {
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
_collectionsByLocal[CurrentCollectionId] = newCollection; _collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1; CurrentCollectionId += 1;
return newCollection; return newCollection;
} }
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter) public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
{ {
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter); var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
_collectionsByLocal[CurrentCollectionId] = newCollection; _collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1; CurrentCollectionId += 1;
return newCollection; return newCollection;
} }
public void Delete(ModCollection collection) public void Delete(ModCollection collection)
=> _collectionsByLocal.Remove(collection.Identity.LocalId); => _collectionsByLocal.Remove(collection.Identity.LocalId);
/// <remarks> The empty collection is always available at Index 0. </remarks> /// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections = private readonly List<ModCollection> _collections =
[ [
ModCollection.Empty, ModCollection.Empty,
]; ];
/// <remarks> A list of all collections ever created still existing by their local id. </remarks> /// <remarks> A list of all collections ever created still existing by their local id. </remarks>
private readonly Dictionary<LocalCollectionId, ModCollection> private readonly Dictionary<LocalCollectionId, ModCollection>
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty }; _collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
public readonly ModCollection DefaultNamed; public readonly ModCollection DefaultNamed;
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks> /// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1; public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
/// <summary> Default enumeration skips the empty collection. </summary> /// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator() public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator(); => _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
public int Count public int Count
=> _collections.Count; => _collections.Count;
public ModCollection this[int index] public ModCollection this[int index]
=> _collections[index]; => _collections[index];
/// <summary> Find a collection by its name. If the name is empty or None, the empty collection is returned. </summary> /// <summary> Find a collection by its name. If the name is empty or None, the empty collection is returned. </summary>
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
{ {
if (name.Length != 0) if (name.Length != 0)
return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection); return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
collection = ModCollection.Empty; collection = ModCollection.Empty;
return true; return true;
} }
/// <summary> Find a collection by its id. If the GUID is empty, the empty collection is returned. </summary> /// <summary> Find a collection by its id. If the GUID is empty, the empty collection is returned. </summary>
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
{ {
if (id != Guid.Empty) if (id != Guid.Empty)
return _collections.FindFirst(c => c.Identity.Id == id, out collection); return _collections.FindFirst(c => c.Identity.Id == id, out collection);
collection = ModCollection.Empty; collection = ModCollection.Empty;
return true; return true;
} }
/// <summary> Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. </summary> /// <summary> Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. </summary>
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection) public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
{ {
if (Guid.TryParse(identifier, out var guid)) if (Guid.TryParse(identifier, out var guid))
return ById(guid, out collection); return ById(guid, out collection);
return ByName(identifier, out collection); return ByName(identifier, out collection);
} }
/// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary> /// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary>
public ModCollection ByLocalId(LocalCollectionId localId) public ModCollection ByLocalId(LocalCollectionId localId)
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty; => _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{ {
_communicator = communicator; _communicator = communicator;
_saveService = saveService; _saveService = saveService;
_modStorage = modStorage; _modStorage = modStorage;
_communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage); _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage);
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage); _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage);
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage); _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage);
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage); _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage);
ReadCollections(out DefaultNamed); ReadCollections(out DefaultNamed);
} }
public void Dispose() public void Dispose()
{ {
_communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted); _communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted);
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange); _communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged); _communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
} }
/// <summary> /// <summary>
/// Add a new collection of the given name. /// Add a new collection of the given name.
/// If duplicate is not-null, the new collection will be a duplicate of it. /// If duplicate is not-null, the new collection will be a duplicate of it.
/// If the name of the collection would result in an already existing filename, skip it. /// If the name of the collection would result in an already existing filename, skip it.
/// Returns true if the collection was successfully created and fires a Inactive event. /// Returns true if the collection was successfully created and fires a Inactive event.
/// Also sets the current collection to the new collection afterwards. /// Also sets the current collection to the new collection afterwards.
/// </summary> /// </summary>
public bool AddCollection(string name, ModCollection? duplicate) public bool AddCollection(string name, ModCollection? duplicate)
{ {
if (name.Length == 0) if (name.Length == 0)
return false; return false;
var newCollection = Create(name, _collections.Count, duplicate); var newCollection = Create(name, _collections.Count, duplicate);
_collections.Add(newCollection); _collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false); Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success,
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty)); false);
return true; _communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty));
} return true;
}
/// <summary>
/// Remove the given collection if it exists and is neither the empty nor the default-named collection. /// <summary> Rename a collection. </summary>
/// </summary> /// <param name="collection"> The collection to rename. </param>
public bool RemoveCollection(ModCollection collection) /// <param name="newName"> The new name for the collection. </param>
{ /// <returns> True if a change has taken place. </returns>
if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count) public bool RenameCollection(ModCollection collection, string newName)
{ {
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); var oldName = collection.Identity.Name;
return false; if (newName == oldName)
} return false;
if (collection.Identity.Index == DefaultNamed.Identity.Index) collection.Identity.Name = newName;
{ _saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); _communicator.CollectionRename.Invoke(new CollectionRename.Arguments(collection, oldName, newName));
return false; return true;
} }
Delete(collection); /// <summary>
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); /// Remove the given collection if it exists and is neither the empty nor the default-named collection.
_collections.RemoveAt(collection.Identity.Index); /// </summary>
// Update indices. public bool RemoveCollection(ModCollection collection)
for (var i = collection.Identity.Index; i < Count; ++i) {
_collections[i].Identity.Index = i; if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
_collectionsByLocal.Remove(collection.Identity.LocalId); {
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false); return false;
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty)); }
return true;
} if (collection.Identity.Index == DefaultNamed.Identity.Index)
{
/// <summary> Remove all settings for not currently-installed mods from the given collection. </summary> Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false);
public int CleanUnavailableSettings(ModCollection collection) return false;
{ }
var count = collection.Settings.Unused.Count;
if (count > 0) Delete(collection);
{ _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear(); _collections.RemoveAt(collection.Identity.Index);
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); // Update indices.
} for (var i = collection.Identity.Index; i < Count; ++i)
_collections[i].Identity.Index = i;
return count; _collectionsByLocal.Remove(collection.Identity.LocalId);
}
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary> _communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty));
public void CleanUnavailableSetting(ModCollection collection, string? setting) return true;
{ }
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); /// <summary> Remove all settings for not currently-installed mods from the given collection. </summary>
} public int CleanUnavailableSettings(ModCollection collection)
{
/// <summary> var count = collection.Settings.Unused.Count;
/// Read all collection files in the Collection Directory. if (count > 0)
/// Ensure that the default named collection exists, and apply inheritances afterward. {
/// Duplicate collection files are not deleted, just not added here. ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear();
/// </summary> _saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
private void ReadCollections(out ModCollection defaultNamedCollection) }
{
Penumbra.Log.Debug("[Collections] Reading saved collections..."); return count;
foreach (var file in _saveService.FileNames.CollectionFiles) }
{
if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance)) /// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
continue; public void CleanUnavailableSetting(ModCollection collection, string? setting)
{
if (id == Guid.Empty) if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
{ _saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning); }
continue;
} /// <summary>
/// Read all collection files in the Collection Directory.
if (ById(id, out _)) /// Ensure that the default named collection exists, and apply inheritances afterward.
{ /// Duplicate collection files are not deleted, just not added here.
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.", /// </summary>
NotificationType.Warning); private void ReadCollections(out ModCollection defaultNamedCollection)
continue; {
} Penumbra.Log.Debug("[Collections] Reading saved collections...");
foreach (var file in _saveService.FileNames.CollectionFiles)
var collection = CreateFromData(id, name, version, settings, inheritance); {
var correctName = _saveService.FileNames.CollectionFile(collection); if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance))
if (file.FullName != correctName) continue;
try
{ if (id == Guid.Empty)
if (version >= 2) {
{ Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
try continue;
{ }
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage( if (ById(id, out _))
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.", {
NotificationType.Warning); Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
} NotificationType.Warning);
catch (Exception ex) continue;
{ }
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}", var collection = CreateFromData(id, name, version, settings, inheritance);
NotificationType.Warning); var correctName = _saveService.FileNames.CollectionFile(collection);
} if (file.FullName != correctName)
} try
else {
{ if (version >= 2)
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection)); {
try try
{ {
File.Move(file.FullName, file.FullName + ".bak", true); File.Move(file.FullName, correctName, false);
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file."); Penumbra.Messager.NotificationMessage(
} $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
catch (Exception ex) NotificationType.Warning);
{ }
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}"); catch (Exception ex)
} {
} Penumbra.Messager.NotificationMessage(
} $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
catch (Exception e) NotificationType.Warning);
{ }
Penumbra.Messager.NotificationMessage(e, }
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.", else
NotificationType.Error); {
} _saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
try
_collections.Add(collection); {
} File.Move(file.FullName, file.FullName + ".bak", true);
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file.");
defaultNamedCollection = SetDefaultNamedCollection(); }
Penumbra.Log.Debug($"[Collections] Found {Count} saved collections."); catch (Exception ex)
} {
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}");
/// <summary> }
/// Add the collection with the default name if it does not exist. }
/// It should always be ensured that it exists, otherwise it will be created. }
/// This can also not be deleted, so there are always at least the empty and a collection with default name. catch (Exception e)
/// </summary> {
private ModCollection SetDefaultNamedCollection() Penumbra.Messager.NotificationMessage(e,
{ $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection)) NotificationType.Error);
return collection; }
if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null)) _collections.Add(collection);
return _collections[^1]; }
Penumbra.Messager.NotificationMessage( defaultNamedCollection = SetDefaultNamedCollection();
$"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.", Penumbra.Log.Debug($"[Collections] Found {Count} saved collections.");
NotificationType.Error); }
return Count > 1 ? _collections[1] : _collections[0];
} /// <summary>
/// Add the collection with the default name if it does not exist.
/// <summary> Move all settings in all collections to unused settings. </summary> /// It should always be ensured that it exists, otherwise it will be created.
private void OnModDiscoveryStarted() /// This can also not be deleted, so there are always at least the empty and a collection with default name.
{ /// </summary>
foreach (var collection in this) private ModCollection SetDefaultNamedCollection()
collection.Settings.PrepareModDiscovery(_modStorage); {
} if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
return collection;
/// <summary> Restore all settings in all collections to mods. </summary>
private void OnModDiscoveryFinished() if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
{ return _collections[^1];
// Re-apply all mod settings.
foreach (var collection in this) Penumbra.Messager.NotificationMessage(
collection.Settings.ApplyModSettings(collection, _saveService, _modStorage); $"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.",
} NotificationType.Error);
return Count > 1 ? _collections[1] : _collections[0];
/// <summary> Add or remove a mod from all collections, or re-save all collections where the mod has settings. </summary> }
private void OnModPathChange(in ModPathChanged.Arguments arguments)
{ /// <summary> Move all settings in all collections to unused settings. </summary>
switch (arguments.Type) private void OnModDiscoveryStarted()
{ {
case ModPathChangeType.Added: foreach (var collection in this)
foreach (var collection in this) collection.Settings.PrepareModDiscovery(_modStorage);
collection.Settings.AddMod(arguments.Mod); }
break;
case ModPathChangeType.Deleted: /// <summary> Restore all settings in all collections to mods. </summary>
foreach (var collection in this) private void OnModDiscoveryFinished()
collection.Settings.RemoveMod(arguments.Mod); {
break; // Re-apply all mod settings.
case ModPathChangeType.Moved: foreach (var collection in this)
var index = arguments.Mod.Index; collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null)) }
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
break; /// <summary> Add or remove a mod from all collections, or re-save all collections where the mod has settings. </summary>
case ModPathChangeType.Reloaded: private void OnModPathChange(in ModPathChanged.Arguments arguments)
foreach (var collection in this) {
{ switch (arguments.Type)
if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false) {
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); case ModPathChangeType.Added:
collection.Settings.SetTemporary(arguments.Mod.Index, null); foreach (var collection in this)
} collection.Settings.AddMod(arguments.Mod);
break;
break; case ModPathChangeType.Deleted:
} foreach (var collection in this)
} collection.Settings.RemoveMod(arguments.Mod);
break;
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary> case ModPathChangeType.Moved:
private void OnModOptionChange(in ModOptionChanged.Arguments arguments) var index = arguments.Mod.Index;
{ foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null))
arguments.Type.HandlingInfo(out var requiresSaving, out _, out _); _saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
if (!requiresSaving) break;
return; case ModPathChangeType.Reloaded:
foreach (var collection in this)
foreach (var collection in this) {
{ if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false)
if (collection.GetOwnSettings(arguments.Mod.Index)?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); collection.Settings.SetTemporary(arguments.Mod.Index, null);
collection.Settings.SetTemporary(arguments.Mod.Index, null); }
}
} break;
}
/// <summary> Update change counters when changing files. </summary> }
private void OnModFileChanged(in ModFileChanged.Arguments arguments)
{ /// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
if (arguments.File.CurrentUsage == 0) private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
return; {
arguments.Type.HandlingInfo(out var requiresSaving, out _, out _);
foreach (var collection in this) if (!requiresSaving)
{ return;
var (settings, _) = collection.GetActualSettings(arguments.Mod.Index);
if (settings is { Enabled: true }) foreach (var collection in this)
collection.Counters.IncrementChange(); {
} if (collection.GetOwnSettings(arguments.Mod.Index)
} ?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex)
} ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(arguments.Mod.Index, null);
}
}
/// <summary> Update change counters when changing files. </summary>
private void OnModFileChanged(in ModFileChanged.Arguments arguments)
{
if (arguments.File.CurrentUsage == 0)
return;
foreach (var collection in this)
{
var (settings, _) = collection.GetActualSettings(arguments.Mod.Index);
if (settings is { Enabled: true })
collection.Counters.IncrementChange();
}
}
}

View file

@ -39,8 +39,8 @@ public sealed class CollectionChange(Logger log)
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" /> /// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" />
ItemSwapTab = 0, ItemSwapTab = 0,
/// <seealso cref="UI.CollectionTab.CollectionSelector.OnCollectionChange" /> /// <seealso cref="UI.CollectionTab.CollectionSelector.Cache.OnCollectionChange" />
CollectionSelector = 0, CollectionSelectorCache = 0,
/// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/> /// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/>
IndividualAssignmentUi = 0, IndividualAssignmentUi = 0,

View file

@ -0,0 +1,20 @@
using Luna;
using Penumbra.Collections;
namespace Penumbra.Communication;
public sealed class CollectionRename(Logger log)
: EventBase<CollectionRename.Arguments, CollectionRename.Priority>(nameof(CollectionRename), log)
{
public enum Priority
{
/// <seealso cref="UI.CollectionTab.CollectionSelector.Cache.OnCollectionRename" />
CollectionSelectorCache = int.MinValue,
}
/// <summary> The arguments for a collection rename event. </summary>
/// <param name="Collection"> The renamed collection. </param>
/// <param name="OldName"> The old name of the collection. </param>
/// <param name="NewName"> The new name of the collection. </param>
public readonly record struct Arguments(ModCollection Collection, string OldName, string NewName);
}

View file

@ -26,25 +26,25 @@ public class EphemeralConfig : ISavable, IDisposable, IService
public float ModSelectorMinimumScale { get; set; } = 0.1f; public float ModSelectorMinimumScale { get; set; } = 0.1f;
public float ModSelectorMaximumScale { get; set; } = 0.5f; public float ModSelectorMaximumScale { get; set; } = 0.5f;
public int Version { get; set; } = Configuration.Constants.CurrentVersion; public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false; public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0; public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false; public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty; public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false; public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true; public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings; public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
public bool FixMainWindow { get; set; } = false; public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty; public string LastModPath { get; set; } = string.Empty;
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = []; public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = [];
public bool ForceRedrawOnFileChange { get; set; } = false; public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false; public bool IncognitoMode { get; set; } = false;
/// <summary> /// <summary>
/// Load the current configuration. /// Load the current configuration.

View file

@ -56,7 +56,8 @@ public class ModEditor(
SwapEditor.Revert(Option!); SwapEditor.Revert(Option!);
MetaEditor.Load(Mod!, Option!); MetaEditor.Load(Mod!, Option!);
Duplicates.Clear(); Duplicates.Clear();
MdlMaterialEditor.ScanModels(Mod!); MdlMaterialEditor.ScanModels(Mod!);
OptionLoaded?.Invoke();
}); });
} }
@ -80,21 +81,22 @@ public class ModEditor(
Files.UpdatePaths(Mod!, Option!); Files.UpdatePaths(Mod!, Option!);
MetaEditor.Load(Mod!, Option!); MetaEditor.Load(Mod!, Option!);
FileEditor.Clear(); FileEditor.Clear();
Duplicates.Clear(); Duplicates.Clear();
OptionLoaded?.Invoke();
}); });
} }
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary> /// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
private void LoadOption(int groupIdx, int dataIdx, bool message) private void LoadOption(int groupIdx, int dataIdx, bool message)
{ {
if (Mod != null && Mod.Groups.Count > groupIdx) if (Mod is not null && Mod.Groups.Count > groupIdx)
{ {
if (groupIdx == -1 && dataIdx == 0) if (groupIdx == -1 && dataIdx == 0)
{ {
Group = null; Group = null;
Option = Mod.Default; Option = Mod.Default;
GroupIdx = groupIdx; GroupIdx = groupIdx;
DataIdx = dataIdx; DataIdx = dataIdx;
return; return;
} }
@ -105,7 +107,7 @@ public class ModEditor(
{ {
Option = Group.DataContainers[dataIdx]; Option = Group.DataContainers[dataIdx];
GroupIdx = groupIdx; GroupIdx = groupIdx;
DataIdx = dataIdx; DataIdx = dataIdx;
return; return;
} }
} }
@ -119,6 +121,8 @@ public class ModEditor(
Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}.");
} }
public event Action? OptionLoaded;
public void Clear() public void Clear()
{ {
Duplicates.Clear(); Duplicates.Clear();
@ -126,7 +130,8 @@ public class ModEditor(
Files.Clear(); Files.Clear();
MetaEditor.Clear(); MetaEditor.Clear();
Mod = null; Mod = null;
LoadOption(0, 0, false); LoadOption(0, 0, false);
OptionLoaded?.Invoke();
} }
public void Dispose() public void Dispose()
@ -146,7 +151,7 @@ public class ModEditor(
foreach (var subDir in baseDir.GetDirectories()) foreach (var subDir in baseDir.GetDirectories())
{ {
ClearEmptySubDirectories(subDir); ClearEmptySubDirectories(subDir);
if (subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0) if (subDir.GetFiles().Length is 0 && subDir.GetDirectories().Length is 0)
subDir.Delete(); subDir.Delete();
} }
} }

View file

@ -1,208 +1,197 @@
using Luna; using ImSharp.Containers;
using Penumbra.Mods.SubMods; using Luna;
using Penumbra.String.Classes; using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Editor;
namespace Penumbra.Mods.Editor;
public class ModFileCollection : IDisposable
{ public class ModFileCollection : IDisposable
private readonly List<FileRegistry> _available = []; {
private readonly List<FileRegistry> _mtrl = []; private readonly ObservableList<FileRegistry> _available = [];
private readonly List<FileRegistry> _mdl = []; private readonly ObservableList<FileRegistry> _mtrl = [];
private readonly List<FileRegistry> _tex = []; private readonly ObservableList<FileRegistry> _mdl = [];
private readonly List<FileRegistry> _shpk = []; private readonly ObservableList<FileRegistry> _tex = [];
private readonly List<FileRegistry> _pbd = []; private readonly ObservableList<FileRegistry> _shpk = [];
private readonly List<FileRegistry> _atch = []; private readonly ObservableList<FileRegistry> _pbd = [];
private readonly ObservableList<FileRegistry> _atch = [];
private readonly SortedSet<FullPath> _missing = [];
private readonly HashSet<Utf8GamePath> _usedPaths = []; private readonly SortedSet<FullPath> _missing = [];
private readonly HashSet<Utf8GamePath> _usedPaths = [];
public IReadOnlySet<FullPath> Missing
=> Ready ? _missing : []; public IReadOnlySet<FullPath> Missing
=> Ready ? _missing : [];
public IReadOnlySet<Utf8GamePath> UsedPaths
=> Ready ? _usedPaths : []; public IReadOnlySet<Utf8GamePath> UsedPaths
=> Ready ? _usedPaths : [];
public IReadOnlyList<FileRegistry> Available
=> Ready ? _available : []; public IObservableList<FileRegistry> Available
=> Ready ? _available : [];
public IReadOnlyList<FileRegistry> Mtrl
=> Ready ? _mtrl : []; public IObservableList<FileRegistry> Mtrl
=> Ready ? _mtrl : [];
public IReadOnlyList<FileRegistry> Mdl
=> Ready ? _mdl : []; public IObservableList<FileRegistry> Mdl
=> Ready ? _mdl : [];
public IReadOnlyList<FileRegistry> Tex
=> Ready ? _tex : []; public IObservableList<FileRegistry> Tex
=> Ready ? _tex : [];
public IReadOnlyList<FileRegistry> Shpk
=> Ready ? _shpk : []; public IObservableList<FileRegistry> Shpk
=> Ready ? _shpk : [];
public IReadOnlyList<FileRegistry> Pbd
=> Ready ? _pbd : []; public IObservableList<FileRegistry> Pbd
=> Ready ? _pbd : [];
public IReadOnlyList<FileRegistry> Atch
=> Ready ? _atch : []; public IObservableList<FileRegistry> Atch
=> Ready ? _atch : [];
public bool Ready { get; private set; } = true;
public bool Ready { get; private set; } = true;
public void UpdateAll(Mod mod, IModDataContainer option)
{ public void UpdateAll(Mod mod, IModDataContainer option)
UpdateFiles(mod, CancellationToken.None); {
UpdatePaths(mod, option, false, CancellationToken.None); UpdateFiles(mod, CancellationToken.None);
} UpdatePaths(mod, option, false, CancellationToken.None);
}
public void UpdatePaths(Mod mod, IModDataContainer option)
=> UpdatePaths(mod, option, true, CancellationToken.None); public void UpdatePaths(Mod mod, IModDataContainer option)
=> UpdatePaths(mod, option, true, CancellationToken.None);
public void Clear()
{ public void Clear()
ClearFiles(); {
ClearPaths(false, CancellationToken.None); ClearFiles();
} ClearPaths(false, CancellationToken.None);
}
public void Dispose()
=> Clear(); public void Dispose()
=> Clear();
public void ClearMissingFiles()
=> _missing.Clear(); public void ClearMissingFiles()
=> _missing.Clear();
public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{ public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
_usedPaths.Remove(gamePath); {
if (file != null) _usedPaths.Remove(gamePath);
{ if (file != null)
--file.CurrentUsage; {
file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath)); --file.CurrentUsage;
} file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath));
} }
}
public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
=> RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
=> RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{ public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
_usedPaths.Add(gamePath); {
if (file == null) _usedPaths.Add(gamePath);
return; if (file == null)
return;
++file.CurrentUsage;
file.SubModUsage.Add((option, gamePath)); ++file.CurrentUsage;
} file.SubModUsage.Add((option, gamePath));
}
public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
=> AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
=> AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
{ public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
var oldPath = file.SubModUsage[pathIdx]; {
_usedPaths.Remove(oldPath.Item2); var oldPath = file.SubModUsage[pathIdx];
if (!gamePath.IsEmpty) _usedPaths.Remove(oldPath.Item2);
{ if (!gamePath.IsEmpty)
file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath); {
_usedPaths.Add(gamePath); file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath);
} _usedPaths.Add(gamePath);
else }
{ else
--file.CurrentUsage; {
file.SubModUsage.RemoveAt(pathIdx); --file.CurrentUsage;
} file.SubModUsage.RemoveAt(pathIdx);
} }
}
private void UpdateFiles(Mod mod, CancellationToken tok)
{ private void UpdateFiles(Mod mod, CancellationToken tok)
tok.ThrowIfCancellationRequested(); {
ClearFiles(); tok.ThrowIfCancellationRequested();
ClearFiles();
foreach (var file in mod.ModPath.EnumerateDirectories().Where(d => !d.IsHidden()).SelectMany(FileExtensions.EnumerateNonHiddenFiles))
{ foreach (var file in mod.ModPath.EnumerateDirectories().Where(d => !d.IsHidden()).SelectMany(FileExtensions.EnumerateNonHiddenFiles))
tok.ThrowIfCancellationRequested(); {
if (!FileRegistry.FromFile(mod.ModPath, file, out var registry)) tok.ThrowIfCancellationRequested();
continue; if (!FileRegistry.FromFile(mod.ModPath, file, out var registry))
continue;
_available.Add(registry);
switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant()) _available.Add(registry);
{ switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant())
case ".mtrl": {
_mtrl.Add(registry); case ".mtrl": _mtrl.Add(registry); break;
break; case ".mdl": _mdl.Add(registry); break;
case ".mdl": case ".tex": _tex.Add(registry); break;
_mdl.Add(registry); case ".shpk": _shpk.Add(registry); break;
break; case ".pbd": _pbd.Add(registry); break;
case ".tex": case ".atch": _atch.Add(registry); break;
_tex.Add(registry); }
break; }
case ".shpk": }
_shpk.Add(registry);
break; private void ClearFiles()
case ".pbd": {
_pbd.Add(registry); _available.Clear();
break; _mtrl.Clear();
case ".atch": _mdl.Clear();
_atch.Add(registry); _tex.Clear();
break; _shpk.Clear();
} _pbd.Clear();
} _atch.Clear();
} }
private void ClearFiles() private void ClearPaths(bool clearRegistries, CancellationToken tok)
{ {
_available.Clear(); if (clearRegistries)
_mtrl.Clear(); foreach (var reg in _available)
_mdl.Clear(); {
_tex.Clear(); tok.ThrowIfCancellationRequested();
_shpk.Clear(); reg.CurrentUsage = 0;
_pbd.Clear(); reg.SubModUsage.Clear();
_atch.Clear(); }
}
_missing.Clear();
private void ClearPaths(bool clearRegistries, CancellationToken tok) _usedPaths.Clear();
{ }
if (clearRegistries)
foreach (var reg in _available) private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok)
{ {
tok.ThrowIfCancellationRequested(); tok.ThrowIfCancellationRequested();
reg.CurrentUsage = 0; ClearPaths(clearRegistries, tok);
reg.SubModUsage.Clear();
} tok.ThrowIfCancellationRequested();
_missing.Clear(); foreach (var subMod in mod.AllDataContainers)
_usedPaths.Clear(); {
} foreach (var (gamePath, file) in subMod.Files)
{
private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok) tok.ThrowIfCancellationRequested();
{ if (!file.Exists)
tok.ThrowIfCancellationRequested(); {
ClearPaths(clearRegistries, tok); _missing.Add(file);
if (subMod == option)
tok.ThrowIfCancellationRequested(); _usedPaths.Add(gamePath);
}
foreach (var subMod in mod.AllDataContainers) else
{ {
foreach (var (gamePath, file) in subMod.Files) var registry = _available.Find(x => x.File.Equals(file));
{ if (registry == null)
tok.ThrowIfCancellationRequested(); continue;
if (!file.Exists)
{ if (subMod == option)
_missing.Add(file); {
if (subMod == option) ++registry.CurrentUsage;
_usedPaths.Add(gamePath); _usedPaths.Add(gamePath);
} }
else
{ registry.SubModUsage.Add((subMod, gamePath));
var registry = _available.Find(x => x.File.Equals(file)); }
if (registry == null) }
continue; }
}
if (subMod == option) }
{
++registry.CurrentUsage;
_usedPaths.Add(gamePath);
}
registry.SubModUsage.Add((subMod, gamePath));
}
}
}
}
}

View file

@ -5,6 +5,9 @@ namespace Penumbra.Services;
public class CommunicatorService(ServiceManager services) : IService public class CommunicatorService(ServiceManager services) : IService
{ {
/// <inheritdoc cref="Communication.CollectionRename"/>
public readonly CollectionRename CollectionRename = services.GetService<CollectionRename>();
/// <inheritdoc cref="Communication.CollectionChange"/> /// <inheritdoc cref="Communication.CollectionChange"/>
public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>(); public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>();

View file

@ -124,7 +124,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu
?? _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<CollectionPanelMode>() ?? _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;

View file

@ -1,4 +1,3 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImSharp; using ImSharp;
using OtterGui.Widgets; using OtterGui.Widgets;
@ -8,121 +7,181 @@ 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.FilterComboColors;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.Services; namespace Penumbra.Services;
//public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) : SimpleFilterCombo<StmKeyType>(SimpleFilterType.Text) public sealed record StainTemplate(StringPair Id, Vector4 Diffuse, Vector4 Specular, Vector4 Emissive, int Key, bool Found)
// where TDyePack : unmanaged, IDyePack
//{
// public override StringU8 DisplayString(in StmKeyType value)
// => new($"{value,4}");
//
// public override string FilterString(in StmKeyType value)
// => $"{value,4}";
//
// public override IEnumerable<StmKeyType> GetBaseItems()
// => throw new NotImplementedException();
//
// protected override bool DrawFilter(float width, FilterComboBaseCache<SimpleCacheItem<StmKeyType>> cache)
// {
// using var font = Im.Font.PushDefault();
// return base.DrawFilter(width, cache);
// }
//
// public bool Draw(Utf8StringHandler<LabelStringHandlerBuffer> label, Utf8StringHandler<HintStringHandlerBuffer> preview, Utf8StringHandler<TextStringHandlerBuffer> tooltip, ref int currentSelection, float previewWidth, float itemHeight,
// ComboFlags flags = ComboFlags.None)
// {
// using var font = Im.Font.PushMono();
// using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f))
// .PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X);
// var spaceSize = Im.Font.Mono.GetCharacterAdvance(' ');
// var spaces = (int)(previewWidth / spaceSize) - 1;
// return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
// }
//
// protected override bool DrawSelectable(int globalIdx, bool selected)
// {
// var ret = base.DrawSelectable(globalIdx, selected);
// var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key;
// if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
// return ret;
//
// Im.Line.Same();
//
// var frame = new Vector2(Im.Style.TextHeight);
// Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame);
// Im.Line.Same();
// Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame);
// Im.Line.Same();
// Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame);
// return ret;
// }
//}
public class StainService : Luna.IService
{ {
public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) public Vector4 Diffuse { get; set; } = Diffuse;
: FilterComboCache<StmKeyType>(stmFile.Entries.Keys.Prepend(0), MouseWheelType.None, Penumbra.Log) public Vector4 Specular { get; set; } = Specular;
where TDyePack : unmanaged, IDyePack public Vector4 Emissive { get; set; } = Emissive;
public bool Found { get; set; } = Found;
}
public sealed class TemplateFilter : TextFilterBase<StainTemplate>
{
protected override string ToFilterString(in StainTemplate item, int globalIndex)
=> item.Id;
}
public sealed class StainTemplateCombo<TDyePack> : ImSharp.FilterComboBase<StainTemplate>
where TDyePack : unmanaged, IDyePack
{
private readonly StainService.StainCombo[] _stainCombos;
private readonly StmFile<TDyePack> _stmFile;
private int _currentDyeChannel;
private ushort _currentSelection;
public StainTemplateCombo(StainService.StainCombo[] stainCombos, StmFile<TDyePack> stmFile)
: base(new TemplateFilter())
{ {
// FIXME There might be a better way to handle that. PreviewAlignment = new Vector2(0.90f, 0.5f);
public int CurrentDyeChannel = 0; _stainCombos = stainCombos;
_stmFile = stmFile;
ComputeWidth = true;
}
protected override float GetFilterWidth() protected override FilterComboBaseCache<StainTemplate> CreateCache()
=> new Cache(this);
private sealed class Cache : FilterComboBaseCache<StainTemplate>
{
private readonly StainTemplateCombo<TDyePack> _parent;
private int _dyeChannel;
public Cache(StainTemplateCombo<TDyePack> parent)
: base(parent)
{ {
var baseSize = Im.Font.CalculateSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X; _parent = parent;
if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0) foreach (var combo in _parent._stainCombos)
return baseSize; combo.SelectionChanged += OnSelectionChanged;
_dyeChannel = _parent._currentDyeChannel;
return baseSize + Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
} }
protected override string ToString(StmKeyType obj) public override void Update()
=> $"{obj,4}";
protected override void DrawFilter(int currentSelected, float width)
{ {
using var font = Im.Font.PushDefault(); base.Update();
base.DrawFilter(currentSelected, width); if (_dyeChannel != _parent._currentDyeChannel)
{
UpdateItems();
ComputeWidth();
_dyeChannel = _parent._currentDyeChannel;
}
} }
public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight, private void OnSelectionChanged(Luna.FilterComboColors.Item obj)
ImGuiComboFlags flags = ImGuiComboFlags.None)
{ {
using var font = Im.Font.PushMono(); UpdateItems();
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f)) ComputeWidth();
.PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X);
var spaceSize = Im.Font.Mono.GetCharacterAdvance(' ');
var spaces = (int)(previewWidth / spaceSize) - 1;
return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
} }
protected override bool DrawSelectable(int globalIdx, bool selected) private void UpdateItems()
{ {
var ret = base.DrawSelectable(globalIdx, selected); foreach (var item in UnfilteredItems)
var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; {
if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) var dye = _parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id;
return ret; if (dye > 0 && _parent._stmFile.TryGetValue(item.Key, dye, out var dyes))
{
item.Found = true;
item.Diffuse = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1);
item.Specular = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1);
item.Emissive = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1);
}
else
{
item.Found = false;
}
}
}
Im.Line.Same();
protected override void ComputeWidth()
var frame = new Vector2(Im.Style.TextHeight); {
Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); ComboWidth = Im.Font.Mono.CalculateTextSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X;
Im.Line.Same(); if (_parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id is 0)
Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); return;
Im.Line.Same();
Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame); ComboWidth += Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
return ret; }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
foreach (var combo in _parent._stainCombos)
combo.SelectionChanged -= OnSelectionChanged;
} }
} }
public bool Draw(Utf8StringHandler<LabelStringHandlerBuffer> label, ushort currentSelection, int currentDyeChannel,
Utf8StringHandler<TextStringHandlerBuffer> tooltip, out int newSelection, float previewWidth, float itemHeight,
ComboFlags flags = ComboFlags.None)
{
Flags = flags;
_currentDyeChannel = currentDyeChannel;
_currentSelection = currentSelection;
using var font = Im.Font.PushMono();
if (!base.Draw(label, $"{_currentSelection,4}", tooltip, previewWidth, out var selection))
{
newSelection = 0;
return false;
}
newSelection = selection.Key;
return true;
}
protected override IEnumerable<StainTemplate> GetItems()
{
var dye = _stainCombos[_currentDyeChannel].CurrentSelection.Id;
foreach (var key in _stmFile.Entries.Keys.Prepend(0))
{
if (dye > 0 && _stmFile.TryGetValue(key, dye, out var dyes))
yield return new StainTemplate(new StringPair($"{key,4}"),
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1),
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1),
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1), key.Int, true);
else
yield return new StainTemplate(new StringPair($"{key,4}"), Vector4.Zero, Vector4.Zero, Vector4.Zero, key.Int, false);
}
}
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in StainTemplate item, int globalIndex, bool selected)
{
var ret = Im.Selectable(item.Id.Utf8, selected);
if (item.Found)
{
Im.Line.SameInner();
var frame = new Vector2(Im.Style.TextHeight);
Im.Color.Button("D"u8, item.Diffuse, 0, frame);
Im.Line.SameInner();
Im.Color.Button("S"u8, item.Specular, 0, frame);
Im.Line.SameInner();
Im.Color.Button("E"u8, item.Emissive, 0, frame);
}
return ret;
}
protected override bool IsSelected(StainTemplate item, int globalIndex)
=> item.Key == _currentSelection;
protected override bool DrawFilter(float width, FilterComboBaseCache<StainTemplate> cache)
{
using var font = Im.Font.PushDefault();
return base.DrawFilter(width, cache);
}
}
public class StainService : Luna.IService
{
public const int ChannelCount = 2; public const int ChannelCount = 2;
public readonly DictStain StainData; public readonly StainCombo StainCombo1;
public readonly FilterComboColors StainCombo1; public readonly StainCombo StainCombo2; // FIXME is there a better way to handle this?
public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this?
public readonly StmFile<LegacyDyePack> LegacyStmFile; public readonly StmFile<LegacyDyePack> LegacyStmFile;
public readonly StmFile<DyePack> GudStmFile; public readonly StmFile<DyePack> GudStmFile;
public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo; public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo;
@ -130,9 +189,8 @@ public class StainService : Luna.IService
public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData)
{ {
StainData = stainData; StainCombo1 = new StainCombo(stainData);
StainCombo1 = CreateStainCombo(); StainCombo2 = new StainCombo(stainData);
StainCombo2 = CreateStainCombo();
if (characterUtility.Address == null) if (characterUtility.Address == null)
{ {
@ -146,14 +204,14 @@ public class StainService : Luna.IService
} }
FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; StainCombo[] stainCombos = [StainCombo1, StainCombo2];
LegacyTemplateCombo = new StainTemplateCombo<LegacyDyePack>(stainCombos, LegacyStmFile); LegacyTemplateCombo = new StainTemplateCombo<LegacyDyePack>(stainCombos, LegacyStmFile);
GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile); GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile);
} }
/// <summary> Retrieves the <see cref="FilterComboColors"/> instance for the given channel. Indexing is zero-based. </summary> /// <summary> Retrieves the <see cref="FilterComboColors"/> instance for the given channel. Indexing is zero-based. </summary>
public FilterComboColors GetStainCombo(int channel) public StainCombo GetStainCombo(int channel)
=> channel switch => channel switch
{ {
0 => StainCombo1, 0 => StainCombo1,
@ -180,8 +238,9 @@ public class StainService : Luna.IService
return new StmFile<TDyePack>(dataManager); return new StmFile<TDyePack>(dataManager);
} }
private FilterComboColors CreateStainCombo() public sealed class StainCombo(DictStain stainData) : Luna.FilterComboColors
=> new(140, MouseWheelType.None, {
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(), protected override IEnumerable<Item> GetItems()
Penumbra.Log); => stainData.Value.Select(t => new Item(new StringPair(t.Value.Name), t.Value.Dye, t.Key, t.Value.Gloss)).Prepend(None);
}
} }

View file

@ -2,8 +2,6 @@ using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui.Classes;
using OtterGui.Widgets;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
@ -11,7 +9,6 @@ using Penumbra.Mods.Editor;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -24,7 +21,7 @@ public class FileEditor<T>(
FileDialogService fileDialog, FileDialogService fileDialog,
string tabName, string tabName,
string fileType, string fileType,
Func<IReadOnlyList<FileRegistry>> getFiles, Func<IEnumerable<FileRegistry>> getFiles,
Func<T, bool, bool> drawEdit, Func<T, bool, bool> drawEdit,
Func<string> getInitialPath, Func<string> getInitialPath,
Func<byte[], string, bool, T?> parseFile) Func<byte[], string, bool, T?> parseFile)
@ -74,10 +71,15 @@ public class FileEditor<T>(
_defaultFile = null; _defaultFile = null;
} }
private FileRegistry? _currentPath; private FileRegistry? CurrentPath
private T? _currentFile; {
private Exception? _currentException; get => _combo.Selected;
private bool _changed; set => _combo.Selected = value;
}
private T? _currentFile;
private Exception? _currentException;
private bool _changed;
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty; private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
private bool _inInput; private bool _inInput;
@ -163,7 +165,7 @@ public class FileEditor<T>(
public void Reset() public void Reset()
{ {
_currentException = null; _currentException = null;
_currentPath = null; CurrentPath = null;
(_currentFile as IDisposable)?.Dispose(); (_currentFile as IDisposable)?.Dispose();
_currentFile = null; _currentFile = null;
_changed = false; _changed = false;
@ -171,26 +173,32 @@ public class FileEditor<T>(
private void DrawFileSelectCombo() private void DrawFileSelectCombo()
{ {
if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty, if (CurrentPath is not null)
Im.ContentRegion.Available.X, Im.Style.TextHeight) {
&& _combo.CurrentSelection != null) if (_combo.Draw("##select"u8, CurrentPath.RelPath.Path.Span, StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection))
UpdateCurrentFile(_combo.CurrentSelection); UpdateCurrentFile(newSelection);
}
else
{
if (_combo.Draw("##select"u8, $"Select {fileType} File...", StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection))
UpdateCurrentFile(newSelection);
}
} }
private void UpdateCurrentFile(FileRegistry path) private void UpdateCurrentFile(FileRegistry path)
{ {
if (ReferenceEquals(_currentPath, path)) if (ReferenceEquals(CurrentPath, path))
return; return;
_changed = false; _changed = false;
_currentPath = path; CurrentPath = path;
_currentException = null; _currentException = null;
try try
{ {
var bytes = File.ReadAllBytes(_currentPath.File.FullName); var bytes = File.ReadAllBytes(CurrentPath.File.FullName);
(_currentFile as IDisposable)?.Dispose(); (_currentFile as IDisposable)?.Dispose();
_currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
_currentFile = parseFile(bytes, _currentPath.File.FullName, true); _currentFile = parseFile(bytes, CurrentPath.File.FullName, true);
} }
catch (Exception e) catch (Exception e)
{ {
@ -210,9 +218,9 @@ public class FileEditor<T>(
public void SaveFile() public void SaveFile()
{ {
compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); compactor.WriteAllBytes(CurrentPath!.File.FullName, _currentFile!.Write());
if (owner.Mod is not null) if (owner.Mod is not null)
communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, _currentPath)); communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, CurrentPath));
_changed = false; _changed = false;
} }
@ -221,8 +229,8 @@ public class FileEditor<T>(
if (ImEx.Button("Reset Changes"u8, Vector2.Zero, if (ImEx.Button("Reset Changes"u8, Vector2.Zero,
$"Reset all changes made to the {fileType} file.", !_changed)) $"Reset all changes made to the {fileType} file.", !_changed))
{ {
var tmp = _currentPath; var tmp = CurrentPath;
_currentPath = null; CurrentPath = null;
UpdateCurrentFile(tmp!); UpdateCurrentFile(tmp!);
} }
} }
@ -233,7 +241,7 @@ public class FileEditor<T>(
if (!child) if (!child)
return; return;
if (_currentPath is not null) if (CurrentPath is not null)
{ {
if (_currentFile is null) if (_currentFile is null)
{ {
@ -253,7 +261,7 @@ public class FileEditor<T>(
if (!_inInput && _defaultPath.Length > 0) if (!_inInput && _defaultPath.Length > 0)
{ {
if (_currentPath is not null) if (CurrentPath is not null)
{ {
Im.Line.New(); Im.Line.New();
Im.Line.New(); Im.Line.New();
@ -278,47 +286,70 @@ public class FileEditor<T>(
} }
} }
private class Combo(Func<IReadOnlyList<FileRegistry>> generator) private sealed class Combo : FilterComboBase<FileRegistry>
: FilterComboCache<FileRegistry>(generator, MouseWheelType.None, Penumbra.Log)
{ {
protected override bool DrawSelectable(int globalIdx, bool selected) private sealed class FileFilter : RegexFilterBase<FileRegistry>
{
// TODO: Avoid ToString.
public override bool WouldBeVisible(in FileRegistry item, int globalIndex)
=> WouldBeVisible(item.File.FullName) || item.SubModUsage.Any(f => WouldBeVisible(f.Item2.ToString()));
/// <summary> Unused. </summary>
protected override string ToFilterString(in FileRegistry item, int globalIndex)
=> string.Empty;
}
private readonly Func<IEnumerable<FileRegistry>> _getFiles;
public FileRegistry? Selected;
public Combo(Func<IEnumerable<FileRegistry>> getFiles)
{
_getFiles = getFiles;
Filter = new FileFilter();
}
protected override IEnumerable<FileRegistry> GetItems()
=> _getFiles();
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in FileRegistry item, int globalIndex, bool selected)
{ {
var file = Items[globalIdx];
bool ret; bool ret;
using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), item.IsOnPlayer))
{ {
ret = Im.Selectable(file.RelPath.ToString(), selected); ret = Im.Selectable(item.RelPath.Path.Span, selected);
} }
if (Im.Item.Hovered()) if (Im.Item.Hovered())
{ {
using var tt = Im.Tooltip.Begin(); using var style = Im.Style.PushDefault(ImStyleDouble.WindowPadding);
using var tt = Im.Tooltip.Begin();
Im.Text("All Game Paths"u8); Im.Text("All Game Paths"u8);
Im.Separator(); Im.Separator();
using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit); using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit);
if (t) if (t)
{ foreach (var (option, gamePath) in item.SubModUsage)
foreach (var (option, gamePath) in file.SubModUsage)
{ {
t.DrawColumn(gamePath.Path.Span); t.DrawColumn(gamePath.Path.Span);
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
t.DrawColumn(option.GetFullName()); t.DrawColumn(option.GetFullName());
} }
}
} }
if (file.SubModUsage.Count > 0) if (item.SubModUsage.Count > 0)
{ {
Im.Line.Same(); Im.Line.Same();
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
ImEx.TextRightAligned($"{file.SubModUsage[0].Item2.Path}"); ImEx.TextRightAligned($"{item.SubModUsage[0].Item2.Path}");
} }
return ret; return ret;
} }
protected override bool IsVisible(int globalIndex, LowerString filter) protected override bool IsSelected(FileRegistry item, int globalIndex)
=> filter.IsContained(Items[globalIndex].File.FullName) => item.Equals(Selected);
|| Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString()));
} }
} }

View file

@ -0,0 +1,94 @@
using ImSharp;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Mods;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
namespace Penumbra.UI.AdvancedWindow;
public sealed class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type)
: FilterComboBase<ItemSelector.CacheItem>(new ItemFilter())
{
public EquipItem CurrentSelection = new(string.Empty, default, default, default, default, default, FullEquipType.Unknown, default, default,
default);
public sealed record CacheItem(EquipItem Item, StringPair Name, Vector4 Color, bool InCurrentMod, StringU8[] CollectionMods)
{
public Vector4 Color { get; set; } = Color;
public CacheItem(EquipItem item, Mod currentMod, ModCollection currentCollection)
: this(item, new StringPair(item.Name), Im.Style[ImGuiColor.Text], currentMod.ChangedItems.Any(c => c.Key == item.Name),
ConvertCollection(item, currentCollection))
{
if (InCurrentMod)
Color = ColorId.ResTreeLocalPlayer.Value().ToVector();
else if (CollectionMods.Length > 0)
Color = ColorId.ResTreeNonNetworked.Value().ToVector();
}
public CacheItem(EquipItem item, ModCollection currentCollection)
: this(item, new StringPair(item.Name), Im.Style[ImGuiColor.Text], false, ConvertCollection(item, currentCollection))
{
if (CollectionMods.Length > 0)
Color = ColorId.ResTreeNonNetworked.Value().ToVector();
}
private static StringU8[] ConvertCollection(in EquipItem item, ModCollection collection)
{
if (!collection.ChangedItems.TryGetValue(item.Name, out var mods))
return [];
var ret = new StringU8[mods.Item1.Count];
for (var i = 0; i < mods.Item1.Count; ++i)
ret[i] = new StringU8(mods.Item1[i].Name);
return ret;
}
}
private sealed class ItemFilter : PartwiseFilterBase<CacheItem>
{
protected override string ToFilterString(in CacheItem item, int globalIndex)
=> item.Name.Utf16;
}
protected override IEnumerable<CacheItem> GetItems()
{
var list = data.ByType[type];
if (selector?.Selected is { } currentMod && currentMod.ChangedItems.Values.Any(c => c is IdentifiedItem i && i.Item.Type == type))
return list.Select(item => new CacheItem(item, currentMod, collections.Current)).OrderByDescending(i => i.InCurrentMod)
.ThenByDescending(i => i.CollectionMods.Length);
if (selector is null)
return list.Select(item => new CacheItem(item, collections.Current)).OrderBy(i => i.CollectionMods.Length);
return list.Select(item => new CacheItem(item, collections.Current)).OrderByDescending(i => i.CollectionMods.Length);
}
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in CacheItem item, int globalIndex, bool selected)
{
using var color = item.Color.W > 0 ? ImGuiColor.Text.Push(item.Color) : null;
var ret = Im.Selectable(item.Name.Utf8, selected);
if (item.CollectionMods.Length > 0 && Im.Item.Hovered())
{
using var style = Im.Style.PushDefault(ImStyleDouble.WindowPadding);
using var tt = Im.Tooltip.Begin();
foreach (var mod in item.CollectionMods)
Im.Text(mod);
}
if (ret)
CurrentSelection = item.Item;
return ret;
}
protected override bool IsSelected(CacheItem item, int globalIndex)
=> item.Item.Equals(CurrentSelection);
}

View file

@ -2,7 +2,6 @@ using Dalamud.Interface.ImGuiNotification;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Luna.Generators; using Luna.Generators;
using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Communication; using Penumbra.Communication;
@ -12,7 +11,6 @@ using Penumbra.GameData.Structs;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.ItemSwap; using Penumbra.Mods.ItemSwap;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
@ -22,8 +20,6 @@ using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab;
using ITab = OtterGui.Widgets.ITab;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -152,44 +148,6 @@ public class ItemSwapTab : IDisposable, ITab
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
} }
private class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type)
: FilterComboCache<(EquipItem Item, bool InMod, SingleArray<IMod> InCollection)>(() =>
{
var list = data.ByType[type];
var enumerable = selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)
? list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name),
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
.OrderByDescending(p => p.Item2).ThenByDescending(p => p.Item3.Count)
: selector is null
? list.Select(i => (i, false,
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
.OrderBy(p => p.Item3.Count)
: list.Select(i => (i, false,
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
.OrderByDescending(p => p.Item3.Count);
return enumerable.ToList();
}, MouseWheelType.None, Penumbra.Log)
{
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var (_, inMod, inCollection) = Items[globalIdx];
using var color = inMod
? ImGuiColor.Text.Push(ColorId.ResTreeLocalPlayer.Value())
: inCollection.Count > 0
? ImGuiColor.Text.Push(ColorId.ResTreeNonNetworked.Value())
: null;
var ret = base.DrawSelectable(globalIdx, selected);
if (inCollection.Count > 0)
Im.Tooltip.OnHover(string.Join('\n', inCollection.Select(m => m.Name)));
return ret;
}
protected override string ToString((EquipItem Item, bool InMod, SingleArray<IMod> InCollection) obj)
=> obj.Item.Name;
}
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, StringU8 TextFrom, StringU8 TextTo)> _selectors; private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, StringU8 TextFrom, StringU8 TextTo)> _selectors;
private readonly ItemSwapContainer _swapData; private readonly ItemSwapContainer _swapData;
@ -241,17 +199,17 @@ public class ItemSwapTab : IDisposable, ITab
case SwapType.Ring: case SwapType.Ring:
case SwapType.Glasses: case SwapType.Glasses:
var values = _selectors[_lastTab]; var values = _selectors[_lastTab];
if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown if (values.Source.CurrentSelection.Type is not FullEquipType.Unknown
&& values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) && values.Target.CurrentSelection.Type is not FullEquipType.Unknown)
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection,
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
break; break;
case SwapType.BetweenSlots: case SwapType.BetweenSlots:
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid)
_affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom), _affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection, ToEquipSlot(_slotFrom),
selectorFrom.CurrentSelection.Item, selectorFrom.CurrentSelection,
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0: case SwapType.Hair when _targetId > 0 && _sourceId > 0:
@ -315,10 +273,10 @@ public class ItemSwapTab : IDisposable, ITab
$"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}"; $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}";
case SwapType.BetweenSlots: case SwapType.BetweenSlots:
return return
$"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}";
default: default:
return return
$"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}";
} }
} }
@ -543,9 +501,7 @@ public class ItemSwapTab : IDisposable, ITab
} }
table.NextColumn(); table.NextColumn();
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name, string.Empty, _dirty |= selector.Draw("##itemSource"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
InputWidth * 2 * Im.Style.GlobalScale,
Im.Style.TextHeightWithSpacing);
(article1, _, selector) = GetAccessorySelector(_slotTo, false); (article1, _, selector) = GetAccessorySelector(_slotTo, false);
table.DrawFrameColumn($"and put {article2} on {article1}"); table.DrawFrameColumn($"and put {article2} on {article1}");
@ -566,8 +522,7 @@ public class ItemSwapTab : IDisposable, ITab
} }
table.NextColumn(); table.NextColumn();
_dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * Im.Style.GlobalScale, _dirty |= selector.Draw("##itemTarget"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
Im.Style.TextHeightWithSpacing);
if (_affectedItems is not { Count: > 1 }) if (_affectedItems is not { Count: > 1 })
return; return;
@ -576,7 +531,7 @@ public class ItemSwapTab : IDisposable, ITab
if (Im.Item.Hovered()) if (Im.Item.Hovered())
{ {
using var tt = Im.Tooltip.Begin(); using var tt = Im.Tooltip.Begin();
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name))) foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name)))
Im.Text(item.Name); Im.Text(item.Name);
} }
} }
@ -610,8 +565,7 @@ public class ItemSwapTab : IDisposable, ITab
return; return;
table.DrawFrameColumn(text1); table.DrawFrameColumn(text1);
table.NextColumn(); table.NextColumn();
_dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item.Name, string.Empty, _dirty |= sourceSelector.Draw("##itemSource"u8, sourceSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
InputWidth * 2 * Im.Style.GlobalScale, Im.Style.TextHeightWithSpacing);
if (type is SwapType.Ring) if (type is SwapType.Ring)
{ {
@ -621,9 +575,7 @@ public class ItemSwapTab : IDisposable, ITab
table.DrawFrameColumn(text2); table.DrawFrameColumn(text2);
table.NextColumn(); table.NextColumn();
_dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item.Name, string.Empty, _dirty |= targetSelector.Draw("##itemTarget"u8, targetSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
InputWidth * 2 * Im.Style.GlobalScale,
Im.Style.TextHeightWithSpacing);
if (type is SwapType.Ring) if (type is SwapType.Ring)
{ {
Im.Line.Same(); Im.Line.Same();
@ -638,7 +590,7 @@ public class ItemSwapTab : IDisposable, ITab
if (Im.Item.Hovered()) if (Im.Item.Hovered())
{ {
using var tt = Im.Tooltip.Begin(); using var tt = Im.Tooltip.Begin();
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name))) foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name)))
Im.Text(item.Name); Im.Text(item.Name);
} }
} }

View file

@ -1,4 +1,3 @@
using Dalamud.Bindings.ImGui;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.MaterialStructs;
@ -87,8 +86,8 @@ public partial class MtrlTab
var rowBIdx = rowAIdx | 1; var rowBIdx = rowAIdx | 1;
var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default;
var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default;
var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Id;
var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Id;
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 = Im.Columns(2, "ColorTable"u8)) using (var columns = Im.Columns(2, "ColorTable"u8))
@ -599,11 +598,10 @@ public partial class MtrlTab
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
Im.Line.Same(subColWidth); Im.Line.Same(subColWidth);
Im.Item.SetNextWidth(scalarSize); Im.Item.SetNextWidth(scalarSize);
_stainService.GudTemplateCombo.CurrentDyeChannel = dye.Channel; if (_stainService.GudTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection,
if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton))
{ {
dye.Template = _stainService.GudTemplateCombo.CurrentSelection.UShort; dye.Template = (ushort) newSelection;
ret = true; ret = true;
} }
@ -613,8 +611,8 @@ public partial class MtrlTab
using var dis = Im.Disabled(!dyePack.HasValue); using var dis = Im.Disabled(!dyePack.HasValue);
if (Im.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.Id,
_stainService.StainCombo2.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Id,
], rowIdx); ], rowIdx);
return ret; return ret;

View file

@ -83,8 +83,8 @@ public partial class MtrlTab
private bool DrawPreviewDye(bool disabled) private bool DrawPreviewDye(bool disabled)
{ {
var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; var (name1, _, dyeId1, _) = _stainService.StainCombo1.CurrentSelection;
var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; var (name2, _, dyeId2, _) = _stainService.StainCombo2.CurrentSelection;
var tt = dyeId1 is 0 && dyeId2 is 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;
@ -103,12 +103,10 @@ public partial class MtrlTab
} }
Im.Line.Same(); Im.Line.Same();
var label = dyeId1 is 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1"; if (_stainService.StainCombo1.Draw(dyeId1 is 0 ? "Preview Dye 1###pd1" : $"{name1} (Preview 1)###pd1"))
if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1))
UpdateColorTablePreview(); UpdateColorTablePreview();
Im.Line.Same(); Im.Line.Same();
label = dyeId2 is 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2"; if (_stainService.StainCombo2.Draw(dyeId2 is 0 ? "Preview Dye 2###pd2" : $"{name2} (Preview 2)###pd2"))
if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2))
UpdateColorTablePreview(); UpdateColorTablePreview();
return false; return false;
} }

View file

@ -1,4 +1,3 @@
using Dalamud.Bindings.ImGui;
using ImSharp; using ImSharp;
using Luna; using Luna;
using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.MaterialStructs;
@ -177,13 +176,13 @@ public partial class MtrlTab
ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false,
m => table[rowIdx].TileTransform = m); m => table[rowIdx].TileTransform = m);
if (dyeTable != null) if (dyeTable is not null)
{ {
Im.Table.NextColumn(); Im.Table.NextColumn();
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, 0, StringU8.Empty, out var newSelection,
+ Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton)) intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
{ {
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; dyeTable[rowIdx].Template = (ushort)newSelection;
ret = true; ret = true;
} }
@ -294,11 +293,10 @@ public partial class MtrlTab
ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f, ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f,
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
Im.Line.SameInner(); Im.Line.SameInner();
_stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel; if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection,
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
+ Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton))
{ {
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; dyeTable[rowIdx].Template = (ushort)newSelection;
ret = true; ret = true;
} }
@ -313,8 +311,8 @@ public partial class MtrlTab
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize) private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize)
{ {
var stain = _stainService.StainCombo1.CurrentSelection.Key; var stain = _stainService.StainCombo1.CurrentSelection.Id;
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
return false; return false;
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2); using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
@ -331,8 +329,8 @@ public partial class MtrlTab
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize)
{ {
var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Id;
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
return false; return false;
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2); using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
@ -341,8 +339,8 @@ public partial class MtrlTab
ret = ret ret = ret
&& Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [
_stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo1.CurrentSelection.Id,
_stainService.StainCombo2.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Id,
], rowIdx); ], rowIdx);
Im.Line.Same(); Im.Line.Same();

View file

@ -226,7 +226,7 @@ public partial class MtrlTab
}; };
if (dyeRow.Channel < StainService.ChannelCount) if (dyeRow.Channel < StainService.ChannelCount)
{ {
StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key; StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Id;
if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes)) if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes))
row.ApplyDye(dyeRow, legacyDyes); row.ApplyDye(dyeRow, legacyDyes);
if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes)) if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes))
@ -260,8 +260,8 @@ public partial class MtrlTab
{ {
ReadOnlySpan<StainId> stainIds = ReadOnlySpan<StainId> stainIds =
[ [
_stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo1.CurrentSelection.Id,
_stainService.StainCombo2.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Id,
]; ];
rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows); rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows);
rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows);

View file

@ -89,17 +89,17 @@ public sealed partial class MtrlTab : IWritable, IDisposable
} }
DrawMaterialLivePreviewRebind(disabled); DrawMaterialLivePreviewRebind(disabled);
Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
var ret = DrawBackFaceAndTransparency(disabled); var ret = DrawBackFaceAndTransparency(disabled);
Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
ret |= DrawShaderSection(disabled); ret |= DrawShaderSection(disabled);
ret |= DrawTextureSection(disabled); ret |= DrawTextureSection(disabled);
ret |= DrawColorTableSection(disabled); ret |= DrawColorTableSection(disabled);
ret |= DrawConstantsSection(disabled); ret |= DrawConstantsSection(disabled);
Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
DrawOtherMaterialDetails(disabled); DrawOtherMaterialDetails(disabled);

View file

@ -41,8 +41,12 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
var height = ColumnHeight; var height = ColumnHeight;
using var clipper = new Im.ListClipper(Count, height); using var clipper = new Im.ListClipper(Count, height);
foreach (var (identifier, value) in clipper.Iterate(Enumerate())) foreach (var (index, (identifier, value)) in clipper.Iterate(Enumerate().Index()))
{
id.Push(index);
DrawEntry(identifier, value); DrawEntry(identifier, value);
id.Pop();
}
} }
public abstract ReadOnlySpan<byte> Label { get; } public abstract ReadOnlySpan<byte> Label { get; }

View file

@ -1,5 +1,4 @@
using ImSharp; using ImSharp;
using OtterGui;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
@ -61,17 +60,19 @@ public partial class ModEditWindow
} }
} }
private sealed class BoneCache(PbdData pbdData) : BasicFilterCache<string>(pbdData.BoneFilter)
{
protected override IEnumerable<string> GetItems()
=> pbdData.SelectedDeformer is null || pbdData.SelectedDeformer.IsEmpty ? [] : pbdData.SelectedDeformer.DeformMatrices.Keys;
}
private void DrawBoneSelector() private void DrawBoneSelector()
{ {
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Get((int)_pbdData.SelectedRaceCode), () => new BoneCache(_pbdData));
using var group = Im.Group(); using var group = Im.Group();
var width = 200 * Im.Style.GlobalScale; var width = 200 * Im.Style.GlobalScale;
using (ImStyleSingle.FrameRounding.Push(0) _pbdData.BoneFilter.DrawFilter("Filter..."u8, new Vector2(width, Im.Style.FrameHeight));
.Push(ImStyleDouble.ItemSpacing, Vector2.Zero)) Im.Cursor.Y -= Im.Style.ItemSpacing.Y;
{
Im.Item.SetNextWidth(width);
Im.Input.Text("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8);
}
using var child = Im.Child.Begin("Bone"u8, using var child = Im.Child.Begin("Bone"u8,
new Vector2(width, Im.ContentRegion.Maximum.Y - Im.Style.FrameHeight - Im.Style.WindowPadding.Y), true); new Vector2(width, Im.ContentRegion.Maximum.Y - Im.Style.FrameHeight - Im.Style.WindowPadding.Y), true);
if (!child) if (!child)
@ -80,23 +81,14 @@ public partial class ModEditWindow
if (_pbdData.SelectedDeformer is null) if (_pbdData.SelectedDeformer is null)
return; return;
if (_pbdData.SelectedDeformer.IsEmpty) if (cache.AllItems.Count is 0)
{
Im.Text("<Empty>"u8); Im.Text("<Empty>"u8);
}
else else
{ foreach (var item in cache)
var height = Im.Style.TextHeightWithSpacing; {
var skips = ImGuiClip.GetNecessarySkips(height); if (Im.Selectable(item, item == _pbdData.SelectedBone))
var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips, _pbdData.SelectedBone = item;
b => b.Contains(_pbdData.BoneFilter), bone }
=>
{
if (Im.Selectable(bone, bone == _pbdData.SelectedBone))
_pbdData.SelectedBone = bone;
});
ImGuiClip.DrawEndDummy(remainder, height);
}
} }
private bool DrawBoneData(PbdTab tab, bool disabled) private bool DrawBoneData(PbdTab tab, bool disabled)
@ -324,8 +316,8 @@ public partial class ModEditWindow
public GenderRace SelectedRaceCode = GenderRace.Unknown; public GenderRace SelectedRaceCode = GenderRace.Unknown;
public RacialDeformer? SelectedDeformer; public RacialDeformer? SelectedDeformer;
public string? SelectedBone; public string? SelectedBone;
public TextFilter BoneFilter = new();
public string NewBoneName = string.Empty; public string NewBoneName = string.Empty;
public string BoneFilter = string.Empty;
public string RaceCodeFilter = string.Empty; public string RaceCodeFilter = string.Empty;
public TransformMatrix? CopiedMatrix; public TransformMatrix? CopiedMatrix;

View file

@ -1,7 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -20,10 +19,7 @@ public partial class ModEditWindow
private int _pathIdx = -1; private int _pathIdx = -1;
private int _folderSkip; private int _folderSkip;
private bool _overviewMode; private bool _overviewMode;
private readonly OverviewTable _overviewTable;
private string _fileOverviewFilter1 = string.Empty;
private string _fileOverviewFilter2 = string.Empty;
private string _fileOverviewFilter3 = string.Empty;
private bool CheckFilter(FileRegistry registry) private bool CheckFilter(FileRegistry registry)
=> _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase); => _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase);
@ -40,9 +36,7 @@ public partial class ModEditWindow
DrawOptionSelectHeader(); DrawOptionSelectHeader();
DrawButtonHeader(); DrawButtonHeader();
if (_overviewMode) if (!_overviewMode)
DrawFileManagementOverview();
else
DrawFileManagementNormal(); DrawFileManagementNormal();
using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true); using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true);
@ -50,65 +44,11 @@ public partial class ModEditWindow
return; return;
if (_overviewMode) if (_overviewMode)
DrawFilesOverviewMode(); _overviewTable.Draw();
else else
DrawFilesNormalMode(); DrawFilesNormalMode();
} }
private void DrawFilesOverviewMode()
{
var height = Im.Style.TextHeightWithSpacing + 2 * Im.Style.CellPadding.Y;
var skips = ImGuiClip.GetNecessarySkips(height);
using var table = Im.Table.Begin("##table"u8, 3, TableFlags.RowBackground | TableFlags.BordersInnerVertical, Im.ContentRegion.Available);
if (!table)
return;
var width = Im.ContentRegion.Available.X / 8;
table.SetupColumn("##file"u8, TableColumnFlags.WidthFixed, width * 3);
table.SetupColumn("##path"u8, TableColumnFlags.WidthFixed, width * 3 + Im.Style.FrameBorderThickness);
table.SetupColumn("##option"u8, TableColumnFlags.WidthFixed, width * 2);
var idx = 0;
var files = _editor.Files.Available.SelectMany(f =>
{
var file = f.RelPath.ToString();
return f.SubModUsage.Count == 0
? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1)
: f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(),
_editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u));
});
void DrawLine((string, string, string, uint) data)
{
using var id = Im.Id.Push(idx++);
if (data.Item4 is not 0)
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
ImEx.CopyOnClickSelectable(data.Item1);
Im.Table.NextColumn();
if (data.Item4 is not 0)
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
ImEx.CopyOnClickSelectable(data.Item2);
Im.Table.NextColumn();
if (data.Item4 is not 0)
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
ImEx.CopyOnClickSelectable(data.Item3);
}
bool Filter((string, string, string, uint) data)
=> data.Item1.Contains(_fileOverviewFilter1, StringComparison.OrdinalIgnoreCase)
&& data.Item2.Contains(_fileOverviewFilter2, StringComparison.OrdinalIgnoreCase)
&& data.Item3.Contains(_fileOverviewFilter3, StringComparison.OrdinalIgnoreCase);
var end = ImGuiClip.FilteredClippedDraw(files, skips, Filter, DrawLine);
ImGuiClip.DrawEndDummy(end, height);
}
private void DrawFilesNormalMode() private void DrawFilesNormalMode()
{ {
@ -154,7 +94,7 @@ public partial class ModEditWindow
{ {
using var tt = Im.Tooltip.Begin(); using var tt = Im.Tooltip.Begin();
using var c = ImGuiColor.Text.PushDefault(); using var c = ImGuiColor.Text.PushDefault();
Im.Text(StringU8.Join((byte) '\n', text)); Im.Text(StringU8.Join((byte)'\n', text));
} }
@ -255,7 +195,7 @@ public partial class ModEditWindow
var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString(); var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString();
var pos = Im.Cursor.X - Im.Style.FrameHeight; var pos = Im.Cursor.X - Im.Style.FrameHeight;
Im.Item.SetNextWidth(-1); Im.Item.SetNextWidth(-1);
if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength:Utf8GamePath.MaxGamePathLength)) if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength: Utf8GamePath.MaxGamePathLength))
{ {
_fileIdx = i; _fileIdx = i;
_pathIdx = j; _pathIdx = j;
@ -341,7 +281,8 @@ public partial class ModEditWindow
if (Im.Button("Add Paths"u8)) if (Im.Button("Add Paths"u8))
_editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip); _editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip);
Im.Tooltip.OnHover("Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8); Im.Tooltip.OnHover(
"Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8);
Im.Line.Same(); Im.Line.Same();
@ -365,7 +306,7 @@ public partial class ModEditWindow
Im.Line.Same(); Im.Line.Same();
var changes = _editor.FileEditor.Changes; var changes = _editor.FileEditor.Changes;
var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8; var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8;
if (ImEx.Button("Apply Changes"u8, Vector2.Zero, tt2, !changes)) if (ImEx.Button("Apply Changes"u8, Vector2.Zero, tt2, !changes))
{ {
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
@ -412,22 +353,4 @@ public partial class ModEditWindow
ImEx.TextRightAligned($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected"); ImEx.TextRightAligned($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected");
} }
private void DrawFileManagementOverview()
{
using var style = ImStyleSingle.FrameRounding.Push(0)
.Push(ImStyleDouble.ItemSpacing, Vector2.Zero)
.Push(ImStyleSingle.FrameBorderThickness, Im.Style.ChildBorderThickness);
var width = Im.ContentRegion.Available.X / 8;
Im.Item.SetNextWidth(width * 3);
Im.Input.Text("##fileFilter"u8, ref _fileOverviewFilter1, "Filter file..."u8);
Im.Line.Same();
Im.Item.SetNextWidth(width * 3);
Im.Input.Text("##pathFilter"u8, ref _fileOverviewFilter2, "Filter path..."u8);
Im.Line.Same();
Im.Item.SetNextWidth(width * 2);
Im.Input.Text("##optionFilter"u8, ref _fileOverviewFilter3, "Filter option..."u8);
}
} }

View file

@ -107,7 +107,7 @@ public partial class ModEditWindow
private void DrawEditHeader(MetaManipulationType type) private void DrawEditHeader(MetaManipulationType type)
{ {
var drawer = _metaDrawers.Get(type); var drawer = _metaDrawers.Get(type);
if (drawer == null) if (drawer is null)
return; return;
var oldPos = Im.Cursor.Y; var oldPos = Im.Cursor.Y;

View file

@ -621,6 +621,7 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
_fileDialog = fileDialog; _fileDialog = fileDialog;
_framework = framework; _framework = framework;
_metaDrawers = metaDrawers; _metaDrawers = metaDrawers;
_overviewTable = new OverviewTable(_editor);
_optionSelect = new OptionSelectCombo(editor, this); _optionSelect = new OptionSelectCombo(editor, this);
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", _materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,

View file

@ -0,0 +1,141 @@
using ImSharp;
using ImSharp.Containers;
using ImSharp.Table;
using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
namespace Penumbra.UI.AdvancedWindow;
public sealed class OverviewTable(ModEditor parent)
: DefaultTable<OverviewTable.OverviewFile>(new StringU8("##overview"u8),
new FileColumn
{
Label = new StringU8("File"u8),
Flags = TableColumnFlags.WidthStretch,
},
new PathColumn
{
Label = new StringU8("Path"u8),
Flags = TableColumnFlags.WidthStretch,
},
new OptionColumn
{
Label = new StringU8("Option"u8),
Flags = TableColumnFlags.WidthStretch,
})
{
public sealed record OverviewFile(
StringPair File,
StringPair Path,
StringPair OptionName,
IModDataContainer? Option,
ColorParameter Color)
{
public ColorParameter Color { get; set; } = Color;
public OverviewFile(FileRegistry file)
: this(new StringPair(file.RelPath.Path.Span), new StringPair("Unused", new StringU8("Unused"u8)), StringPair.Empty, null,
0x40000080)
{ }
public OverviewFile(FileRegistry file, IModDataContainer option, Utf8GamePath gamePath, bool tint)
: this(new StringPair(file.RelPath.Path.Span), new StringPair(gamePath.Path.Span), new StringPair(option.GetFullName()),
option, tint ? 0x40008000 : ColorParameter.Default)
{ }
}
private sealed class FileColumn : TextColumn<OverviewFile>
{
protected override string ComparisonText(in OverviewFile item, int globalIndex)
=> item.File;
protected override StringU8 DisplayText(in OverviewFile item, int globalIndex)
=> item.File;
public override void DrawColumn(in OverviewFile item, int globalIndex)
{
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color);
ImEx.CopyOnClickSelectable(item.File.Utf8);
}
public override float ComputeWidth(IEnumerable<OverviewFile> _)
=> 3 / 8f;
}
private sealed class PathColumn : TextColumn<OverviewFile>
{
protected override string ComparisonText(in OverviewFile item, int globalIndex)
=> item.Path;
protected override StringU8 DisplayText(in OverviewFile item, int globalIndex)
=> item.Path;
public override void DrawColumn(in OverviewFile item, int globalIndex)
{
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color);
ImEx.CopyOnClickSelectable(item.Path.Utf8);
}
public override float ComputeWidth(IEnumerable<OverviewFile> _)
=> 3 / 8f;
}
private sealed class OptionColumn : TextColumn<OverviewFile>
{
protected override string ComparisonText(in OverviewFile item, int globalIndex)
=> item.OptionName;
protected override StringU8 DisplayText(in OverviewFile item, int globalIndex)
=> item.OptionName;
public override void DrawColumn(in OverviewFile item, int globalIndex)
{
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color);
ImEx.CopyOnClickSelectable(item.OptionName.Utf8);
}
public override float ComputeWidth(IEnumerable<OverviewFile> _)
=> 2 / 8f;
}
public override IEnumerable<OverviewFile> GetItems()
=> parent.Files.Available.SelectMany(f =>
{
return f.SubModUsage.Count is 0
? [new OverviewFile(f)]
: f.SubModUsage.Select(s => new OverviewFile(f, s.Item1, s.Item2, parent.Option! == s.Item1 && parent.Mod!.HasOptions));
});
protected override TableCache<OverviewFile> CreateCache()
=> new Cache(this, parent);
private sealed class Cache : TableCache<OverviewFile>
{
private readonly ModEditor _editor;
public Cache(OverviewTable table, ModEditor editor)
: base(table)
{
_editor = editor;
_editor.Files.Available.OnChange += OnChange;
_editor.OptionLoaded += OnOptionLoaded;
}
private void OnOptionLoaded()
{
foreach (var item in UnfilteredItems.Where(i => i.Option is not null))
item.Color = _editor.Option == item.Option && _editor.Mod!.HasOptions ? 0x40008000 : ColorParameter.Default;
}
private void OnChange(in ObservableList<FileRegistry>.ChangeArguments args)
=> Dirty |= IManagedCache.DirtyFlags.Custom;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_editor.Files.Available.OnChange -= OnChange;
_editor.OptionLoaded -= OnOptionLoaded;
}
}
}

View file

@ -46,7 +46,8 @@ public class CollectionSelectHeader(
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
if (!_activeCollections.CurrentCollectionInUse) if (!_activeCollections.CurrentCollectionInUse)
ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 },
Colors.PressEnterWarningBg);
} }
private void DrawTemporaryCheckbox() private void DrawTemporaryCheckbox()
@ -185,6 +186,7 @@ public class CollectionSelectHeader(
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
if (!_activeCollections.CurrentCollectionInUse) if (!_activeCollections.CurrentCollectionInUse)
ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 },
Colors.PressEnterWarningBg);
} }
} }

View file

@ -24,7 +24,7 @@ public class IncognitoService(TutorialService tutorial, Configuration config) :
} }
if (!hold) if (!hold)
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle."); Im.Tooltip.OnHover($"\nHold {config.IncognitoModifier} while clicking to toggle.", HoveredFlags.AllowWhenDisabled, true);
tutorial.OpenTutorial(BasicTutorialSteps.Incognito); tutorial.OpenTutorial(BasicTutorialSteps.Incognito);
} }

View file

@ -0,0 +1,13 @@
using ImSharp;
using Luna;
namespace Penumbra.UI.CollectionTab;
public sealed class CollectionFilter : TextFilterBase<CollectionSelector.Entry>, IUiService
{
public override bool WouldBeVisible(in CollectionSelector.Entry item, int globalIndex)
=> base.WouldBeVisible(in item, globalIndex) || WouldBeVisible(item.AnonymousName.Utf16);
protected override string ToFilterString(in CollectionSelector.Entry item, int globalIndex)
=> item.Collection.Identity.Name;
}

View file

@ -1,4 +1,3 @@
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
@ -14,6 +13,7 @@ using Penumbra.GameData.Enums;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
namespace Penumbra.UI.CollectionTab; namespace Penumbra.UI.CollectionTab;
@ -26,8 +26,9 @@ public sealed class CollectionPanel(
ITargetManager targets, ITargetManager targets,
ModStorage mods, ModStorage mods,
SaveService saveService, SaveService saveService,
IncognitoService incognito) IncognitoService incognito,
: IDisposable Configuration config)
: IDisposable, IPanel
{ {
private readonly CollectionStorage _collections = manager.Storage; private readonly CollectionStorage _collections = manager.Storage;
private readonly ActiveCollections _active = manager.Active; private readonly ActiveCollections _active = manager.Active;
@ -223,13 +224,8 @@ public sealed class CollectionPanel(
{ {
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f)); using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f));
Im.Item.SetNextWidth(width); Im.Item.SetNextWidth(width);
if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName) if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName))
&& newName != collection.Identity.Name) _collections.RenameCollection(collection, newName);
{
collection.Identity.Name = newName;
saveService.QueueSave(new ModCollectionSave(mods, collection));
selector.RestoreCollections();
}
} }
if (_collections.DefaultNamed == collection) if (_collections.DefaultNamed == collection)
@ -770,4 +766,18 @@ public sealed class CollectionPanel(
ret.Add((type, localPre, post, name, border)); ret.Add((type, localPre, post, name, border));
} }
} }
public ReadOnlySpan<byte> Id
=> "cp"u8;
public void Draw()
{
switch (config.Ephemeral.CollectionPanel)
{
case CollectionPanelMode.SimpleAssignment: DrawSimple(); break;
case CollectionPanelMode.IndividualAssignment: DrawIndividualPanel(); break;
case CollectionPanelMode.GroupAssignment: DrawGroupPanel(); break;
case CollectionPanelMode.Details: DrawDetailsPanel(); break;
}
}
} }

View file

@ -1,5 +1,5 @@
using ImSharp; using ImSharp;
using OtterGui; using Luna;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Communication; using Penumbra.Communication;
@ -9,87 +9,20 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab; namespace Penumbra.UI.CollectionTab;
public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposable public sealed class CollectionSelector(ActiveCollections active, TutorialService tutorial, IncognitoService incognito) : IPanel
{ {
private readonly Configuration _config; public ReadOnlySpan<byte> Id
private readonly CommunicatorService _communicator; => "##cs"u8;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
private readonly TutorialService _tutorial;
private readonly IncognitoService _incognito;
private ModCollection? _dragging; private ModCollection? _dragging;
public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, public record struct Entry(ModCollection Collection, StringU8 Name, StringPair AnonymousName)
TutorialService tutorial, IncognitoService incognito)
: base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
{ {
_config = config; public Entry(ModCollection collection)
_communicator = communicator; : this(collection,
_storage = storage; collection.Identity.Name.Length > 0 ? new StringU8(collection.Identity.Name) : new StringU8(collection.Identity.AnonymizedName),
_active = active; new StringPair(collection.Identity.AnonymizedName))
_tutorial = tutorial; { }
_incognito = incognito;
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector);
// Set items.
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty));
// Set selection.
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty));
}
protected override bool OnDelete(int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
// Always return false since we handle the selection update ourselves.
_storage.RemoveCollection(Items[idx]);
return false;
}
protected override bool DeleteButtonEnabled()
=> _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
protected override string DeleteButtonTooltip()
=> _storage.DefaultNamed == Current
? $"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.";
protected override bool OnAdd(string name)
=> _storage.AddCollection(name, null);
protected override bool OnDuplicate(string name, int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.AddCollection(name, Items[idx]);
}
protected override bool Filtered(int idx)
=> !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
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 source = Im.DragDrop.Source();
if (idx == CurrentIdx)
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
if (source)
{
_dragging = Items[idx];
source.SetPayload("Collection"u8);
Im.Text($"Assigning {Name(_dragging)} to...");
}
if (ret)
_active.SetCollection(Items[idx], CollectionType.Current);
return ret;
} }
public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier) public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
@ -98,45 +31,72 @@ public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposabl
if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8)) if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8))
return; return;
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); active.SetCollection(_dragging, type, active.Individuals.GetGroup(identifier));
_dragging = null; _dragging = null;
} }
public void Dispose() public void Draw()
{ {
_communicator.CollectionChange.Unsubscribe(OnCollectionChange); Im.Cursor.Y += Im.Style.FramePadding.Y;
} var cache = CacheManager.Instance.GetOrCreateCache<Cache>(Im.Id.Current);
using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
private string Name(ModCollection collection) foreach (var item in cache)
=> _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name;
public void RestoreCollections()
{
Items.Clear();
Items.Add(_storage.DefaultNamed);
foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
Items.Add(c);
SetFilterDirty();
SetCurrent(_active.Current);
}
private void OnCollectionChange(in CollectionChange.Arguments arguments)
{
switch (arguments.Type)
{ {
case CollectionType.Temporary: return; Im.Cursor.X += Im.Style.FramePadding.X;
case CollectionType.Current: var ret = Im.Selectable(incognito.IncognitoMode ? item.AnonymousName : item.Name, active.Current == item.Collection);
if (arguments.NewCollection is not null) using var source = Im.DragDrop.Source();
SetCurrent(arguments.NewCollection);
SetFilterDirty(); if (active.Current == item.Collection)
return; tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
case CollectionType.Inactive:
RestoreCollections(); if (source)
SetFilterDirty(); {
return; _dragging = item.Collection;
default: source.SetPayload("Collection"u8);
SetFilterDirty(); Im.Text($"Assigning {(incognito.IncognitoMode ? item.AnonymousName : item.Name)} to...");
return; }
if (ret)
active.SetCollection(item.Collection, CollectionType.Current);
}
}
public sealed class Cache : BasicFilterCache<Entry>, IService
{
private readonly CollectionStorage _collections;
private readonly CommunicatorService _communicator;
public Cache(CollectionFilter filter, CollectionStorage collections, CommunicatorService communicator)
: base(filter)
{
_collections = collections;
_communicator = communicator;
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelectorCache);
_communicator.CollectionRename.Subscribe(OnCollectionRename, CollectionRename.Priority.CollectionSelectorCache);
}
private void OnCollectionRename(in CollectionRename.Arguments arguments)
=> Dirty |= IManagedCache.DirtyFlags.Custom;
private void OnCollectionChange(in CollectionChange.Arguments arguments)
{
if (arguments.Type is CollectionType.Inactive)
Dirty |= IManagedCache.DirtyFlags.Custom;
}
protected override IEnumerable<Entry> GetItems()
{
yield return new Entry(_collections.DefaultNamed);
foreach (var collection in _collections.Where(c => c != _collections.DefaultNamed).OrderBy(c => c.Identity.Name))
yield return new Entry(collection);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
_communicator.CollectionRename.Unsubscribe(OnCollectionRename);
} }
} }
} }

View file

@ -0,0 +1,52 @@
using ImSharp;
using Luna;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;
using Penumbra.UI.ModsTab.Groups;
namespace Penumbra.UI;
public sealed class SingleGroupCombo : FilterComboBase<SingleGroupCombo.Test>, IUiService
{
private class OptionFilter : Utf8FilterBase<Test>
{
protected override ReadOnlySpan<byte> ToFilterString(in Test item, int globalIndex)
=> item.Name;
}
public SingleGroupCombo()
=> Filter = new OptionFilter();
public readonly record struct Test(int OptionIndex, StringU8 Name, StringU8 Description);
private readonly WeakReference<SingleModGroup> _group = new(null!);
private Setting _currentOption;
public void Draw(ModGroupDrawer parent, SingleModGroup group, int groupIndex, Setting currentOption)
{
_currentOption = currentOption;
_group.SetTarget(group);
if (base.Draw(group.Name, group.OptionData[currentOption.AsIndex].Name, StringU8.Empty, UiHelpers.InputTextWidth.X * 3 / 4,
out var newOption))
parent.SetModSetting(group, groupIndex, Setting.Single(newOption.OptionIndex));
}
protected override IEnumerable<Test> GetItems()
=> _group.TryGetTarget(out var target)
? target.OptionData.Select(o => new Test(o.GetIndex(), new StringU8(o.Name), new StringU8(o.Description)))
: [];
protected override float ItemHeight
=> Im.Style.TextHeightWithSpacing;
protected override bool DrawItem(in Test item, int globalIndex, bool selected)
{
var ret = Im.Selectable(item.Name, selected);
if (item.Description.Length > 0)
LunaStyle.DrawHelpMarker(item.Description, treatAsHovered: Im.Item.Hovered());
return ret;
}
protected override bool IsSelected(Test item, int globalIndex)
=> item.OptionIndex == _currentOption.AsIndex;
}

View file

@ -1,118 +0,0 @@
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;
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

@ -3,7 +3,6 @@ using ImSharp;
using Luna; using Luna;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType; using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra.UI.MainWindow; namespace Penumbra.UI.MainWindow;

View file

@ -1,65 +1,22 @@
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui.Widgets;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab.Groups;
public sealed class ModGroupDrawer : IUiService public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager, SingleGroupCombo combo)
: IUiService
{ {
private readonly List<(IModGroup, int)> _blockGroupCache = []; private readonly List<(IModGroup, int)> _blockGroupCache = [];
private bool _temporary; private bool _temporary;
private bool _locked; private bool _locked;
private TemporaryModSettings? _tempSettings; private TemporaryModSettings? _tempSettings;
private ModSettings? _settings; private ModSettings? _settings;
private readonly SingleGroupCombo _combo;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
public ModGroupDrawer(Configuration config, CollectionManager collectionManager)
{
_config = config;
_collectionManager = collectionManager;
_combo = new SingleGroupCombo(this);
}
private sealed class SingleGroupCombo(ModGroupDrawer parent)
: FilterComboCache<IModOption>(() => _group!.Options, MouseWheelType.Control, Penumbra.Log)
{
private static IModGroup? _group;
private static int _groupIdx;
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var option = _group!.Options[globalIdx];
var ret = Im.Selectable(option.Name, globalIdx == CurrentSelectionIdx);
if (option.Description.Length > 0)
LunaStyle.DrawHelpMarker(option.Description, treatAsHovered: Im.Item.Hovered());
return ret;
}
protected override string ToString(IModOption obj)
=> obj.Name;
public void Draw(IModGroup group, int groupIndex, int currentOption)
{
_group = group;
_groupIdx = groupIndex;
CurrentSelectionIdx = currentOption;
CurrentSelection = _group.Options[CurrentSelectionIdx];
if (Draw(string.Empty, CurrentSelection.Name, string.Empty, ref CurrentSelectionIdx, UiHelpers.InputTextWidth.X * 3 / 4,
Im.Style.TextHeightWithSpacing))
parent.SetModSetting(_group, _groupIdx, Setting.Single(CurrentSelectionIdx));
}
}
public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings)
{ {
@ -79,7 +36,7 @@ public sealed class ModGroupDrawer : IUiService
switch (group.Behaviour) switch (group.Behaviour)
{ {
case GroupDrawBehaviour.SingleSelection when group.Options.Count <= _config.SingleGroupRadioMax: case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax:
case GroupDrawBehaviour.MultiSelection: case GroupDrawBehaviour.MultiSelection:
_blockGroupCache.Add((group, idx)); _blockGroupCache.Add((group, idx));
break; break;
@ -120,9 +77,8 @@ public sealed class ModGroupDrawer : IUiService
private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting)
{ {
using var id = Im.Id.Push(groupIdx); using var id = Im.Id.Push(groupIdx);
var selectedOption = setting.AsIndex;
using var disabled = Im.Disabled(_locked); using var disabled = Im.Disabled(_locked);
_combo.Draw(group, groupIdx, selectedOption); combo.Draw(this, (SingleModGroup)group, groupIdx, setting);
if (group.Description.Length > 0) if (group.Description.Length > 0)
{ {
LunaStyle.DrawHelpMarkerLabel(group.Name, group.Description); LunaStyle.DrawHelpMarkerLabel(group.Name, group.Description);
@ -227,7 +183,7 @@ public sealed class ModGroupDrawer : IUiService
private void DrawCollapseHandling(IReadOnlyList<IModOption> options, float minWidth, Action draw) private void DrawCollapseHandling(IReadOnlyList<IModOption> options, float minWidth, Action draw)
{ {
if (options.Count <= _config.OptionGroupCollapsibleMin) if (options.Count <= config.OptionGroupCollapsibleMin)
{ {
draw(); draw();
} }
@ -272,21 +228,21 @@ public sealed class ModGroupDrawer : IUiService
} }
private ModCollection Current private ModCollection Current
=> _collectionManager.Active.Current; => collectionManager.Active.Current;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void SetModSetting(IModGroup group, int groupIdx, Setting setting) internal void SetModSetting(IModGroup group, int groupIdx, Setting setting)
{ {
if (_temporary || _config.DefaultTemporaryMode) if (_temporary || config.DefaultTemporaryMode)
{ {
_tempSettings ??= new TemporaryModSettings(group.Mod, _settings); _tempSettings ??= new TemporaryModSettings(group.Mod, _settings);
_tempSettings!.ForceInherit = false; _tempSettings!.ForceInherit = false;
_tempSettings!.Settings[groupIdx] = setting; _tempSettings!.Settings[groupIdx] = setting;
_collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings);
} }
else else
{ {
_collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting);
} }
} }
} }

View file

@ -1,7 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using ImSharp; using ImSharp;
using Luna; using Luna;
using OtterGui.Widgets;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;

View file

@ -120,7 +120,15 @@ public sealed class ModFileSystemCache(ModFileSystemDrawer parent)
} }
public override void Update() public override void Update()
{ } {
if (ColorsDirty)
{
CollapsedFolderColor = ColorId.FolderCollapsed.Value().ToVector();
ExpandedFolderColor = ColorId.FolderExpanded.Value().ToVector();
LineColor = ColorId.FolderLine.Value().ToVector();
Dirty &= ~IManagedCache.DirtyFlags.Colors;
}
}
protected override ModData ConvertNode(in IFileSystemNode node) protected override ModData ConvertNode(in IFileSystemNode node)
=> new((IFileSystemData<Mod>)node); => new((IFileSystemData<Mod>)node);

View file

@ -0,0 +1,90 @@
using ImSharp;
using Luna;
using Penumbra.Collections.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public sealed class CollectionButtonFooter : ButtonFooter
{
public CollectionButtonFooter(CollectionManager collectionManager, CommunicatorService communicator, Configuration configuration,
TutorialService tutorial, IncognitoService incognito)
{
Buttons.AddButton(new AddButton(collectionManager.Storage), 100);
Buttons.AddButton(new DuplicateButton(collectionManager.Storage, collectionManager.Active), 50);
Buttons.AddButton(new DeleteButton(collectionManager.Storage, collectionManager.Active, configuration), 0);
}
public sealed class AddButton(CollectionStorage collections) : BaseIconButton<AwesomeIcon>
{
public override AwesomeIcon Icon
=> LunaStyle.AddObjectIcon;
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text("Add a new, empty collection."u8);
public override void OnClick()
=> Im.Popup.Open("NewCollection"u8);
protected override void PostDraw()
{
if (!InputPopup.OpenName("NewCollection"u8, out var newCollectionName))
return;
collections.AddCollection(newCollectionName, null);
}
}
public sealed class DeleteButton(CollectionStorage collections, ActiveCollections active, Configuration config)
: BaseIconButton<AwesomeIcon>
{
public override AwesomeIcon Icon
=> LunaStyle.DeleteIcon;
public override bool HasTooltip
=> true;
public override bool Enabled
=> collections.DefaultNamed != active.Current
&& config.DeleteModModifier.IsActive();
public override void DrawTooltip()
{
Im.Text("Delete the current collection."u8);
if (collections.DefaultNamed == active.Current)
Im.Text("The default collection cannot be deleted."u8);
else if (!config.DeleteModModifier.IsActive())
Im.Text($"Hold {config.DeleteModModifier} to delete the current collection.");
}
public override void OnClick()
=> collections.RemoveCollection(active.Current);
}
public sealed class DuplicateButton(CollectionStorage collections, ActiveCollections active) : BaseIconButton<AwesomeIcon>
{
public override AwesomeIcon Icon
=> LunaStyle.DuplicateIcon;
public override bool HasTooltip
=> true;
public override void DrawTooltip()
=> Im.Text("Duplicate the currently selected collection to a new one."u8);
public override void OnClick()
=> Im.Popup.Open("DuplicateCollection"u8);
protected override void PostDraw()
{
if (!InputPopup.OpenName("DuplicateCollection"u8, out var newCollectionName))
return;
collections.AddCollection(newCollectionName, active.Current);
}
}
}

View file

@ -0,0 +1,66 @@
using ImSharp;
using Luna;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public enum CollectionPanelMode
{
SimpleAssignment,
IndividualAssignment,
GroupAssignment,
Details,
};
public sealed class CollectionModeHeader(Configuration config, TutorialService tutorial, IncognitoService incognito) : IHeader
{
public bool Collapsed
=> false;
private CollectionPanelMode Mode
{
get => config.Ephemeral.CollectionPanel;
set
{
config.Ephemeral.CollectionPanel = value;
config.Ephemeral.Save();
}
}
public void Draw(Vector2 size)
{
var withSpacing = Im.Style.FrameHeightWithSpacing;
var buttonSize = new Vector2((Im.ContentRegion.Available.X - withSpacing) / 4f, Im.Style.FrameHeight);
var tabSelectedColor = Im.Style[ImGuiColor.TabSelected];
using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is CollectionPanelMode.SimpleAssignment);
if (Im.Button("Simple Assignments"u8, buttonSize))
Mode = CollectionPanelMode.SimpleAssignment;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments);
Im.Line.NoSpacing();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.IndividualAssignment);
if (Im.Button("Individual Assignments"u8, buttonSize))
Mode = CollectionPanelMode.IndividualAssignment;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments);
Im.Line.NoSpacing();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.GroupAssignment);
if (Im.Button("Group Assignments"u8, buttonSize))
Mode = CollectionPanelMode.GroupAssignment;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments);
Im.Line.NoSpacing();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.Details);
if (Im.Button("Collection Details"u8, buttonSize))
Mode = CollectionPanelMode.Details;
color.Pop();
tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails);
Im.Line.NoSpacing();
incognito.DrawToggle(withSpacing);
}
}

View file

@ -1,151 +1,42 @@
using Dalamud.Plugin.Services; using ImSharp;
using Dalamud.Plugin; using Luna;
using ImSharp; using Penumbra.Api.Enums;
using Luna; using Penumbra.UI.Classes;
using Penumbra.Api.Enums; using Penumbra.UI.CollectionTab;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors; namespace Penumbra.UI.Tabs;
using Penumbra.Mods.Manager;
using Penumbra.Services; public sealed class CollectionsTab : TwoPanelLayout, ITab<TabType>
using Penumbra.UI.Classes; {
using Penumbra.UI.CollectionTab; private readonly TutorialService _tutorial;
namespace Penumbra.UI.Tabs; public TabType Identifier
=> TabType.Collections;
public sealed class CollectionsTab : ITab<TabType>, IDisposable
{ public CollectionsTab(TutorialService tutorial, CollectionButtonFooter leftFooter, CollectionSelector leftPanel, CollectionFilter filter,
private readonly EphemeralConfig _config; CollectionModeHeader rightHeader, CollectionPanel rightPanel)
private readonly CollectionSelector _selector; {
private readonly CollectionPanel _panel; LeftHeader = new FilterHeader<CollectionSelector.Entry>(filter, new StringU8("Filter..."u8));
private readonly TutorialService _tutorial; LeftPanel = leftPanel;
private readonly IncognitoService _incognito; LeftFooter = leftFooter;
RightHeader = rightHeader;
public enum PanelMode RightPanel = rightPanel;
{ RightFooter = NopHeaderFooter.Instance;
SimpleAssignment, _tutorial = tutorial;
IndividualAssignment, }
GroupAssignment,
Details, public override ReadOnlySpan<byte> Label
}; => "Collections"u8;
public PanelMode Mode protected override void DrawLeftGroup()
{ {
get => _config.CollectionPanel; base.DrawLeftGroup();
set _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
{ }
_config.CollectionPanel = value;
_config.Save(); public void DrawContent()
} => Draw();
}
public void PostTabButton()
public TabType Identifier => _tutorial.OpenTutorial(BasicTutorialSteps.Collections);
=> TabType.Collections; }
public CollectionsTab(IDalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito,
CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService)
{
_config = configuration.Ephemeral;
_tutorial = tutorial;
_incognito = incognito;
_selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito);
_panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, saveService, incognito);
}
public void Dispose()
{
_selector.Dispose();
_panel.Dispose();
}
public ReadOnlySpan<byte> Label
=> "Collections"u8;
public void DrawContent()
{
var width = Im.Font.CalculateSize("nnnnnnnnnnnnnnnnnnnnnnnnnn"u8).X;
using (Im.Group())
{
_selector.Draw(width);
}
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
Im.Line.Same();
using (Im.Group())
{
DrawHeaderLine();
DrawPanel();
}
}
public void DrawHeader()
{
_tutorial.OpenTutorial(BasicTutorialSteps.Collections);
}
private void DrawHeaderLine()
{
var withSpacing = Im.Style.FrameHeightWithSpacing;
using var style = ImStyleSingle.FrameRounding.Push(0).Push(ImStyleDouble.ItemSpacing, Vector2.Zero);
var buttonSize = new Vector2((Im.ContentRegion.Available.X - withSpacing) / 4f, Im.Style.FrameHeight);
using var _ = Im.Group();
var tabSelectedColor = Im.Style[ImGuiColor.TabSelected];
using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is PanelMode.SimpleAssignment);
if (Im.Button("Simple Assignments"u8, buttonSize))
Mode = PanelMode.SimpleAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments);
Im.Line.Same();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.IndividualAssignment);
if (Im.Button("Individual Assignments"u8, buttonSize))
Mode = PanelMode.IndividualAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments);
Im.Line.Same();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.GroupAssignment);
if (Im.Button("Group Assignments"u8, buttonSize))
Mode = PanelMode.GroupAssignment;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments);
Im.Line.Same();
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.Details);
if (Im.Button("Collection Details"u8, buttonSize))
Mode = PanelMode.Details;
color.Pop();
_tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails);
Im.Line.Same();
_incognito.DrawToggle(withSpacing);
}
private void DrawPanel()
{
using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero);
using var child = Im.Child.Begin("##CollectionSettings"U8, Im.ContentRegion.Available with { Y = 0 }, true);
if (!child)
return;
style.Pop();
switch (Mode)
{
case PanelMode.SimpleAssignment:
_panel.DrawSimple();
break;
case PanelMode.IndividualAssignment:
_panel.DrawIndividualPanel();
break;
case PanelMode.GroupAssignment:
_panel.DrawGroupPanel();
break;
case PanelMode.Details:
_panel.DrawDetailsPanel();
break;
}
style.Push(ImStyleDouble.ItemSpacing, Vector2.Zero);
}
}

View file

@ -0,0 +1,51 @@
using ImSharp;
using Luna;
using Penumbra.Interop.Services;
using Penumbra.String;
namespace Penumbra.UI.Tabs.Debug;
public sealed class ActionTmbListDrawer(SchedulerResourceManagementService service) : IUiService
{
public readonly SchedulerResourceManagementService Service = service;
public readonly IFilter<TmbEntry> KeyFilter = new TmbKeyFilter();
public sealed class Cache(ActionTmbListDrawer parent) : BasicFilterCache<TmbEntry>(parent.KeyFilter)
{
protected override IEnumerable<TmbEntry> GetItems()
=> parent.Service.ActionTmbs.OrderBy(t => t.Value).Select(k => new TmbEntry(k.Key, k.Value));
}
public readonly struct TmbEntry(CiByteString key, uint value)
{
public readonly StringPair Key = new(key.ToString());
public readonly StringU8 Value = new($"{value}");
public void Draw()
{
Im.Table.DrawColumn(Value);
Im.Table.DrawColumn(Key.Utf8);
}
}
public sealed class TmbKeyFilter : RegexFilterBase<TmbEntry>
{
protected override string ToFilterString(in TmbEntry item, int globalIndex)
=> item.Key.Utf16;
}
public void Draw()
{
KeyFilter.DrawFilter("Key"u8, Im.ContentRegion.Available);
using var table = Im.Table.Begin("##table"u8, 2,
TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.ScrollX | TableFlags.SizingFixedFit,
Im.ContentRegion.Available with { Y = 12 * Im.Style.TextHeightWithSpacing });
if (!table)
return;
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(this));
using var clip = new Im.ListClipper(cache.Count, Im.Style.TextHeightWithSpacing);
foreach (var tmb in clip.Iterate(cache))
tmb.Draw();
}
}

View file

@ -1,12 +1,10 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Group;
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 FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using ImSharp; using ImSharp;
using Luna; using Luna;
@ -15,7 +13,6 @@ using Penumbra.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Interop; using Penumbra.GameData.Interop;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
@ -28,7 +25,6 @@ using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using ImGuiClip = OtterGui.ImGuiClip;
using Penumbra.Api.IpcTester; using Penumbra.Api.IpcTester;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.PostProcessing;
@ -65,54 +61,54 @@ public class Diagnostics(ServiceManager provider) : IUiService
public sealed class DebugTab : Window, ITab<TabType> public sealed class DebugTab : Window, ITab<TabType>
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly ValidityChecker _validityChecker; private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi; private readonly HttpApi _httpApi;
private readonly ActorManager _actors; private readonly ActorManager _actors;
private readonly StainService _stains; private readonly StainService _stains;
private readonly GlobalVariablesDrawer _globalVariablesDrawer; private readonly GlobalVariablesDrawer _globalVariablesDrawer;
private readonly ResourceManagerService _resourceManager; private readonly ResourceManagerService _resourceManager;
private readonly ResourceLoader _resourceLoader; private readonly ResourceLoader _resourceLoader;
private readonly CollectionResolver _collectionResolver; private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState; private readonly DrawObjectState _drawObjectState;
private readonly PathState _pathState; private readonly PathState _pathState;
private readonly SubfileHelper _subfileHelper; private readonly SubfileHelper _subfileHelper;
private readonly IdentifiedCollectionCache _identifiedCollectionCache; private readonly IdentifiedCollectionCache _identifiedCollectionCache;
private readonly CutsceneService _cutsceneService; private readonly CutsceneService _cutsceneService;
private readonly ModImportManager _modImporter; private readonly ModImportManager _modImporter;
private readonly ImportPopup _importPopup; private readonly ImportPopup _importPopup;
private readonly FrameworkManager _framework; private readonly FrameworkManager _framework;
private readonly TextureManager _textureManager; private readonly TextureManager _textureManager;
private readonly ShaderReplacementFixer _shaderReplacementFixer; private readonly ShaderReplacementFixer _shaderReplacementFixer;
private readonly RedrawService _redraws; private readonly RedrawService _redraws;
private readonly DictEmote _emotes; private readonly EmoteListDrawer _emotes;
private readonly Diagnostics _diagnostics; private readonly Diagnostics _diagnostics;
private readonly ObjectManager _objects; private readonly ObjectManager _objects;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IpcTester _ipcTester; private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel; private readonly CrashHandlerPanel _crashHandlerPanel;
private readonly TexHeaderDrawer _texHeaderDrawer; private readonly TexHeaderDrawer _texHeaderDrawer;
private readonly HookOverrideDrawer _hookOverrides; private readonly HookOverrideDrawer _hookOverrides;
private readonly RsfService _rsfService; private readonly RsfService _rsfService;
private readonly SchedulerResourceManagementService _schedulerService; private readonly ActionTmbListDrawer _actionTmbs;
private readonly ObjectIdentification _objectIdentification; private readonly ObjectIdentification _objectIdentification;
private readonly RenderTargetDrawer _renderTargetDrawer; private readonly RenderTargetDrawer _renderTargetDrawer;
private readonly ModMigratorDebug _modMigratorDebug; private readonly ModMigratorDebug _modMigratorDebug;
private readonly ShapeInspector _shapeInspector; private readonly ShapeInspector _shapeInspector;
private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer; private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer;
private readonly DragDropManager _dragDropManager; private readonly DragDropManager _dragDropManager;
public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager, public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver, ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, EmoteListDrawer emotes,
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer,
SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, ActionTmbListDrawer actionTmbs, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer,
ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer, ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer,
DragDropManager dragDropManager) DragDropManager dragDropManager)
: base("Penumbra Debug Window", WindowFlags.NoCollapse) : base("Penumbra Debug Window", WindowFlags.NoCollapse)
@ -152,7 +148,7 @@ public sealed class DebugTab : Window, ITab<TabType>
_hookOverrides = hookOverrides; _hookOverrides = hookOverrides;
_rsfService = rsfService; _rsfService = rsfService;
_globalVariablesDrawer = globalVariablesDrawer; _globalVariablesDrawer = globalVariablesDrawer;
_schedulerService = schedulerService; _actionTmbs = actionTmbs;
_objectIdentification = objectIdentification; _objectIdentification = objectIdentification;
_renderTargetDrawer = renderTargetDrawer; _renderTargetDrawer = renderTargetDrawer;
_modMigratorDebug = modMigratorDebug; _modMigratorDebug = modMigratorDebug;
@ -205,7 +201,7 @@ public sealed class DebugTab : Window, ITab<TabType>
_globalVariablesDrawer.Draw(); _globalVariablesDrawer.Draw();
DrawCloudApi(); DrawCloudApi();
DrawDebugTabIpc(); DrawDebugTabIpc();
if(Im.Tree.Header("Drag & Drop Manager"u8)) if (Im.Tree.Header("Drag & Drop Manager"u8))
_dragDropManager.DrawDebugInfo(); _dragDropManager.DrawDebugInfo();
} }
@ -742,7 +738,7 @@ public sealed class DebugTab : Window, ITab<TabType>
{ {
using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit); using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit);
if (table) if (table)
foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key)) foreach (var (id, name) in _actionTmbs.Service.ListedTmbs.OrderBy(kvp => kvp.Key))
table.DrawDataPair($"{id:D6}", name.Span); table.DrawDataPair($"{id:D6}", name.Span);
} }
} }
@ -814,11 +810,6 @@ public sealed class DebugTab : Window, ITab<TabType>
Im.Selectable(item.Key); Im.Selectable(item.Key);
} }
private string _emoteSearchFile = string.Empty;
private string _emoteSearchName = string.Empty;
private AtchFile? _atchFile; private AtchFile? _atchFile;
private void DrawAtch() private void DrawAtch()
@ -842,57 +833,19 @@ public sealed class DebugTab : Window, ITab<TabType>
AtchDrawer.Draw(_atchFile); AtchDrawer.Draw(_atchFile);
} }
private void DrawEmotes() private void DrawEmotes()
{ {
using var mainTree = Im.Tree.Node("Emotes"u8); using var mainTree = Im.Tree.Node("Emotes"u8);
if (!mainTree) if (mainTree)
return; _emotes.Draw();
Im.Input.Text("File Name"u8, ref _emoteSearchFile);
Im.Input.Text("Emote Name"u8, ref _emoteSearchName);
using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit,
new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing));
if (!table)
return;
var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing);
var dummy = ImGuiClip.FilteredClippedDraw(_emotes, skips,
p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase)
&& (_emoteSearchName.Length == 0
|| p.Value.Any(s => s.Name.ToDalamudString().TextValue.Contains(_emoteSearchName, StringComparison.OrdinalIgnoreCase))),
p =>
{
Im.Table.DrawColumn(p.Key);
Im.Table.DrawColumn(StringU8.Join(", "u8, p.Value.Select(v => v.Name.ToDalamudString().TextValue)));
});
ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing);
} }
private string _tmbKeyFilter = string.Empty;
private CiByteString _tmbKeyFilterU8 = CiByteString.Empty;
private void DrawActionTmbs() private void DrawActionTmbs()
{ {
using var mainTree = Im.Tree.Node("Action TMBs"u8); using var mainTree = Im.Tree.Node("Action TMBs"u8);
if (!mainTree) if (mainTree)
return; _actionTmbs.Draw();
if (Im.Input.Text("Key"u8, ref _tmbKeyFilter))
_tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty;
using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit,
new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing));
if (!table)
return;
var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing);
var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips,
kvp => kvp.Key.Contains(_tmbKeyFilterU8),
p =>
{
Im.Table.DrawColumn($"{p.Value}");
Im.Table.DrawColumn(p.Key.Span);
});
ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing);
} }
private void DrawStainTemplates() private void DrawStainTemplates()

View file

@ -0,0 +1,79 @@
using ImSharp;
using Lumina.Excel.Sheets;
using Luna;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
namespace Penumbra.UI.Tabs.Debug;
public sealed class EmoteListDrawer(DictEmote emotes) : IUiService
{
public readonly DictEmote Emotes = emotes;
public readonly IFilter<EmoteEntry> FileFilter = new EmoteFileFilter();
public readonly IFilter<EmoteEntry> NameFilter = new EmoteNameFilter();
public sealed class Cache(EmoteListDrawer parent)
: BasicFilterCache<EmoteEntry>(new PairFilter<EmoteEntry>(parent.FileFilter, parent.NameFilter))
{
protected override IEnumerable<EmoteEntry> GetItems()
=> parent.Emotes.Value.Select(kvp => new EmoteEntry(kvp.Key, kvp.Value));
}
public sealed class EmoteFileFilter : RegexFilterBase<EmoteEntry>
{
protected override string ToFilterString(in EmoteEntry item, int globalIndex)
=> item.File.Utf16;
}
public sealed class EmoteNameFilter : RegexFilterBase<EmoteEntry>
{
public override bool WouldBeVisible(in EmoteEntry item, int globalIndex)
=> Text.Length is 0 || item.Emotes.Any(e => WouldBeVisible(e.Utf16));
protected override string ToFilterString(in EmoteEntry item, int globalIndex)
=> string.Empty;
}
public readonly struct EmoteEntry
{
public readonly StringPair File;
public readonly List<StringPair> Emotes;
public EmoteEntry(string key, IReadOnlyList<Emote> emotes)
{
File = new StringPair(key);
Emotes = emotes.Select(e => new StringPair(e.Name.ExtractTextExtended())).ToList();
}
public void Draw()
{
Im.Table.DrawColumn(File.Utf8);
if (Emotes.Count > 0)
Im.Table.DrawColumn(Emotes[0].Utf8);
foreach (var emote in Emotes.Skip(1))
{
Im.Line.NoSpacing();
Im.Text(", "u8);
Im.Line.NoSpacing();
Im.Text(emote.Utf16);
}
}
}
public void Draw()
{
FileFilter.DrawFilter("File Name"u8, Im.ContentRegion.Available);
NameFilter.DrawFilter("Emote Name"u8, Im.ContentRegion.Available);
using var table = Im.Table.Begin("##table"u8, 2,
TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.ScrollX | TableFlags.SizingFixedFit,
Im.ContentRegion.Available with { Y = 12 * Im.Style.TextHeightWithSpacing });
if (!table)
return;
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(this));
using var clip = new Im.ListClipper(cache.Count, Im.Style.TextHeightWithSpacing);
foreach (var emote in clip.Iterate(cache))
emote.Draw();
}
}