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 Luna;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
{
public static readonly LocalCollectionId Zero = new(0);
public static LocalCollectionId operator +(LocalCollectionId left, int right)
=> new(left.Id + right);
}
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
public ModCollection Create(string name, int index, ModCollection? duplicate)
{
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> inheritances)
{
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
{
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public void Delete(ModCollection collection)
=> _collectionsByLocal.Remove(collection.Identity.LocalId);
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections =
[
ModCollection.Empty,
];
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
private readonly Dictionary<LocalCollectionId, ModCollection>
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
public readonly ModCollection DefaultNamed;
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
/// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _collections.Count;
public ModCollection this[int index]
=> _collections[index];
/// <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)
{
if (name.Length != 0)
return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
collection = ModCollection.Empty;
return true;
}
/// <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)
{
if (id != Guid.Empty)
return _collections.FindFirst(c => c.Identity.Id == id, out collection);
collection = ModCollection.Empty;
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>
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
{
if (Guid.TryParse(identifier, out var guid))
return ById(guid, 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>
public ModCollection ByLocalId(LocalCollectionId localId)
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
_saveService = saveService;
_modStorage = modStorage;
_communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage);
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage);
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage);
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage);
ReadCollections(out DefaultNamed);
}
public void Dispose()
{
_communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted);
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
}
/// <summary>
/// Add a new collection of the given name.
/// 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.
/// Returns true if the collection was successfully created and fires a Inactive event.
/// Also sets the current collection to the new collection afterwards.
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
if (name.Length == 0)
return false;
var newCollection = Create(name, _collections.Count, duplicate);
_collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false);
_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>
public bool RemoveCollection(ModCollection collection)
{
if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
{
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
return false;
}
if (collection.Identity.Index == DefaultNamed.Identity.Index)
{
Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false);
return false;
}
Delete(collection);
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
_collections.RemoveAt(collection.Identity.Index);
// Update indices.
for (var i = collection.Identity.Index; i < Count; ++i)
_collections[i].Identity.Index = i;
_collectionsByLocal.Remove(collection.Identity.LocalId);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty));
return true;
}
/// <summary> Remove all settings for not currently-installed mods from the given collection. </summary>
public int CleanUnavailableSettings(ModCollection collection)
{
var count = collection.Settings.Unused.Count;
if (count > 0)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear();
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
return count;
}
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
public void CleanUnavailableSetting(ModCollection collection, string? setting)
{
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
/// <summary>
/// Read all collection files in the Collection Directory.
/// Ensure that the default named collection exists, and apply inheritances afterward.
/// Duplicate collection files are not deleted, just not added here.
/// </summary>
private void ReadCollections(out ModCollection defaultNamedCollection)
{
Penumbra.Log.Debug("[Collections] Reading saved collections...");
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))
continue;
if (id == Guid.Empty)
{
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
continue;
}
if (ById(id, out _))
{
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
NotificationType.Warning);
continue;
}
var collection = CreateFromData(id, name, version, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
try
{
if (version >= 2)
{
try
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
NotificationType.Warning);
}
catch (Exception ex)
{
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
NotificationType.Warning);
}
}
else
{
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
try
{
File.Move(file.FullName, file.FullName + ".bak", true);
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file.");
}
catch (Exception ex)
{
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}");
}
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
NotificationType.Error);
}
_collections.Add(collection);
}
defaultNamedCollection = SetDefaultNamedCollection();
Penumbra.Log.Debug($"[Collections] Found {Count} saved collections.");
}
/// <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.
/// </summary>
private ModCollection SetDefaultNamedCollection()
{
if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
return collection;
if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
return _collections[^1];
Penumbra.Messager.NotificationMessage(
$"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> Move all settings in all collections to unused settings. </summary>
private void OnModDiscoveryStarted()
{
foreach (var collection in this)
collection.Settings.PrepareModDiscovery(_modStorage);
}
/// <summary> Restore all settings in all collections to mods. </summary>
private void OnModDiscoveryFinished()
{
// Re-apply all mod settings.
foreach (var collection in this)
collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
}
/// <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)
{
switch (arguments.Type)
{
case ModPathChangeType.Added:
foreach (var collection in this)
collection.Settings.AddMod(arguments.Mod);
break;
case ModPathChangeType.Deleted:
foreach (var collection in this)
collection.Settings.RemoveMod(arguments.Mod);
break;
case ModPathChangeType.Moved:
var index = arguments.Mod.Index;
foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
break;
case ModPathChangeType.Reloaded:
foreach (var collection in this)
{
if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(arguments.Mod.Index, null);
}
break;
}
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
{
arguments.Type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)
return;
foreach (var collection in this)
{
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();
}
}
}
using Dalamud.Interface.ImGuiNotification;
using Luna;
using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Mods.Settings;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
{
public static readonly LocalCollectionId Zero = new(0);
public static LocalCollectionId operator +(LocalCollectionId left, int right)
=> new(left.Id + right);
}
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _modStorage;
public ModCollection Create(string name, int index, ModCollection? duplicate)
{
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
IReadOnlyList<string> inheritances)
{
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
{
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
_collectionsByLocal[CurrentCollectionId] = newCollection;
CurrentCollectionId += 1;
return newCollection;
}
public void Delete(ModCollection collection)
=> _collectionsByLocal.Remove(collection.Identity.LocalId);
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections =
[
ModCollection.Empty,
];
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
private readonly Dictionary<LocalCollectionId, ModCollection>
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
public readonly ModCollection DefaultNamed;
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
/// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _collections.Count;
public ModCollection this[int index]
=> _collections[index];
/// <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)
{
if (name.Length != 0)
return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
collection = ModCollection.Empty;
return true;
}
/// <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)
{
if (id != Guid.Empty)
return _collections.FindFirst(c => c.Identity.Id == id, out collection);
collection = ModCollection.Empty;
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>
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
{
if (Guid.TryParse(identifier, out var guid))
return ById(guid, 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>
public ModCollection ByLocalId(LocalCollectionId localId)
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
{
_communicator = communicator;
_saveService = saveService;
_modStorage = modStorage;
_communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage);
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage);
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage);
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage);
ReadCollections(out DefaultNamed);
}
public void Dispose()
{
_communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted);
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
}
/// <summary>
/// Add a new collection of the given name.
/// 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.
/// Returns true if the collection was successfully created and fires a Inactive event.
/// Also sets the current collection to the new collection afterwards.
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
if (name.Length == 0)
return false;
var newCollection = Create(name, _collections.Count, duplicate);
_collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success,
false);
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty));
return true;
}
/// <summary> Rename a collection. </summary>
/// <param name="collection"> The collection to rename. </param>
/// <param name="newName"> The new name for the collection. </param>
/// <returns> True if a change has taken place. </returns>
public bool RenameCollection(ModCollection collection, string newName)
{
var oldName = collection.Identity.Name;
if (newName == oldName)
return false;
collection.Identity.Name = newName;
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
_communicator.CollectionRename.Invoke(new CollectionRename.Arguments(collection, oldName, newName));
return true;
}
/// <summary>
/// Remove the given collection if it exists and is neither the empty nor the default-named collection.
/// </summary>
public bool RemoveCollection(ModCollection collection)
{
if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
{
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
return false;
}
if (collection.Identity.Index == DefaultNamed.Identity.Index)
{
Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false);
return false;
}
Delete(collection);
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
_collections.RemoveAt(collection.Identity.Index);
// Update indices.
for (var i = collection.Identity.Index; i < Count; ++i)
_collections[i].Identity.Index = i;
_collectionsByLocal.Remove(collection.Identity.LocalId);
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty));
return true;
}
/// <summary> Remove all settings for not currently-installed mods from the given collection. </summary>
public int CleanUnavailableSettings(ModCollection collection)
{
var count = collection.Settings.Unused.Count;
if (count > 0)
{
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear();
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
return count;
}
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
public void CleanUnavailableSetting(ModCollection collection, string? setting)
{
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
/// <summary>
/// Read all collection files in the Collection Directory.
/// Ensure that the default named collection exists, and apply inheritances afterward.
/// Duplicate collection files are not deleted, just not added here.
/// </summary>
private void ReadCollections(out ModCollection defaultNamedCollection)
{
Penumbra.Log.Debug("[Collections] Reading saved collections...");
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))
continue;
if (id == Guid.Empty)
{
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
continue;
}
if (ById(id, out _))
{
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
NotificationType.Warning);
continue;
}
var collection = CreateFromData(id, name, version, settings, inheritance);
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
try
{
if (version >= 2)
{
try
{
File.Move(file.FullName, correctName, false);
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
NotificationType.Warning);
}
catch (Exception ex)
{
Penumbra.Messager.NotificationMessage(
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
NotificationType.Warning);
}
}
else
{
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
try
{
File.Move(file.FullName, file.FullName + ".bak", true);
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file.");
}
catch (Exception ex)
{
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}");
}
}
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e,
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
NotificationType.Error);
}
_collections.Add(collection);
}
defaultNamedCollection = SetDefaultNamedCollection();
Penumbra.Log.Debug($"[Collections] Found {Count} saved collections.");
}
/// <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.
/// </summary>
private ModCollection SetDefaultNamedCollection()
{
if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
return collection;
if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
return _collections[^1];
Penumbra.Messager.NotificationMessage(
$"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> Move all settings in all collections to unused settings. </summary>
private void OnModDiscoveryStarted()
{
foreach (var collection in this)
collection.Settings.PrepareModDiscovery(_modStorage);
}
/// <summary> Restore all settings in all collections to mods. </summary>
private void OnModDiscoveryFinished()
{
// Re-apply all mod settings.
foreach (var collection in this)
collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
}
/// <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)
{
switch (arguments.Type)
{
case ModPathChangeType.Added:
foreach (var collection in this)
collection.Settings.AddMod(arguments.Mod);
break;
case ModPathChangeType.Deleted:
foreach (var collection in this)
collection.Settings.RemoveMod(arguments.Mod);
break;
case ModPathChangeType.Moved:
var index = arguments.Mod.Index;
foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null))
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
break;
case ModPathChangeType.Reloaded:
foreach (var collection in this)
{
if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
collection.Settings.SetTemporary(arguments.Mod.Index, null);
}
break;
}
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
{
arguments.Type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)
return;
foreach (var collection in this)
{
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" />
ItemSwapTab = 0,
/// <seealso cref="UI.CollectionTab.CollectionSelector.OnCollectionChange" />
CollectionSelector = 0,
/// <seealso cref="UI.CollectionTab.CollectionSelector.Cache.OnCollectionChange" />
CollectionSelectorCache = 0,
/// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/>
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 ModSelectorMaximumScale { get; set; } = 0.5f;
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty;
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = [];
public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false;
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty;
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = [];
public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false;
/// <summary>
/// Load the current configuration.

View file

@ -56,7 +56,8 @@ public class ModEditor(
SwapEditor.Revert(Option!);
MetaEditor.Load(Mod!, Option!);
Duplicates.Clear();
MdlMaterialEditor.ScanModels(Mod!);
MdlMaterialEditor.ScanModels(Mod!);
OptionLoaded?.Invoke();
});
}
@ -80,21 +81,22 @@ public class ModEditor(
Files.UpdatePaths(Mod!, Option!);
MetaEditor.Load(Mod!, Option!);
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>
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)
{
Group = null;
Option = Mod.Default;
GroupIdx = groupIdx;
DataIdx = dataIdx;
DataIdx = dataIdx;
return;
}
@ -105,7 +107,7 @@ public class ModEditor(
{
Option = Group.DataContainers[dataIdx];
GroupIdx = groupIdx;
DataIdx = dataIdx;
DataIdx = dataIdx;
return;
}
}
@ -119,6 +121,8 @@ public class ModEditor(
Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}.");
}
public event Action? OptionLoaded;
public void Clear()
{
Duplicates.Clear();
@ -126,7 +130,8 @@ public class ModEditor(
Files.Clear();
MetaEditor.Clear();
Mod = null;
LoadOption(0, 0, false);
LoadOption(0, 0, false);
OptionLoaded?.Invoke();
}
public void Dispose()
@ -146,7 +151,7 @@ public class ModEditor(
foreach (var subDir in baseDir.GetDirectories())
{
ClearEmptySubDirectories(subDir);
if (subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0)
if (subDir.GetFiles().Length is 0 && subDir.GetDirectories().Length is 0)
subDir.Delete();
}
}

View file

@ -1,208 +1,197 @@
using Luna;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Editor;
public class ModFileCollection : IDisposable
{
private readonly List<FileRegistry> _available = [];
private readonly List<FileRegistry> _mtrl = [];
private readonly List<FileRegistry> _mdl = [];
private readonly List<FileRegistry> _tex = [];
private readonly List<FileRegistry> _shpk = [];
private readonly List<FileRegistry> _pbd = [];
private readonly List<FileRegistry> _atch = [];
private readonly SortedSet<FullPath> _missing = [];
private readonly HashSet<Utf8GamePath> _usedPaths = [];
public IReadOnlySet<FullPath> Missing
=> Ready ? _missing : [];
public IReadOnlySet<Utf8GamePath> UsedPaths
=> Ready ? _usedPaths : [];
public IReadOnlyList<FileRegistry> Available
=> Ready ? _available : [];
public IReadOnlyList<FileRegistry> Mtrl
=> Ready ? _mtrl : [];
public IReadOnlyList<FileRegistry> Mdl
=> Ready ? _mdl : [];
public IReadOnlyList<FileRegistry> Tex
=> Ready ? _tex : [];
public IReadOnlyList<FileRegistry> Shpk
=> Ready ? _shpk : [];
public IReadOnlyList<FileRegistry> Pbd
=> Ready ? _pbd : [];
public IReadOnlyList<FileRegistry> Atch
=> Ready ? _atch : [];
public bool Ready { get; private set; } = true;
public void UpdateAll(Mod mod, IModDataContainer option)
{
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 Clear()
{
ClearFiles();
ClearPaths(false, CancellationToken.None);
}
public void Dispose()
=> Clear();
public void ClearMissingFiles()
=> _missing.Clear();
public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Remove(gamePath);
if (file != null)
{
--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 AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Add(gamePath);
if (file == null)
return;
++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 ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
{
var oldPath = file.SubModUsage[pathIdx];
_usedPaths.Remove(oldPath.Item2);
if (!gamePath.IsEmpty)
{
file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath);
_usedPaths.Add(gamePath);
}
else
{
--file.CurrentUsage;
file.SubModUsage.RemoveAt(pathIdx);
}
}
private void UpdateFiles(Mod mod, CancellationToken tok)
{
tok.ThrowIfCancellationRequested();
ClearFiles();
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))
continue;
_available.Add(registry);
switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant())
{
case ".mtrl":
_mtrl.Add(registry);
break;
case ".mdl":
_mdl.Add(registry);
break;
case ".tex":
_tex.Add(registry);
break;
case ".shpk":
_shpk.Add(registry);
break;
case ".pbd":
_pbd.Add(registry);
break;
case ".atch":
_atch.Add(registry);
break;
}
}
}
private void ClearFiles()
{
_available.Clear();
_mtrl.Clear();
_mdl.Clear();
_tex.Clear();
_shpk.Clear();
_pbd.Clear();
_atch.Clear();
}
private void ClearPaths(bool clearRegistries, CancellationToken tok)
{
if (clearRegistries)
foreach (var reg in _available)
{
tok.ThrowIfCancellationRequested();
reg.CurrentUsage = 0;
reg.SubModUsage.Clear();
}
_missing.Clear();
_usedPaths.Clear();
}
private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok)
{
tok.ThrowIfCancellationRequested();
ClearPaths(clearRegistries, tok);
tok.ThrowIfCancellationRequested();
foreach (var subMod in mod.AllDataContainers)
{
foreach (var (gamePath, file) in subMod.Files)
{
tok.ThrowIfCancellationRequested();
if (!file.Exists)
{
_missing.Add(file);
if (subMod == option)
_usedPaths.Add(gamePath);
}
else
{
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));
}
}
}
}
}
using ImSharp.Containers;
using Luna;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Editor;
public class ModFileCollection : IDisposable
{
private readonly ObservableList<FileRegistry> _available = [];
private readonly ObservableList<FileRegistry> _mtrl = [];
private readonly ObservableList<FileRegistry> _mdl = [];
private readonly ObservableList<FileRegistry> _tex = [];
private readonly ObservableList<FileRegistry> _shpk = [];
private readonly ObservableList<FileRegistry> _pbd = [];
private readonly ObservableList<FileRegistry> _atch = [];
private readonly SortedSet<FullPath> _missing = [];
private readonly HashSet<Utf8GamePath> _usedPaths = [];
public IReadOnlySet<FullPath> Missing
=> Ready ? _missing : [];
public IReadOnlySet<Utf8GamePath> UsedPaths
=> Ready ? _usedPaths : [];
public IObservableList<FileRegistry> Available
=> Ready ? _available : [];
public IObservableList<FileRegistry> Mtrl
=> Ready ? _mtrl : [];
public IObservableList<FileRegistry> Mdl
=> Ready ? _mdl : [];
public IObservableList<FileRegistry> Tex
=> Ready ? _tex : [];
public IObservableList<FileRegistry> Shpk
=> Ready ? _shpk : [];
public IObservableList<FileRegistry> Pbd
=> Ready ? _pbd : [];
public IObservableList<FileRegistry> Atch
=> Ready ? _atch : [];
public bool Ready { get; private set; } = true;
public void UpdateAll(Mod mod, IModDataContainer option)
{
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 Clear()
{
ClearFiles();
ClearPaths(false, CancellationToken.None);
}
public void Dispose()
=> Clear();
public void ClearMissingFiles()
=> _missing.Clear();
public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Remove(gamePath);
if (file != null)
{
--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 AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Add(gamePath);
if (file == null)
return;
++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 ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
{
var oldPath = file.SubModUsage[pathIdx];
_usedPaths.Remove(oldPath.Item2);
if (!gamePath.IsEmpty)
{
file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath);
_usedPaths.Add(gamePath);
}
else
{
--file.CurrentUsage;
file.SubModUsage.RemoveAt(pathIdx);
}
}
private void UpdateFiles(Mod mod, CancellationToken tok)
{
tok.ThrowIfCancellationRequested();
ClearFiles();
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))
continue;
_available.Add(registry);
switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant())
{
case ".mtrl": _mtrl.Add(registry); break;
case ".mdl": _mdl.Add(registry); break;
case ".tex": _tex.Add(registry); break;
case ".shpk": _shpk.Add(registry); break;
case ".pbd": _pbd.Add(registry); break;
case ".atch": _atch.Add(registry); break;
}
}
}
private void ClearFiles()
{
_available.Clear();
_mtrl.Clear();
_mdl.Clear();
_tex.Clear();
_shpk.Clear();
_pbd.Clear();
_atch.Clear();
}
private void ClearPaths(bool clearRegistries, CancellationToken tok)
{
if (clearRegistries)
foreach (var reg in _available)
{
tok.ThrowIfCancellationRequested();
reg.CurrentUsage = 0;
reg.SubModUsage.Clear();
}
_missing.Clear();
_usedPaths.Clear();
}
private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok)
{
tok.ThrowIfCancellationRequested();
ClearPaths(clearRegistries, tok);
tok.ThrowIfCancellationRequested();
foreach (var subMod in mod.AllDataContainers)
{
foreach (var (gamePath, file) in subMod.Files)
{
tok.ThrowIfCancellationRequested();
if (!file.Exists)
{
_missing.Add(file);
if (subMod == option)
_usedPaths.Add(gamePath);
}
else
{
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
{
/// <inheritdoc cref="Communication.CollectionRename"/>
public readonly CollectionRename CollectionRename = services.GetService<CollectionRename>();
/// <inheritdoc cref="Communication.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.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.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()
?? _config.Ephemeral.ChangedItemFilter;

View file

@ -1,4 +1,3 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin.Services;
using ImSharp;
using OtterGui.Widgets;
@ -8,121 +7,181 @@ using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.UI.AdvancedWindow.Materials;
using FilterComboColors = Penumbra.UI.FilterComboColors;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.Services;
//public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) : SimpleFilterCombo<StmKeyType>(SimpleFilterType.Text)
// 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 record StainTemplate(StringPair Id, Vector4 Diffuse, Vector4 Specular, Vector4 Emissive, int Key, bool Found)
{
public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile)
: FilterComboCache<StmKeyType>(stmFile.Entries.Keys.Prepend(0), MouseWheelType.None, Penumbra.Log)
where TDyePack : unmanaged, IDyePack
public Vector4 Diffuse { get; set; } = Diffuse;
public Vector4 Specular { get; set; } = Specular;
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.
public int CurrentDyeChannel = 0;
PreviewAlignment = new Vector2(0.90f, 0.5f);
_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;
if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0)
return baseSize;
return baseSize + Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
_parent = parent;
foreach (var combo in _parent._stainCombos)
combo.SelectionChanged += OnSelectionChanged;
_dyeChannel = _parent._currentDyeChannel;
}
protected override string ToString(StmKeyType obj)
=> $"{obj,4}";
protected override void DrawFilter(int currentSelected, float width)
public override void Update()
{
using var font = Im.Font.PushDefault();
base.DrawFilter(currentSelected, width);
base.Update();
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,
ImGuiComboFlags flags = ImGuiComboFlags.None)
private void OnSelectionChanged(Luna.FilterComboColors.Item obj)
{
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);
UpdateItems();
ComputeWidth();
}
protected override bool DrawSelectable(int globalIdx, bool selected)
private void UpdateItems()
{
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;
foreach (var item in UnfilteredItems)
{
var dye = _parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id;
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();
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;
protected override void ComputeWidth()
{
ComboWidth = Im.Font.Mono.CalculateTextSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X;
if (_parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id is 0)
return;
ComboWidth += Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
}
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 readonly DictStain StainData;
public readonly FilterComboColors StainCombo1;
public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this?
public readonly StainCombo StainCombo1;
public readonly StainCombo StainCombo2; // FIXME is there a better way to handle this?
public readonly StmFile<LegacyDyePack> LegacyStmFile;
public readonly StmFile<DyePack> GudStmFile;
public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo;
@ -130,9 +189,8 @@ public class StainService : Luna.IService
public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData)
{
StainData = stainData;
StainCombo1 = CreateStainCombo();
StainCombo2 = CreateStainCombo();
StainCombo1 = new StainCombo(stainData);
StainCombo2 = new StainCombo(stainData);
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);
GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile);
}
/// <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
{
0 => StainCombo1,
@ -180,8 +238,9 @@ public class StainService : Luna.IService
return new StmFile<TDyePack>(dataManager);
}
private FilterComboColors CreateStainCombo()
=> new(140, MouseWheelType.None,
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(),
Penumbra.Log);
public sealed class StainCombo(DictStain stainData) : Luna.FilterComboColors
{
protected override IEnumerable<Item> GetItems()
=> 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 ImSharp;
using Luna;
using OtterGui.Classes;
using OtterGui.Widgets;
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
@ -11,7 +9,6 @@ using Penumbra.Mods.Editor;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.AdvancedWindow;
@ -24,7 +21,7 @@ public class FileEditor<T>(
FileDialogService fileDialog,
string tabName,
string fileType,
Func<IReadOnlyList<FileRegistry>> getFiles,
Func<IEnumerable<FileRegistry>> getFiles,
Func<T, bool, bool> drawEdit,
Func<string> getInitialPath,
Func<byte[], string, bool, T?> parseFile)
@ -74,10 +71,15 @@ public class FileEditor<T>(
_defaultFile = null;
}
private FileRegistry? _currentPath;
private T? _currentFile;
private Exception? _currentException;
private bool _changed;
private FileRegistry? CurrentPath
{
get => _combo.Selected;
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 bool _inInput;
@ -163,7 +165,7 @@ public class FileEditor<T>(
public void Reset()
{
_currentException = null;
_currentPath = null;
CurrentPath = null;
(_currentFile as IDisposable)?.Dispose();
_currentFile = null;
_changed = false;
@ -171,26 +173,32 @@ public class FileEditor<T>(
private void DrawFileSelectCombo()
{
if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty,
Im.ContentRegion.Available.X, Im.Style.TextHeight)
&& _combo.CurrentSelection != null)
UpdateCurrentFile(_combo.CurrentSelection);
if (CurrentPath is not null)
{
if (_combo.Draw("##select"u8, CurrentPath.RelPath.Path.Span, StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection))
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)
{
if (ReferenceEquals(_currentPath, path))
if (ReferenceEquals(CurrentPath, path))
return;
_changed = false;
_currentPath = path;
CurrentPath = path;
_currentException = null;
try
{
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
var bytes = File.ReadAllBytes(CurrentPath.File.FullName);
(_currentFile as IDisposable)?.Dispose();
_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)
{
@ -210,9 +218,9 @@ public class FileEditor<T>(
public void SaveFile()
{
compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write());
compactor.WriteAllBytes(CurrentPath!.File.FullName, _currentFile!.Write());
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;
}
@ -221,8 +229,8 @@ public class FileEditor<T>(
if (ImEx.Button("Reset Changes"u8, Vector2.Zero,
$"Reset all changes made to the {fileType} file.", !_changed))
{
var tmp = _currentPath;
_currentPath = null;
var tmp = CurrentPath;
CurrentPath = null;
UpdateCurrentFile(tmp!);
}
}
@ -233,7 +241,7 @@ public class FileEditor<T>(
if (!child)
return;
if (_currentPath is not null)
if (CurrentPath is not null)
{
if (_currentFile is null)
{
@ -253,7 +261,7 @@ public class FileEditor<T>(
if (!_inInput && _defaultPath.Length > 0)
{
if (_currentPath is not null)
if (CurrentPath is not null)
{
Im.Line.New();
Im.Line.New();
@ -278,47 +286,70 @@ public class FileEditor<T>(
}
}
private class Combo(Func<IReadOnlyList<FileRegistry>> generator)
: FilterComboCache<FileRegistry>(generator, MouseWheelType.None, Penumbra.Log)
private sealed class Combo : FilterComboBase<FileRegistry>
{
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;
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())
{
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.Separator();
using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit);
if (t)
{
foreach (var (option, gamePath) in file.SubModUsage)
foreach (var (option, gamePath) in item.SubModUsage)
{
t.DrawColumn(gamePath.Path.Span);
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
t.DrawColumn(option.GetFullName());
}
}
}
if (file.SubModUsage.Count > 0)
if (item.SubModUsage.Count > 0)
{
Im.Line.Same();
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;
}
protected override bool IsVisible(int globalIndex, LowerString filter)
=> filter.IsContained(Items[globalIndex].File.FullName)
|| Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString()));
protected override bool IsSelected(FileRegistry item, int globalIndex)
=> item.Equals(Selected);
}
}

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 Luna;
using Luna.Generators;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@ -12,7 +11,6 @@ using Penumbra.GameData.Structs;
using Penumbra.Import.Structs;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.ItemSwap;
using Penumbra.Mods.Manager;
@ -22,8 +20,6 @@ using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
using ITab = OtterGui.Widgets.ITab;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.UI.AdvancedWindow;
@ -152,44 +148,6 @@ public class ItemSwapTab : IDisposable, ITab
_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 ItemSwapContainer _swapData;
@ -241,17 +199,17 @@ public class ItemSwapTab : IDisposable, ITab
case SwapType.Ring:
case SwapType.Glasses:
var values = _selectors[_lastTab];
if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown
&& values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown)
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item,
if (values.Source.CurrentSelection.Type is not FullEquipType.Unknown
&& values.Target.CurrentSelection.Type is not FullEquipType.Unknown)
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection,
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
break;
case SwapType.BetweenSlots:
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid)
_affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom),
selectorFrom.CurrentSelection.Item,
if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid)
_affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection, ToEquipSlot(_slotFrom),
selectorFrom.CurrentSelection,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
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()}";
case SwapType.BetweenSlots:
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:
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();
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name, string.Empty,
InputWidth * 2 * Im.Style.GlobalScale,
Im.Style.TextHeightWithSpacing);
_dirty |= selector.Draw("##itemSource"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
(article1, _, selector) = GetAccessorySelector(_slotTo, false);
table.DrawFrameColumn($"and put {article2} on {article1}");
@ -566,8 +522,7 @@ public class ItemSwapTab : IDisposable, ITab
}
table.NextColumn();
_dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * Im.Style.GlobalScale,
Im.Style.TextHeightWithSpacing);
_dirty |= selector.Draw("##itemTarget"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
if (_affectedItems is not { Count: > 1 })
return;
@ -576,7 +531,7 @@ public class ItemSwapTab : IDisposable, ITab
if (Im.Item.Hovered())
{
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);
}
}
@ -610,8 +565,7 @@ public class ItemSwapTab : IDisposable, ITab
return;
table.DrawFrameColumn(text1);
table.NextColumn();
_dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item.Name, string.Empty,
InputWidth * 2 * Im.Style.GlobalScale, Im.Style.TextHeightWithSpacing);
_dirty |= sourceSelector.Draw("##itemSource"u8, sourceSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
if (type is SwapType.Ring)
{
@ -621,9 +575,7 @@ public class ItemSwapTab : IDisposable, ITab
table.DrawFrameColumn(text2);
table.NextColumn();
_dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item.Name, string.Empty,
InputWidth * 2 * Im.Style.GlobalScale,
Im.Style.TextHeightWithSpacing);
_dirty |= targetSelector.Draw("##itemTarget"u8, targetSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
if (type is SwapType.Ring)
{
Im.Line.Same();
@ -638,7 +590,7 @@ public class ItemSwapTab : IDisposable, ITab
if (Im.Item.Hovered())
{
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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,8 +41,12 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
var height = ColumnHeight;
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);
id.Pop();
}
}
public abstract ReadOnlySpan<byte> Label { get; }

View file

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

View file

@ -1,7 +1,6 @@
using Dalamud.Interface;
using ImSharp;
using Luna;
using OtterGui;
using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
@ -20,10 +19,7 @@ public partial class ModEditWindow
private int _pathIdx = -1;
private int _folderSkip;
private bool _overviewMode;
private string _fileOverviewFilter1 = string.Empty;
private string _fileOverviewFilter2 = string.Empty;
private string _fileOverviewFilter3 = string.Empty;
private readonly OverviewTable _overviewTable;
private bool CheckFilter(FileRegistry registry)
=> _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase);
@ -40,9 +36,7 @@ public partial class ModEditWindow
DrawOptionSelectHeader();
DrawButtonHeader();
if (_overviewMode)
DrawFileManagementOverview();
else
if (!_overviewMode)
DrawFileManagementNormal();
using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true);
@ -50,65 +44,11 @@ public partial class ModEditWindow
return;
if (_overviewMode)
DrawFilesOverviewMode();
_overviewTable.Draw();
else
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()
{
@ -154,7 +94,7 @@ public partial class ModEditWindow
{
using var tt = Im.Tooltip.Begin();
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 pos = Im.Cursor.X - Im.Style.FrameHeight;
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;
_pathIdx = j;
@ -341,7 +281,8 @@ public partial class ModEditWindow
if (Im.Button("Add Paths"u8))
_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();
@ -365,7 +306,7 @@ public partial class ModEditWindow
Im.Line.Same();
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))
{
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");
}
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)
{
var drawer = _metaDrawers.Get(type);
if (drawer == null)
if (drawer is null)
return;
var oldPos = Im.Cursor.Y;

View file

@ -621,6 +621,7 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
_fileDialog = fileDialog;
_framework = framework;
_metaDrawers = metaDrawers;
_overviewTable = new OverviewTable(_editor);
_optionSelect = new OptionSelectCombo(editor, this);
_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,

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);
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()
@ -185,6 +186,7 @@ public class CollectionSelectHeader(
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
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)
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);
}

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.GameFonts;
using Dalamud.Interface.ImGuiNotification;
@ -14,6 +13,7 @@ using Penumbra.GameData.Enums;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
namespace Penumbra.UI.CollectionTab;
@ -26,8 +26,9 @@ public sealed class CollectionPanel(
ITargetManager targets,
ModStorage mods,
SaveService saveService,
IncognitoService incognito)
: IDisposable
IncognitoService incognito,
Configuration config)
: IDisposable, IPanel
{
private readonly CollectionStorage _collections = manager.Storage;
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));
Im.Item.SetNextWidth(width);
if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName)
&& newName != collection.Identity.Name)
{
collection.Identity.Name = newName;
saveService.QueueSave(new ModCollectionSave(mods, collection));
selector.RestoreCollections();
}
if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName))
_collections.RenameCollection(collection, newName);
}
if (_collections.DefaultNamed == collection)
@ -770,4 +766,18 @@ public sealed class CollectionPanel(
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 OtterGui;
using Luna;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
@ -9,87 +9,20 @@ using Penumbra.UI.Classes;
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;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
private readonly TutorialService _tutorial;
private readonly IncognitoService _incognito;
public ReadOnlySpan<byte> Id
=> "##cs"u8;
private ModCollection? _dragging;
public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active,
TutorialService tutorial, IncognitoService incognito)
: base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
public record struct Entry(ModCollection Collection, StringU8 Name, StringPair AnonymousName)
{
_config = config;
_communicator = communicator;
_storage = storage;
_active = active;
_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 Entry(ModCollection collection)
: this(collection,
collection.Identity.Name.Length > 0 ? new StringU8(collection.Identity.Name) : new StringU8(collection.Identity.AnonymizedName),
new StringPair(collection.Identity.AnonymizedName))
{ }
}
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))
return;
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
active.SetCollection(_dragging, type, active.Individuals.GetGroup(identifier));
_dragging = null;
}
public void Dispose()
public void Draw()
{
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
}
private string Name(ModCollection collection)
=> _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)
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());
foreach (var item in cache)
{
case CollectionType.Temporary: return;
case CollectionType.Current:
if (arguments.NewCollection is not null)
SetCurrent(arguments.NewCollection);
SetFilterDirty();
return;
case CollectionType.Inactive:
RestoreCollections();
SetFilterDirty();
return;
default:
SetFilterDirty();
return;
Im.Cursor.X += Im.Style.FramePadding.X;
var ret = Im.Selectable(incognito.IncognitoMode ? item.AnonymousName : item.Name, active.Current == item.Collection);
using var source = Im.DragDrop.Source();
if (active.Current == item.Collection)
tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
if (source)
{
_dragging = item.Collection;
source.SetPayload("Collection"u8);
Im.Text($"Assigning {(incognito.IncognitoMode ? item.AnonymousName : item.Name)} to...");
}
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 Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType;
namespace Penumbra.UI.MainWindow;

View file

@ -1,65 +1,22 @@
using ImSharp;
using Luna;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
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 bool _temporary;
private bool _locked;
private TemporaryModSettings? _tempSettings;
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)
{
@ -79,7 +36,7 @@ public sealed class ModGroupDrawer : IUiService
switch (group.Behaviour)
{
case GroupDrawBehaviour.SingleSelection when group.Options.Count <= _config.SingleGroupRadioMax:
case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax:
case GroupDrawBehaviour.MultiSelection:
_blockGroupCache.Add((group, idx));
break;
@ -120,9 +77,8 @@ public sealed class ModGroupDrawer : IUiService
private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting)
{
using var id = Im.Id.Push(groupIdx);
var selectedOption = setting.AsIndex;
using var disabled = Im.Disabled(_locked);
_combo.Draw(group, groupIdx, selectedOption);
combo.Draw(this, (SingleModGroup)group, groupIdx, setting);
if (group.Description.Length > 0)
{
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)
{
if (options.Count <= _config.OptionGroupCollapsibleMin)
if (options.Count <= config.OptionGroupCollapsibleMin)
{
draw();
}
@ -272,21 +228,21 @@ public sealed class ModGroupDrawer : IUiService
}
private ModCollection Current
=> _collectionManager.Active.Current;
=> collectionManager.Active.Current;
[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!.ForceInherit = false;
_tempSettings!.Settings[groupIdx] = setting;
_collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings);
collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings);
}
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 ImSharp;
using Luna;
using OtterGui.Widgets;
using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations;

View file

@ -120,7 +120,15 @@ public sealed class ModFileSystemCache(ModFileSystemDrawer parent)
}
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)
=> 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 Dalamud.Plugin;
using ImSharp;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs;
public sealed class CollectionsTab : ITab<TabType>, IDisposable
{
private readonly EphemeralConfig _config;
private readonly CollectionSelector _selector;
private readonly CollectionPanel _panel;
private readonly TutorialService _tutorial;
private readonly IncognitoService _incognito;
public enum PanelMode
{
SimpleAssignment,
IndividualAssignment,
GroupAssignment,
Details,
};
public PanelMode Mode
{
get => _config.CollectionPanel;
set
{
_config.CollectionPanel = value;
_config.Save();
}
}
public TabType Identifier
=> 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);
}
}
using ImSharp;
using Luna;
using Penumbra.Api.Enums;
using Penumbra.UI.Classes;
using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs;
public sealed class CollectionsTab : TwoPanelLayout, ITab<TabType>
{
private readonly TutorialService _tutorial;
public TabType Identifier
=> TabType.Collections;
public CollectionsTab(TutorialService tutorial, CollectionButtonFooter leftFooter, CollectionSelector leftPanel, CollectionFilter filter,
CollectionModeHeader rightHeader, CollectionPanel rightPanel)
{
LeftHeader = new FilterHeader<CollectionSelector.Entry>(filter, new StringU8("Filter..."u8));
LeftPanel = leftPanel;
LeftFooter = leftFooter;
RightHeader = rightHeader;
RightPanel = rightPanel;
RightFooter = NopHeaderFooter.Instance;
_tutorial = tutorial;
}
public override ReadOnlySpan<byte> Label
=> "Collections"u8;
protected override void DrawLeftGroup()
{
base.DrawLeftGroup();
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
}
public void DrawContent()
=> Draw();
public void PostTabButton()
=> _tutorial.OpenTutorial(BasicTutorialSteps.Collections);
}

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.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface.Colors;
using ImSharp;
using Luna;
@ -15,7 +13,6 @@ using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Files;
using Penumbra.GameData.Interop;
using Penumbra.Import.Structs;
@ -28,7 +25,6 @@ using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.UI.Classes;
using ImGuiClip = OtterGui.ImGuiClip;
using Penumbra.Api.IpcTester;
using Penumbra.GameData.Data;
using Penumbra.Interop.Hooks.PostProcessing;
@ -65,54 +61,54 @@ public class Diagnostics(ServiceManager provider) : IUiService
public sealed class DebugTab : Window, ITab<TabType>
{
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi;
private readonly ActorManager _actors;
private readonly StainService _stains;
private readonly GlobalVariablesDrawer _globalVariablesDrawer;
private readonly ResourceManagerService _resourceManager;
private readonly ResourceLoader _resourceLoader;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly PathState _pathState;
private readonly SubfileHelper _subfileHelper;
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
private readonly CutsceneService _cutsceneService;
private readonly ModImportManager _modImporter;
private readonly ImportPopup _importPopup;
private readonly FrameworkManager _framework;
private readonly TextureManager _textureManager;
private readonly ShaderReplacementFixer _shaderReplacementFixer;
private readonly RedrawService _redraws;
private readonly DictEmote _emotes;
private readonly Diagnostics _diagnostics;
private readonly ObjectManager _objects;
private readonly IDataManager _dataManager;
private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel;
private readonly TexHeaderDrawer _texHeaderDrawer;
private readonly HookOverrideDrawer _hookOverrides;
private readonly RsfService _rsfService;
private readonly SchedulerResourceManagementService _schedulerService;
private readonly ObjectIdentification _objectIdentification;
private readonly RenderTargetDrawer _renderTargetDrawer;
private readonly ModMigratorDebug _modMigratorDebug;
private readonly ShapeInspector _shapeInspector;
private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer;
private readonly DragDropManager _dragDropManager;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi;
private readonly ActorManager _actors;
private readonly StainService _stains;
private readonly GlobalVariablesDrawer _globalVariablesDrawer;
private readonly ResourceManagerService _resourceManager;
private readonly ResourceLoader _resourceLoader;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly PathState _pathState;
private readonly SubfileHelper _subfileHelper;
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
private readonly CutsceneService _cutsceneService;
private readonly ModImportManager _modImporter;
private readonly ImportPopup _importPopup;
private readonly FrameworkManager _framework;
private readonly TextureManager _textureManager;
private readonly ShaderReplacementFixer _shaderReplacementFixer;
private readonly RedrawService _redraws;
private readonly EmoteListDrawer _emotes;
private readonly Diagnostics _diagnostics;
private readonly ObjectManager _objects;
private readonly IDataManager _dataManager;
private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel;
private readonly TexHeaderDrawer _texHeaderDrawer;
private readonly HookOverrideDrawer _hookOverrides;
private readonly RsfService _rsfService;
private readonly ActionTmbListDrawer _actionTmbs;
private readonly ObjectIdentification _objectIdentification;
private readonly RenderTargetDrawer _renderTargetDrawer;
private readonly ModMigratorDebug _modMigratorDebug;
private readonly ShapeInspector _shapeInspector;
private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer;
private readonly DragDropManager _dragDropManager;
public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
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,
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,
DragDropManager dragDropManager)
: base("Penumbra Debug Window", WindowFlags.NoCollapse)
@ -152,7 +148,7 @@ public sealed class DebugTab : Window, ITab<TabType>
_hookOverrides = hookOverrides;
_rsfService = rsfService;
_globalVariablesDrawer = globalVariablesDrawer;
_schedulerService = schedulerService;
_actionTmbs = actionTmbs;
_objectIdentification = objectIdentification;
_renderTargetDrawer = renderTargetDrawer;
_modMigratorDebug = modMigratorDebug;
@ -205,7 +201,7 @@ public sealed class DebugTab : Window, ITab<TabType>
_globalVariablesDrawer.Draw();
DrawCloudApi();
DrawDebugTabIpc();
if(Im.Tree.Header("Drag & Drop Manager"u8))
if (Im.Tree.Header("Drag & Drop Manager"u8))
_dragDropManager.DrawDebugInfo();
}
@ -742,7 +738,7 @@ public sealed class DebugTab : Window, ITab<TabType>
{
using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit);
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);
}
}
@ -814,11 +810,6 @@ public sealed class DebugTab : Window, ITab<TabType>
Im.Selectable(item.Key);
}
private string _emoteSearchFile = string.Empty;
private string _emoteSearchName = string.Empty;
private AtchFile? _atchFile;
private void DrawAtch()
@ -842,57 +833,19 @@ public sealed class DebugTab : Window, ITab<TabType>
AtchDrawer.Draw(_atchFile);
}
private void DrawEmotes()
{
using var mainTree = Im.Tree.Node("Emotes"u8);
if (!mainTree)
return;
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);
if (mainTree)
_emotes.Draw();
}
private string _tmbKeyFilter = string.Empty;
private CiByteString _tmbKeyFilterU8 = CiByteString.Empty;
private void DrawActionTmbs()
{
using var mainTree = Im.Tree.Node("Action TMBs"u8);
if (!mainTree)
return;
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);
if (mainTree)
_actionTmbs.Draw();
}
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();
}
}