mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-03 14:23:43 +01:00
Current state, all done except for file system selector.
Some checks failed
.NET Build / build (push) Has been cancelled
Some checks failed
.NET Build / build (push) Has been cancelled
This commit is contained in:
parent
97a14db4d5
commit
cabcaadde3
41 changed files with 1749 additions and 1511 deletions
2
Luna
2
Luna
|
|
@ -1 +1 @@
|
|||
Subproject commit e52d0dab9fd7f64d108125b79e387052fae2434f
|
||||
Subproject commit d81c788133b8b557febbad0bf74baee9588215eb
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
20
Penumbra/Communication/CollectionRename.cs
Normal file
20
Penumbra/Communication/CollectionRename.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
94
Penumbra/UI/AdvancedWindow/ItemSelector.cs
Normal file
94
Penumbra/UI/AdvancedWindow/ItemSelector.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
141
Penumbra/UI/AdvancedWindow/OverviewTable.cs
Normal file
141
Penumbra/UI/AdvancedWindow/OverviewTable.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
13
Penumbra/UI/CollectionTab/CollectionFilter.cs
Normal file
13
Penumbra/UI/CollectionTab/CollectionFilter.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
52
Penumbra/UI/Combos/SingleGroupCombo.cs
Normal file
52
Penumbra/UI/Combos/SingleGroupCombo.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
90
Penumbra/UI/Tabs/CollectionButtonFooter.cs
Normal file
90
Penumbra/UI/Tabs/CollectionButtonFooter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Penumbra/UI/Tabs/CollectionModeHeaderFooter.cs
Normal file
66
Penumbra/UI/Tabs/CollectionModeHeaderFooter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
51
Penumbra/UI/Tabs/Debug/ActionTmbListDrawer.cs
Normal file
51
Penumbra/UI/Tabs/Debug/ActionTmbListDrawer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
79
Penumbra/UI/Tabs/Debug/EmoteListDrawer.cs
Normal file
79
Penumbra/UI/Tabs/Debug/EmoteListDrawer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue