mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-20 14:57:50 +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 Dalamud.Interface.ImGuiNotification;
|
||||||
using Luna;
|
using Luna;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Mods.Manager.OptionEditor;
|
using Penumbra.Mods.Manager.OptionEditor;
|
||||||
using Penumbra.Mods.Settings;
|
using Penumbra.Mods.Settings;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Collections.Manager;
|
namespace Penumbra.Collections.Manager;
|
||||||
|
|
||||||
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
|
/// <summary> A contiguously incrementing ID managed by the CollectionCreator. </summary>
|
||||||
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
|
public readonly record struct LocalCollectionId(int Id) : IAdditionOperators<LocalCollectionId, int, LocalCollectionId>
|
||||||
{
|
{
|
||||||
public static readonly LocalCollectionId Zero = new(0);
|
public static readonly LocalCollectionId Zero = new(0);
|
||||||
|
|
||||||
public static LocalCollectionId operator +(LocalCollectionId left, int right)
|
public static LocalCollectionId operator +(LocalCollectionId left, int right)
|
||||||
=> new(left.Id + right);
|
=> new(left.Id + right);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
|
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable, IService
|
||||||
{
|
{
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly SaveService _saveService;
|
private readonly SaveService _saveService;
|
||||||
private readonly ModStorage _modStorage;
|
private readonly ModStorage _modStorage;
|
||||||
|
|
||||||
public ModCollection Create(string name, int index, ModCollection? duplicate)
|
public ModCollection Create(string name, int index, ModCollection? duplicate)
|
||||||
{
|
{
|
||||||
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
|
var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index)
|
||||||
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
|
?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count);
|
||||||
_collectionsByLocal[CurrentCollectionId] = newCollection;
|
_collectionsByLocal[CurrentCollectionId] = newCollection;
|
||||||
CurrentCollectionId += 1;
|
CurrentCollectionId += 1;
|
||||||
return newCollection;
|
return newCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
|
public ModCollection CreateFromData(Guid id, string name, int version, Dictionary<string, ModSettings.SavedSettings> allSettings,
|
||||||
IReadOnlyList<string> inheritances)
|
IReadOnlyList<string> inheritances)
|
||||||
{
|
{
|
||||||
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
|
var newCollection = ModCollection.CreateFromData(_saveService, _modStorage,
|
||||||
new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
|
new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances);
|
||||||
_collectionsByLocal[CurrentCollectionId] = newCollection;
|
_collectionsByLocal[CurrentCollectionId] = newCollection;
|
||||||
CurrentCollectionId += 1;
|
CurrentCollectionId += 1;
|
||||||
return newCollection;
|
return newCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
|
public ModCollection CreateTemporary(string name, int index, int globalChangeCounter)
|
||||||
{
|
{
|
||||||
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
|
var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter);
|
||||||
_collectionsByLocal[CurrentCollectionId] = newCollection;
|
_collectionsByLocal[CurrentCollectionId] = newCollection;
|
||||||
CurrentCollectionId += 1;
|
CurrentCollectionId += 1;
|
||||||
return newCollection;
|
return newCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Delete(ModCollection collection)
|
public void Delete(ModCollection collection)
|
||||||
=> _collectionsByLocal.Remove(collection.Identity.LocalId);
|
=> _collectionsByLocal.Remove(collection.Identity.LocalId);
|
||||||
|
|
||||||
/// <remarks> The empty collection is always available at Index 0. </remarks>
|
/// <remarks> The empty collection is always available at Index 0. </remarks>
|
||||||
private readonly List<ModCollection> _collections =
|
private readonly List<ModCollection> _collections =
|
||||||
[
|
[
|
||||||
ModCollection.Empty,
|
ModCollection.Empty,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
|
/// <remarks> A list of all collections ever created still existing by their local id. </remarks>
|
||||||
private readonly Dictionary<LocalCollectionId, ModCollection>
|
private readonly Dictionary<LocalCollectionId, ModCollection>
|
||||||
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
|
_collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty };
|
||||||
|
|
||||||
|
|
||||||
public readonly ModCollection DefaultNamed;
|
public readonly ModCollection DefaultNamed;
|
||||||
|
|
||||||
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
|
/// <remarks> Incremented by 1 because the empty collection gets Zero. </remarks>
|
||||||
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
|
public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1;
|
||||||
|
|
||||||
/// <summary> Default enumeration skips the empty collection. </summary>
|
/// <summary> Default enumeration skips the empty collection. </summary>
|
||||||
public IEnumerator<ModCollection> GetEnumerator()
|
public IEnumerator<ModCollection> GetEnumerator()
|
||||||
=> _collections.Skip(1).GetEnumerator();
|
=> _collections.Skip(1).GetEnumerator();
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
=> GetEnumerator();
|
=> GetEnumerator();
|
||||||
|
|
||||||
public int Count
|
public int Count
|
||||||
=> _collections.Count;
|
=> _collections.Count;
|
||||||
|
|
||||||
public ModCollection this[int index]
|
public ModCollection this[int index]
|
||||||
=> _collections[index];
|
=> _collections[index];
|
||||||
|
|
||||||
/// <summary> Find a collection by its name. If the name is empty or None, the empty collection is returned. </summary>
|
/// <summary> Find a collection by its name. If the name is empty or None, the empty collection is returned. </summary>
|
||||||
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
||||||
{
|
{
|
||||||
if (name.Length != 0)
|
if (name.Length != 0)
|
||||||
return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
|
return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
|
||||||
|
|
||||||
collection = ModCollection.Empty;
|
collection = ModCollection.Empty;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Find a collection by its id. If the GUID is empty, the empty collection is returned. </summary>
|
/// <summary> Find a collection by its id. If the GUID is empty, the empty collection is returned. </summary>
|
||||||
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
|
public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection)
|
||||||
{
|
{
|
||||||
if (id != Guid.Empty)
|
if (id != Guid.Empty)
|
||||||
return _collections.FindFirst(c => c.Identity.Id == id, out collection);
|
return _collections.FindFirst(c => c.Identity.Id == id, out collection);
|
||||||
|
|
||||||
collection = ModCollection.Empty;
|
collection = ModCollection.Empty;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. </summary>
|
/// <summary> Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. </summary>
|
||||||
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
|
public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection)
|
||||||
{
|
{
|
||||||
if (Guid.TryParse(identifier, out var guid))
|
if (Guid.TryParse(identifier, out var guid))
|
||||||
return ById(guid, out collection);
|
return ById(guid, out collection);
|
||||||
|
|
||||||
return ByName(identifier, out collection);
|
return ByName(identifier, out collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary>
|
/// <summary> Find a collection by its local ID if it still exists, otherwise returns the empty collection. </summary>
|
||||||
public ModCollection ByLocalId(LocalCollectionId localId)
|
public ModCollection ByLocalId(LocalCollectionId localId)
|
||||||
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
|
=> _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty;
|
||||||
|
|
||||||
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
|
public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage)
|
||||||
{
|
{
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_saveService = saveService;
|
_saveService = saveService;
|
||||||
_modStorage = modStorage;
|
_modStorage = modStorage;
|
||||||
_communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage);
|
_communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage);
|
||||||
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage);
|
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage);
|
||||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage);
|
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage);
|
||||||
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage);
|
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage);
|
||||||
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage);
|
_communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage);
|
||||||
ReadCollections(out DefaultNamed);
|
ReadCollections(out DefaultNamed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted);
|
_communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted);
|
||||||
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
|
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
|
||||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||||
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
|
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
|
||||||
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
|
_communicator.ModFileChanged.Unsubscribe(OnModFileChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new collection of the given name.
|
/// Add a new collection of the given name.
|
||||||
/// If duplicate is not-null, the new collection will be a duplicate of it.
|
/// If duplicate is not-null, the new collection will be a duplicate of it.
|
||||||
/// If the name of the collection would result in an already existing filename, skip it.
|
/// If the name of the collection would result in an already existing filename, skip it.
|
||||||
/// Returns true if the collection was successfully created and fires a Inactive event.
|
/// Returns true if the collection was successfully created and fires a Inactive event.
|
||||||
/// Also sets the current collection to the new collection afterwards.
|
/// Also sets the current collection to the new collection afterwards.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AddCollection(string name, ModCollection? duplicate)
|
public bool AddCollection(string name, ModCollection? duplicate)
|
||||||
{
|
{
|
||||||
if (name.Length == 0)
|
if (name.Length == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var newCollection = Create(name, _collections.Count, duplicate);
|
var newCollection = Create(name, _collections.Count, duplicate);
|
||||||
_collections.Add(newCollection);
|
_collections.Add(newCollection);
|
||||||
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
|
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
|
||||||
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false);
|
Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success,
|
||||||
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty));
|
false);
|
||||||
return true;
|
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty));
|
||||||
}
|
return true;
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Remove the given collection if it exists and is neither the empty nor the default-named collection.
|
/// <summary> Rename a collection. </summary>
|
||||||
/// </summary>
|
/// <param name="collection"> The collection to rename. </param>
|
||||||
public bool RemoveCollection(ModCollection collection)
|
/// <param name="newName"> The new name for the collection. </param>
|
||||||
{
|
/// <returns> True if a change has taken place. </returns>
|
||||||
if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
|
public bool RenameCollection(ModCollection collection, string newName)
|
||||||
{
|
{
|
||||||
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
|
var oldName = collection.Identity.Name;
|
||||||
return false;
|
if (newName == oldName)
|
||||||
}
|
return false;
|
||||||
|
|
||||||
if (collection.Identity.Index == DefaultNamed.Identity.Index)
|
collection.Identity.Name = newName;
|
||||||
{
|
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
||||||
Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false);
|
_communicator.CollectionRename.Invoke(new CollectionRename.Arguments(collection, oldName, newName));
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Delete(collection);
|
/// <summary>
|
||||||
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
|
/// Remove the given collection if it exists and is neither the empty nor the default-named collection.
|
||||||
_collections.RemoveAt(collection.Identity.Index);
|
/// </summary>
|
||||||
// Update indices.
|
public bool RemoveCollection(ModCollection collection)
|
||||||
for (var i = collection.Identity.Index; i < Count; ++i)
|
{
|
||||||
_collections[i].Identity.Index = i;
|
if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count)
|
||||||
_collectionsByLocal.Remove(collection.Identity.LocalId);
|
{
|
||||||
|
Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false);
|
||||||
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
|
return false;
|
||||||
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty));
|
}
|
||||||
return true;
|
|
||||||
}
|
if (collection.Identity.Index == DefaultNamed.Identity.Index)
|
||||||
|
{
|
||||||
/// <summary> Remove all settings for not currently-installed mods from the given collection. </summary>
|
Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false);
|
||||||
public int CleanUnavailableSettings(ModCollection collection)
|
return false;
|
||||||
{
|
}
|
||||||
var count = collection.Settings.Unused.Count;
|
|
||||||
if (count > 0)
|
Delete(collection);
|
||||||
{
|
_saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection));
|
||||||
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear();
|
_collections.RemoveAt(collection.Identity.Index);
|
||||||
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
// Update indices.
|
||||||
}
|
for (var i = collection.Identity.Index; i < Count; ++i)
|
||||||
|
_collections[i].Identity.Index = i;
|
||||||
return count;
|
_collectionsByLocal.Remove(collection.Identity.LocalId);
|
||||||
}
|
|
||||||
|
Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false);
|
||||||
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
|
_communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty));
|
||||||
public void CleanUnavailableSetting(ModCollection collection, string? setting)
|
return true;
|
||||||
{
|
}
|
||||||
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
|
|
||||||
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
/// <summary> Remove all settings for not currently-installed mods from the given collection. </summary>
|
||||||
}
|
public int CleanUnavailableSettings(ModCollection collection)
|
||||||
|
{
|
||||||
/// <summary>
|
var count = collection.Settings.Unused.Count;
|
||||||
/// Read all collection files in the Collection Directory.
|
if (count > 0)
|
||||||
/// Ensure that the default named collection exists, and apply inheritances afterward.
|
{
|
||||||
/// Duplicate collection files are not deleted, just not added here.
|
((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Clear();
|
||||||
/// </summary>
|
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
||||||
private void ReadCollections(out ModCollection defaultNamedCollection)
|
}
|
||||||
{
|
|
||||||
Penumbra.Log.Debug("[Collections] Reading saved collections...");
|
return count;
|
||||||
foreach (var file in _saveService.FileNames.CollectionFiles)
|
}
|
||||||
{
|
|
||||||
if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance))
|
/// <summary> Remove a specific setting for not currently-installed mods from the given collection. </summary>
|
||||||
continue;
|
public void CleanUnavailableSetting(ModCollection collection, string? setting)
|
||||||
|
{
|
||||||
if (id == Guid.Empty)
|
if (setting != null && ((Dictionary<string, ModSettings.SavedSettings>)collection.Settings.Unused).Remove(setting))
|
||||||
{
|
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
||||||
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
|
}
|
||||||
continue;
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// Read all collection files in the Collection Directory.
|
||||||
if (ById(id, out _))
|
/// Ensure that the default named collection exists, and apply inheritances afterward.
|
||||||
{
|
/// Duplicate collection files are not deleted, just not added here.
|
||||||
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
|
/// </summary>
|
||||||
NotificationType.Warning);
|
private void ReadCollections(out ModCollection defaultNamedCollection)
|
||||||
continue;
|
{
|
||||||
}
|
Penumbra.Log.Debug("[Collections] Reading saved collections...");
|
||||||
|
foreach (var file in _saveService.FileNames.CollectionFiles)
|
||||||
var collection = CreateFromData(id, name, version, settings, inheritance);
|
{
|
||||||
var correctName = _saveService.FileNames.CollectionFile(collection);
|
if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance))
|
||||||
if (file.FullName != correctName)
|
continue;
|
||||||
try
|
|
||||||
{
|
if (id == Guid.Empty)
|
||||||
if (version >= 2)
|
{
|
||||||
{
|
Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning);
|
||||||
try
|
continue;
|
||||||
{
|
}
|
||||||
File.Move(file.FullName, correctName, false);
|
|
||||||
Penumbra.Messager.NotificationMessage(
|
if (ById(id, out _))
|
||||||
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
|
{
|
||||||
NotificationType.Warning);
|
Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.",
|
||||||
}
|
NotificationType.Warning);
|
||||||
catch (Exception ex)
|
continue;
|
||||||
{
|
}
|
||||||
Penumbra.Messager.NotificationMessage(
|
|
||||||
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
|
var collection = CreateFromData(id, name, version, settings, inheritance);
|
||||||
NotificationType.Warning);
|
var correctName = _saveService.FileNames.CollectionFile(collection);
|
||||||
}
|
if (file.FullName != correctName)
|
||||||
}
|
try
|
||||||
else
|
{
|
||||||
{
|
if (version >= 2)
|
||||||
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.Move(file.FullName, file.FullName + ".bak", true);
|
File.Move(file.FullName, correctName, false);
|
||||||
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file.");
|
Penumbra.Messager.NotificationMessage(
|
||||||
}
|
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.",
|
||||||
catch (Exception ex)
|
NotificationType.Warning);
|
||||||
{
|
}
|
||||||
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}");
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
}
|
Penumbra.Messager.NotificationMessage(
|
||||||
}
|
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}",
|
||||||
catch (Exception e)
|
NotificationType.Warning);
|
||||||
{
|
}
|
||||||
Penumbra.Messager.NotificationMessage(e,
|
}
|
||||||
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
|
else
|
||||||
NotificationType.Error);
|
{
|
||||||
}
|
_saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection));
|
||||||
|
try
|
||||||
_collections.Add(collection);
|
{
|
||||||
}
|
File.Move(file.FullName, file.FullName + ".bak", true);
|
||||||
|
Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file.");
|
||||||
defaultNamedCollection = SetDefaultNamedCollection();
|
}
|
||||||
Penumbra.Log.Debug($"[Collections] Found {Count} saved collections.");
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
|
Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}");
|
||||||
/// <summary>
|
}
|
||||||
/// Add the collection with the default name if it does not exist.
|
}
|
||||||
/// It should always be ensured that it exists, otherwise it will be created.
|
}
|
||||||
/// This can also not be deleted, so there are always at least the empty and a collection with default name.
|
catch (Exception e)
|
||||||
/// </summary>
|
{
|
||||||
private ModCollection SetDefaultNamedCollection()
|
Penumbra.Messager.NotificationMessage(e,
|
||||||
{
|
$"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.",
|
||||||
if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
|
NotificationType.Error);
|
||||||
return collection;
|
}
|
||||||
|
|
||||||
if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
|
_collections.Add(collection);
|
||||||
return _collections[^1];
|
}
|
||||||
|
|
||||||
Penumbra.Messager.NotificationMessage(
|
defaultNamedCollection = SetDefaultNamedCollection();
|
||||||
$"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.",
|
Penumbra.Log.Debug($"[Collections] Found {Count} saved collections.");
|
||||||
NotificationType.Error);
|
}
|
||||||
return Count > 1 ? _collections[1] : _collections[0];
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// Add the collection with the default name if it does not exist.
|
||||||
/// <summary> Move all settings in all collections to unused settings. </summary>
|
/// It should always be ensured that it exists, otherwise it will be created.
|
||||||
private void OnModDiscoveryStarted()
|
/// This can also not be deleted, so there are always at least the empty and a collection with default name.
|
||||||
{
|
/// </summary>
|
||||||
foreach (var collection in this)
|
private ModCollection SetDefaultNamedCollection()
|
||||||
collection.Settings.PrepareModDiscovery(_modStorage);
|
{
|
||||||
}
|
if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection))
|
||||||
|
return collection;
|
||||||
/// <summary> Restore all settings in all collections to mods. </summary>
|
|
||||||
private void OnModDiscoveryFinished()
|
if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null))
|
||||||
{
|
return _collections[^1];
|
||||||
// Re-apply all mod settings.
|
|
||||||
foreach (var collection in this)
|
Penumbra.Messager.NotificationMessage(
|
||||||
collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
|
$"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.",
|
||||||
}
|
NotificationType.Error);
|
||||||
|
return Count > 1 ? _collections[1] : _collections[0];
|
||||||
/// <summary> Add or remove a mod from all collections, or re-save all collections where the mod has settings. </summary>
|
}
|
||||||
private void OnModPathChange(in ModPathChanged.Arguments arguments)
|
|
||||||
{
|
/// <summary> Move all settings in all collections to unused settings. </summary>
|
||||||
switch (arguments.Type)
|
private void OnModDiscoveryStarted()
|
||||||
{
|
{
|
||||||
case ModPathChangeType.Added:
|
foreach (var collection in this)
|
||||||
foreach (var collection in this)
|
collection.Settings.PrepareModDiscovery(_modStorage);
|
||||||
collection.Settings.AddMod(arguments.Mod);
|
}
|
||||||
break;
|
|
||||||
case ModPathChangeType.Deleted:
|
/// <summary> Restore all settings in all collections to mods. </summary>
|
||||||
foreach (var collection in this)
|
private void OnModDiscoveryFinished()
|
||||||
collection.Settings.RemoveMod(arguments.Mod);
|
{
|
||||||
break;
|
// Re-apply all mod settings.
|
||||||
case ModPathChangeType.Moved:
|
foreach (var collection in this)
|
||||||
var index = arguments.Mod.Index;
|
collection.Settings.ApplyModSettings(collection, _saveService, _modStorage);
|
||||||
foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null))
|
}
|
||||||
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
|
||||||
break;
|
/// <summary> Add or remove a mod from all collections, or re-save all collections where the mod has settings. </summary>
|
||||||
case ModPathChangeType.Reloaded:
|
private void OnModPathChange(in ModPathChanged.Arguments arguments)
|
||||||
foreach (var collection in this)
|
{
|
||||||
{
|
switch (arguments.Type)
|
||||||
if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false)
|
{
|
||||||
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
case ModPathChangeType.Added:
|
||||||
collection.Settings.SetTemporary(arguments.Mod.Index, null);
|
foreach (var collection in this)
|
||||||
}
|
collection.Settings.AddMod(arguments.Mod);
|
||||||
|
break;
|
||||||
break;
|
case ModPathChangeType.Deleted:
|
||||||
}
|
foreach (var collection in this)
|
||||||
}
|
collection.Settings.RemoveMod(arguments.Mod);
|
||||||
|
break;
|
||||||
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
|
case ModPathChangeType.Moved:
|
||||||
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
|
var index = arguments.Mod.Index;
|
||||||
{
|
foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null))
|
||||||
arguments.Type.HandlingInfo(out var requiresSaving, out _, out _);
|
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
||||||
if (!requiresSaving)
|
break;
|
||||||
return;
|
case ModPathChangeType.Reloaded:
|
||||||
|
foreach (var collection in this)
|
||||||
foreach (var collection in this)
|
{
|
||||||
{
|
if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false)
|
||||||
if (collection.GetOwnSettings(arguments.Mod.Index)?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex) ?? false)
|
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
||||||
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
collection.Settings.SetTemporary(arguments.Mod.Index, null);
|
||||||
collection.Settings.SetTemporary(arguments.Mod.Index, null);
|
}
|
||||||
}
|
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
/// <summary> Update change counters when changing files. </summary>
|
}
|
||||||
private void OnModFileChanged(in ModFileChanged.Arguments arguments)
|
|
||||||
{
|
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
|
||||||
if (arguments.File.CurrentUsage == 0)
|
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
|
||||||
return;
|
{
|
||||||
|
arguments.Type.HandlingInfo(out var requiresSaving, out _, out _);
|
||||||
foreach (var collection in this)
|
if (!requiresSaving)
|
||||||
{
|
return;
|
||||||
var (settings, _) = collection.GetActualSettings(arguments.Mod.Index);
|
|
||||||
if (settings is { Enabled: true })
|
foreach (var collection in this)
|
||||||
collection.Counters.IncrementChange();
|
{
|
||||||
}
|
if (collection.GetOwnSettings(arguments.Mod.Index)
|
||||||
}
|
?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex)
|
||||||
}
|
?? false)
|
||||||
|
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
|
||||||
|
collection.Settings.SetTemporary(arguments.Mod.Index, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Update change counters when changing files. </summary>
|
||||||
|
private void OnModFileChanged(in ModFileChanged.Arguments arguments)
|
||||||
|
{
|
||||||
|
if (arguments.File.CurrentUsage == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var collection in this)
|
||||||
|
{
|
||||||
|
var (settings, _) = collection.GetActualSettings(arguments.Mod.Index);
|
||||||
|
if (settings is { Enabled: true })
|
||||||
|
collection.Counters.IncrementChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ public sealed class CollectionChange(Logger log)
|
||||||
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" />
|
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnCollectionChange" />
|
||||||
ItemSwapTab = 0,
|
ItemSwapTab = 0,
|
||||||
|
|
||||||
/// <seealso cref="UI.CollectionTab.CollectionSelector.OnCollectionChange" />
|
/// <seealso cref="UI.CollectionTab.CollectionSelector.Cache.OnCollectionChange" />
|
||||||
CollectionSelector = 0,
|
CollectionSelectorCache = 0,
|
||||||
|
|
||||||
/// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/>
|
/// <seealso cref="UI.CollectionTab.IndividualAssignmentUi.UpdateIdentifiers"/>
|
||||||
IndividualAssignmentUi = 0,
|
IndividualAssignmentUi = 0,
|
||||||
|
|
|
||||||
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 ModSelectorMinimumScale { get; set; } = 0.1f;
|
||||||
public float ModSelectorMaximumScale { get; set; } = 0.5f;
|
public float ModSelectorMaximumScale { get; set; } = 0.5f;
|
||||||
|
|
||||||
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
||||||
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
|
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
|
||||||
public bool DebugSeparateWindow { get; set; } = false;
|
public bool DebugSeparateWindow { get; set; } = false;
|
||||||
public int TutorialStep { get; set; } = 0;
|
public int TutorialStep { get; set; } = 0;
|
||||||
public bool EnableResourceLogging { get; set; } = false;
|
public bool EnableResourceLogging { get; set; } = false;
|
||||||
public string ResourceLoggingFilter { get; set; } = string.Empty;
|
public string ResourceLoggingFilter { get; set; } = string.Empty;
|
||||||
public bool EnableResourceWatcher { get; set; } = false;
|
public bool EnableResourceWatcher { get; set; } = false;
|
||||||
public bool OnlyAddMatchingResources { get; set; } = true;
|
public bool OnlyAddMatchingResources { get; set; } = true;
|
||||||
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
|
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
|
||||||
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
|
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
|
||||||
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
|
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
|
||||||
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
|
public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment;
|
||||||
public TabType SelectedTab { get; set; } = TabType.Settings;
|
public TabType SelectedTab { get; set; } = TabType.Settings;
|
||||||
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
|
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
|
||||||
public bool FixMainWindow { get; set; } = false;
|
public bool FixMainWindow { get; set; } = false;
|
||||||
public string LastModPath { get; set; } = string.Empty;
|
public string LastModPath { get; set; } = string.Empty;
|
||||||
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = [];
|
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = [];
|
||||||
public bool ForceRedrawOnFileChange { get; set; } = false;
|
public bool ForceRedrawOnFileChange { get; set; } = false;
|
||||||
public bool IncognitoMode { get; set; } = false;
|
public bool IncognitoMode { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load the current configuration.
|
/// Load the current configuration.
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ public class ModEditor(
|
||||||
SwapEditor.Revert(Option!);
|
SwapEditor.Revert(Option!);
|
||||||
MetaEditor.Load(Mod!, Option!);
|
MetaEditor.Load(Mod!, Option!);
|
||||||
Duplicates.Clear();
|
Duplicates.Clear();
|
||||||
MdlMaterialEditor.ScanModels(Mod!);
|
MdlMaterialEditor.ScanModels(Mod!);
|
||||||
|
OptionLoaded?.Invoke();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,21 +81,22 @@ public class ModEditor(
|
||||||
Files.UpdatePaths(Mod!, Option!);
|
Files.UpdatePaths(Mod!, Option!);
|
||||||
MetaEditor.Load(Mod!, Option!);
|
MetaEditor.Load(Mod!, Option!);
|
||||||
FileEditor.Clear();
|
FileEditor.Clear();
|
||||||
Duplicates.Clear();
|
Duplicates.Clear();
|
||||||
|
OptionLoaded?.Invoke();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
|
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
|
||||||
private void LoadOption(int groupIdx, int dataIdx, bool message)
|
private void LoadOption(int groupIdx, int dataIdx, bool message)
|
||||||
{
|
{
|
||||||
if (Mod != null && Mod.Groups.Count > groupIdx)
|
if (Mod is not null && Mod.Groups.Count > groupIdx)
|
||||||
{
|
{
|
||||||
if (groupIdx == -1 && dataIdx == 0)
|
if (groupIdx == -1 && dataIdx == 0)
|
||||||
{
|
{
|
||||||
Group = null;
|
Group = null;
|
||||||
Option = Mod.Default;
|
Option = Mod.Default;
|
||||||
GroupIdx = groupIdx;
|
GroupIdx = groupIdx;
|
||||||
DataIdx = dataIdx;
|
DataIdx = dataIdx;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +107,7 @@ public class ModEditor(
|
||||||
{
|
{
|
||||||
Option = Group.DataContainers[dataIdx];
|
Option = Group.DataContainers[dataIdx];
|
||||||
GroupIdx = groupIdx;
|
GroupIdx = groupIdx;
|
||||||
DataIdx = dataIdx;
|
DataIdx = dataIdx;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +121,8 @@ public class ModEditor(
|
||||||
Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}.");
|
Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public event Action? OptionLoaded;
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
Duplicates.Clear();
|
Duplicates.Clear();
|
||||||
|
|
@ -126,7 +130,8 @@ public class ModEditor(
|
||||||
Files.Clear();
|
Files.Clear();
|
||||||
MetaEditor.Clear();
|
MetaEditor.Clear();
|
||||||
Mod = null;
|
Mod = null;
|
||||||
LoadOption(0, 0, false);
|
LoadOption(0, 0, false);
|
||||||
|
OptionLoaded?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -146,7 +151,7 @@ public class ModEditor(
|
||||||
foreach (var subDir in baseDir.GetDirectories())
|
foreach (var subDir in baseDir.GetDirectories())
|
||||||
{
|
{
|
||||||
ClearEmptySubDirectories(subDir);
|
ClearEmptySubDirectories(subDir);
|
||||||
if (subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0)
|
if (subDir.GetFiles().Length is 0 && subDir.GetDirectories().Length is 0)
|
||||||
subDir.Delete();
|
subDir.Delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,208 +1,197 @@
|
||||||
using Luna;
|
using ImSharp.Containers;
|
||||||
using Penumbra.Mods.SubMods;
|
using Luna;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.Mods.SubMods;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
namespace Penumbra.Mods.Editor;
|
|
||||||
|
namespace Penumbra.Mods.Editor;
|
||||||
public class ModFileCollection : IDisposable
|
|
||||||
{
|
public class ModFileCollection : IDisposable
|
||||||
private readonly List<FileRegistry> _available = [];
|
{
|
||||||
private readonly List<FileRegistry> _mtrl = [];
|
private readonly ObservableList<FileRegistry> _available = [];
|
||||||
private readonly List<FileRegistry> _mdl = [];
|
private readonly ObservableList<FileRegistry> _mtrl = [];
|
||||||
private readonly List<FileRegistry> _tex = [];
|
private readonly ObservableList<FileRegistry> _mdl = [];
|
||||||
private readonly List<FileRegistry> _shpk = [];
|
private readonly ObservableList<FileRegistry> _tex = [];
|
||||||
private readonly List<FileRegistry> _pbd = [];
|
private readonly ObservableList<FileRegistry> _shpk = [];
|
||||||
private readonly List<FileRegistry> _atch = [];
|
private readonly ObservableList<FileRegistry> _pbd = [];
|
||||||
|
private readonly ObservableList<FileRegistry> _atch = [];
|
||||||
private readonly SortedSet<FullPath> _missing = [];
|
|
||||||
private readonly HashSet<Utf8GamePath> _usedPaths = [];
|
private readonly SortedSet<FullPath> _missing = [];
|
||||||
|
private readonly HashSet<Utf8GamePath> _usedPaths = [];
|
||||||
public IReadOnlySet<FullPath> Missing
|
|
||||||
=> Ready ? _missing : [];
|
public IReadOnlySet<FullPath> Missing
|
||||||
|
=> Ready ? _missing : [];
|
||||||
public IReadOnlySet<Utf8GamePath> UsedPaths
|
|
||||||
=> Ready ? _usedPaths : [];
|
public IReadOnlySet<Utf8GamePath> UsedPaths
|
||||||
|
=> Ready ? _usedPaths : [];
|
||||||
public IReadOnlyList<FileRegistry> Available
|
|
||||||
=> Ready ? _available : [];
|
public IObservableList<FileRegistry> Available
|
||||||
|
=> Ready ? _available : [];
|
||||||
public IReadOnlyList<FileRegistry> Mtrl
|
|
||||||
=> Ready ? _mtrl : [];
|
public IObservableList<FileRegistry> Mtrl
|
||||||
|
=> Ready ? _mtrl : [];
|
||||||
public IReadOnlyList<FileRegistry> Mdl
|
|
||||||
=> Ready ? _mdl : [];
|
public IObservableList<FileRegistry> Mdl
|
||||||
|
=> Ready ? _mdl : [];
|
||||||
public IReadOnlyList<FileRegistry> Tex
|
|
||||||
=> Ready ? _tex : [];
|
public IObservableList<FileRegistry> Tex
|
||||||
|
=> Ready ? _tex : [];
|
||||||
public IReadOnlyList<FileRegistry> Shpk
|
|
||||||
=> Ready ? _shpk : [];
|
public IObservableList<FileRegistry> Shpk
|
||||||
|
=> Ready ? _shpk : [];
|
||||||
public IReadOnlyList<FileRegistry> Pbd
|
|
||||||
=> Ready ? _pbd : [];
|
public IObservableList<FileRegistry> Pbd
|
||||||
|
=> Ready ? _pbd : [];
|
||||||
public IReadOnlyList<FileRegistry> Atch
|
|
||||||
=> Ready ? _atch : [];
|
public IObservableList<FileRegistry> Atch
|
||||||
|
=> Ready ? _atch : [];
|
||||||
public bool Ready { get; private set; } = true;
|
|
||||||
|
public bool Ready { get; private set; } = true;
|
||||||
public void UpdateAll(Mod mod, IModDataContainer option)
|
|
||||||
{
|
public void UpdateAll(Mod mod, IModDataContainer option)
|
||||||
UpdateFiles(mod, CancellationToken.None);
|
{
|
||||||
UpdatePaths(mod, option, false, CancellationToken.None);
|
UpdateFiles(mod, CancellationToken.None);
|
||||||
}
|
UpdatePaths(mod, option, false, CancellationToken.None);
|
||||||
|
}
|
||||||
public void UpdatePaths(Mod mod, IModDataContainer option)
|
|
||||||
=> UpdatePaths(mod, option, true, CancellationToken.None);
|
public void UpdatePaths(Mod mod, IModDataContainer option)
|
||||||
|
=> UpdatePaths(mod, option, true, CancellationToken.None);
|
||||||
public void Clear()
|
|
||||||
{
|
public void Clear()
|
||||||
ClearFiles();
|
{
|
||||||
ClearPaths(false, CancellationToken.None);
|
ClearFiles();
|
||||||
}
|
ClearPaths(false, CancellationToken.None);
|
||||||
|
}
|
||||||
public void Dispose()
|
|
||||||
=> Clear();
|
public void Dispose()
|
||||||
|
=> Clear();
|
||||||
public void ClearMissingFiles()
|
|
||||||
=> _missing.Clear();
|
public void ClearMissingFiles()
|
||||||
|
=> _missing.Clear();
|
||||||
public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
|
|
||||||
{
|
public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
|
||||||
_usedPaths.Remove(gamePath);
|
{
|
||||||
if (file != null)
|
_usedPaths.Remove(gamePath);
|
||||||
{
|
if (file != null)
|
||||||
--file.CurrentUsage;
|
{
|
||||||
file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath));
|
--file.CurrentUsage;
|
||||||
}
|
file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
|
|
||||||
=> RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
|
public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
|
||||||
|
=> RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
|
||||||
public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
|
|
||||||
{
|
public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
|
||||||
_usedPaths.Add(gamePath);
|
{
|
||||||
if (file == null)
|
_usedPaths.Add(gamePath);
|
||||||
return;
|
if (file == null)
|
||||||
|
return;
|
||||||
++file.CurrentUsage;
|
|
||||||
file.SubModUsage.Add((option, gamePath));
|
++file.CurrentUsage;
|
||||||
}
|
file.SubModUsage.Add((option, gamePath));
|
||||||
|
}
|
||||||
public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
|
|
||||||
=> AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
|
public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
|
||||||
|
=> AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
|
||||||
public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
|
|
||||||
{
|
public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
|
||||||
var oldPath = file.SubModUsage[pathIdx];
|
{
|
||||||
_usedPaths.Remove(oldPath.Item2);
|
var oldPath = file.SubModUsage[pathIdx];
|
||||||
if (!gamePath.IsEmpty)
|
_usedPaths.Remove(oldPath.Item2);
|
||||||
{
|
if (!gamePath.IsEmpty)
|
||||||
file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath);
|
{
|
||||||
_usedPaths.Add(gamePath);
|
file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath);
|
||||||
}
|
_usedPaths.Add(gamePath);
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
--file.CurrentUsage;
|
{
|
||||||
file.SubModUsage.RemoveAt(pathIdx);
|
--file.CurrentUsage;
|
||||||
}
|
file.SubModUsage.RemoveAt(pathIdx);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private void UpdateFiles(Mod mod, CancellationToken tok)
|
|
||||||
{
|
private void UpdateFiles(Mod mod, CancellationToken tok)
|
||||||
tok.ThrowIfCancellationRequested();
|
{
|
||||||
ClearFiles();
|
tok.ThrowIfCancellationRequested();
|
||||||
|
ClearFiles();
|
||||||
foreach (var file in mod.ModPath.EnumerateDirectories().Where(d => !d.IsHidden()).SelectMany(FileExtensions.EnumerateNonHiddenFiles))
|
|
||||||
{
|
foreach (var file in mod.ModPath.EnumerateDirectories().Where(d => !d.IsHidden()).SelectMany(FileExtensions.EnumerateNonHiddenFiles))
|
||||||
tok.ThrowIfCancellationRequested();
|
{
|
||||||
if (!FileRegistry.FromFile(mod.ModPath, file, out var registry))
|
tok.ThrowIfCancellationRequested();
|
||||||
continue;
|
if (!FileRegistry.FromFile(mod.ModPath, file, out var registry))
|
||||||
|
continue;
|
||||||
_available.Add(registry);
|
|
||||||
switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant())
|
_available.Add(registry);
|
||||||
{
|
switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant())
|
||||||
case ".mtrl":
|
{
|
||||||
_mtrl.Add(registry);
|
case ".mtrl": _mtrl.Add(registry); break;
|
||||||
break;
|
case ".mdl": _mdl.Add(registry); break;
|
||||||
case ".mdl":
|
case ".tex": _tex.Add(registry); break;
|
||||||
_mdl.Add(registry);
|
case ".shpk": _shpk.Add(registry); break;
|
||||||
break;
|
case ".pbd": _pbd.Add(registry); break;
|
||||||
case ".tex":
|
case ".atch": _atch.Add(registry); break;
|
||||||
_tex.Add(registry);
|
}
|
||||||
break;
|
}
|
||||||
case ".shpk":
|
}
|
||||||
_shpk.Add(registry);
|
|
||||||
break;
|
private void ClearFiles()
|
||||||
case ".pbd":
|
{
|
||||||
_pbd.Add(registry);
|
_available.Clear();
|
||||||
break;
|
_mtrl.Clear();
|
||||||
case ".atch":
|
_mdl.Clear();
|
||||||
_atch.Add(registry);
|
_tex.Clear();
|
||||||
break;
|
_shpk.Clear();
|
||||||
}
|
_pbd.Clear();
|
||||||
}
|
_atch.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearFiles()
|
private void ClearPaths(bool clearRegistries, CancellationToken tok)
|
||||||
{
|
{
|
||||||
_available.Clear();
|
if (clearRegistries)
|
||||||
_mtrl.Clear();
|
foreach (var reg in _available)
|
||||||
_mdl.Clear();
|
{
|
||||||
_tex.Clear();
|
tok.ThrowIfCancellationRequested();
|
||||||
_shpk.Clear();
|
reg.CurrentUsage = 0;
|
||||||
_pbd.Clear();
|
reg.SubModUsage.Clear();
|
||||||
_atch.Clear();
|
}
|
||||||
}
|
|
||||||
|
_missing.Clear();
|
||||||
private void ClearPaths(bool clearRegistries, CancellationToken tok)
|
_usedPaths.Clear();
|
||||||
{
|
}
|
||||||
if (clearRegistries)
|
|
||||||
foreach (var reg in _available)
|
private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok)
|
||||||
{
|
{
|
||||||
tok.ThrowIfCancellationRequested();
|
tok.ThrowIfCancellationRequested();
|
||||||
reg.CurrentUsage = 0;
|
ClearPaths(clearRegistries, tok);
|
||||||
reg.SubModUsage.Clear();
|
|
||||||
}
|
tok.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
_missing.Clear();
|
foreach (var subMod in mod.AllDataContainers)
|
||||||
_usedPaths.Clear();
|
{
|
||||||
}
|
foreach (var (gamePath, file) in subMod.Files)
|
||||||
|
{
|
||||||
private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok)
|
tok.ThrowIfCancellationRequested();
|
||||||
{
|
if (!file.Exists)
|
||||||
tok.ThrowIfCancellationRequested();
|
{
|
||||||
ClearPaths(clearRegistries, tok);
|
_missing.Add(file);
|
||||||
|
if (subMod == option)
|
||||||
tok.ThrowIfCancellationRequested();
|
_usedPaths.Add(gamePath);
|
||||||
|
}
|
||||||
foreach (var subMod in mod.AllDataContainers)
|
else
|
||||||
{
|
{
|
||||||
foreach (var (gamePath, file) in subMod.Files)
|
var registry = _available.Find(x => x.File.Equals(file));
|
||||||
{
|
if (registry == null)
|
||||||
tok.ThrowIfCancellationRequested();
|
continue;
|
||||||
if (!file.Exists)
|
|
||||||
{
|
if (subMod == option)
|
||||||
_missing.Add(file);
|
{
|
||||||
if (subMod == option)
|
++registry.CurrentUsage;
|
||||||
_usedPaths.Add(gamePath);
|
_usedPaths.Add(gamePath);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
registry.SubModUsage.Add((subMod, gamePath));
|
||||||
var registry = _available.Find(x => x.File.Equals(file));
|
}
|
||||||
if (registry == null)
|
}
|
||||||
continue;
|
}
|
||||||
|
}
|
||||||
if (subMod == option)
|
}
|
||||||
{
|
|
||||||
++registry.CurrentUsage;
|
|
||||||
_usedPaths.Add(gamePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.SubModUsage.Add((subMod, gamePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ namespace Penumbra.Services;
|
||||||
|
|
||||||
public class CommunicatorService(ServiceManager services) : IService
|
public class CommunicatorService(ServiceManager services) : IService
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc cref="Communication.CollectionRename"/>
|
||||||
|
public readonly CollectionRename CollectionRename = services.GetService<CollectionRename>();
|
||||||
|
|
||||||
/// <inheritdoc cref="Communication.CollectionChange"/>
|
/// <inheritdoc cref="Communication.CollectionChange"/>
|
||||||
public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>();
|
public readonly CollectionChange CollectionChange = services.GetService<CollectionChange>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu
|
||||||
?? _config.Ephemeral.ResourceWatcherResourceCategories;
|
?? _config.Ephemeral.ResourceWatcherResourceCategories;
|
||||||
_config.Ephemeral.ResourceWatcherRecordTypes =
|
_config.Ephemeral.ResourceWatcherRecordTypes =
|
||||||
_data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
|
_data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
|
||||||
_config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionsTab.PanelMode>() ?? _config.Ephemeral.CollectionPanel;
|
_config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionPanelMode>() ?? _config.Ephemeral.CollectionPanel;
|
||||||
_config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab;
|
_config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab;
|
||||||
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()
|
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()
|
||||||
?? _config.Ephemeral.ChangedItemFilter;
|
?? _config.Ephemeral.ChangedItemFilter;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
|
|
@ -8,121 +7,181 @@ using Penumbra.GameData.Files.StainMapStructs;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.UI.AdvancedWindow.Materials;
|
using Penumbra.UI.AdvancedWindow.Materials;
|
||||||
using FilterComboColors = Penumbra.UI.FilterComboColors;
|
|
||||||
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
|
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
//public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile) : SimpleFilterCombo<StmKeyType>(SimpleFilterType.Text)
|
public sealed record StainTemplate(StringPair Id, Vector4 Diffuse, Vector4 Specular, Vector4 Emissive, int Key, bool Found)
|
||||||
// where TDyePack : unmanaged, IDyePack
|
|
||||||
//{
|
|
||||||
// public override StringU8 DisplayString(in StmKeyType value)
|
|
||||||
// => new($"{value,4}");
|
|
||||||
//
|
|
||||||
// public override string FilterString(in StmKeyType value)
|
|
||||||
// => $"{value,4}";
|
|
||||||
//
|
|
||||||
// public override IEnumerable<StmKeyType> GetBaseItems()
|
|
||||||
// => throw new NotImplementedException();
|
|
||||||
//
|
|
||||||
// protected override bool DrawFilter(float width, FilterComboBaseCache<SimpleCacheItem<StmKeyType>> cache)
|
|
||||||
// {
|
|
||||||
// using var font = Im.Font.PushDefault();
|
|
||||||
// return base.DrawFilter(width, cache);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public bool Draw(Utf8StringHandler<LabelStringHandlerBuffer> label, Utf8StringHandler<HintStringHandlerBuffer> preview, Utf8StringHandler<TextStringHandlerBuffer> tooltip, ref int currentSelection, float previewWidth, float itemHeight,
|
|
||||||
// ComboFlags flags = ComboFlags.None)
|
|
||||||
// {
|
|
||||||
// using var font = Im.Font.PushMono();
|
|
||||||
// using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f))
|
|
||||||
// .PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X);
|
|
||||||
// var spaceSize = Im.Font.Mono.GetCharacterAdvance(' ');
|
|
||||||
// var spaces = (int)(previewWidth / spaceSize) - 1;
|
|
||||||
// return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// protected override bool DrawSelectable(int globalIdx, bool selected)
|
|
||||||
// {
|
|
||||||
// var ret = base.DrawSelectable(globalIdx, selected);
|
|
||||||
// var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key;
|
|
||||||
// if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
|
|
||||||
// return ret;
|
|
||||||
//
|
|
||||||
// Im.Line.Same();
|
|
||||||
//
|
|
||||||
// var frame = new Vector2(Im.Style.TextHeight);
|
|
||||||
// Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame);
|
|
||||||
// Im.Line.Same();
|
|
||||||
// Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame);
|
|
||||||
// Im.Line.Same();
|
|
||||||
// Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame);
|
|
||||||
// return ret;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
public class StainService : Luna.IService
|
|
||||||
{
|
{
|
||||||
public sealed class StainTemplateCombo<TDyePack>(FilterComboColors[] stainCombos, StmFile<TDyePack> stmFile)
|
public Vector4 Diffuse { get; set; } = Diffuse;
|
||||||
: FilterComboCache<StmKeyType>(stmFile.Entries.Keys.Prepend(0), MouseWheelType.None, Penumbra.Log)
|
public Vector4 Specular { get; set; } = Specular;
|
||||||
where TDyePack : unmanaged, IDyePack
|
public Vector4 Emissive { get; set; } = Emissive;
|
||||||
|
public bool Found { get; set; } = Found;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TemplateFilter : TextFilterBase<StainTemplate>
|
||||||
|
{
|
||||||
|
protected override string ToFilterString(in StainTemplate item, int globalIndex)
|
||||||
|
=> item.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StainTemplateCombo<TDyePack> : ImSharp.FilterComboBase<StainTemplate>
|
||||||
|
where TDyePack : unmanaged, IDyePack
|
||||||
|
{
|
||||||
|
private readonly StainService.StainCombo[] _stainCombos;
|
||||||
|
private readonly StmFile<TDyePack> _stmFile;
|
||||||
|
|
||||||
|
private int _currentDyeChannel;
|
||||||
|
private ushort _currentSelection;
|
||||||
|
|
||||||
|
public StainTemplateCombo(StainService.StainCombo[] stainCombos, StmFile<TDyePack> stmFile)
|
||||||
|
: base(new TemplateFilter())
|
||||||
{
|
{
|
||||||
// FIXME There might be a better way to handle that.
|
PreviewAlignment = new Vector2(0.90f, 0.5f);
|
||||||
public int CurrentDyeChannel = 0;
|
_stainCombos = stainCombos;
|
||||||
|
_stmFile = stmFile;
|
||||||
|
ComputeWidth = true;
|
||||||
|
}
|
||||||
|
|
||||||
protected override float GetFilterWidth()
|
protected override FilterComboBaseCache<StainTemplate> CreateCache()
|
||||||
|
=> new Cache(this);
|
||||||
|
|
||||||
|
private sealed class Cache : FilterComboBaseCache<StainTemplate>
|
||||||
|
{
|
||||||
|
private readonly StainTemplateCombo<TDyePack> _parent;
|
||||||
|
private int _dyeChannel;
|
||||||
|
|
||||||
|
public Cache(StainTemplateCombo<TDyePack> parent)
|
||||||
|
: base(parent)
|
||||||
{
|
{
|
||||||
var baseSize = Im.Font.CalculateSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X;
|
_parent = parent;
|
||||||
if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0)
|
foreach (var combo in _parent._stainCombos)
|
||||||
return baseSize;
|
combo.SelectionChanged += OnSelectionChanged;
|
||||||
|
_dyeChannel = _parent._currentDyeChannel;
|
||||||
return baseSize + Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string ToString(StmKeyType obj)
|
public override void Update()
|
||||||
=> $"{obj,4}";
|
|
||||||
|
|
||||||
protected override void DrawFilter(int currentSelected, float width)
|
|
||||||
{
|
{
|
||||||
using var font = Im.Font.PushDefault();
|
base.Update();
|
||||||
base.DrawFilter(currentSelected, width);
|
if (_dyeChannel != _parent._currentDyeChannel)
|
||||||
|
{
|
||||||
|
UpdateItems();
|
||||||
|
ComputeWidth();
|
||||||
|
_dyeChannel = _parent._currentDyeChannel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight,
|
private void OnSelectionChanged(Luna.FilterComboColors.Item obj)
|
||||||
ImGuiComboFlags flags = ImGuiComboFlags.None)
|
|
||||||
{
|
{
|
||||||
using var font = Im.Font.PushMono();
|
UpdateItems();
|
||||||
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f))
|
ComputeWidth();
|
||||||
.PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X);
|
|
||||||
var spaceSize = Im.Font.Mono.GetCharacterAdvance(' ');
|
|
||||||
var spaces = (int)(previewWidth / spaceSize) - 1;
|
|
||||||
return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
private void UpdateItems()
|
||||||
{
|
{
|
||||||
var ret = base.DrawSelectable(globalIdx, selected);
|
foreach (var item in UnfilteredItems)
|
||||||
var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key;
|
{
|
||||||
if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
|
var dye = _parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id;
|
||||||
return ret;
|
if (dye > 0 && _parent._stmFile.TryGetValue(item.Key, dye, out var dyes))
|
||||||
|
{
|
||||||
|
item.Found = true;
|
||||||
|
item.Diffuse = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1);
|
||||||
|
item.Specular = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1);
|
||||||
|
item.Emissive = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item.Found = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Im.Line.Same();
|
|
||||||
|
protected override void ComputeWidth()
|
||||||
var frame = new Vector2(Im.Style.TextHeight);
|
{
|
||||||
Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame);
|
ComboWidth = Im.Font.Mono.CalculateTextSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X;
|
||||||
Im.Line.Same();
|
if (_parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id is 0)
|
||||||
Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame);
|
return;
|
||||||
Im.Line.Same();
|
|
||||||
Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame);
|
ComboWidth += Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3;
|
||||||
return ret;
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
foreach (var combo in _parent._stainCombos)
|
||||||
|
combo.SelectionChanged -= OnSelectionChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Draw(Utf8StringHandler<LabelStringHandlerBuffer> label, ushort currentSelection, int currentDyeChannel,
|
||||||
|
Utf8StringHandler<TextStringHandlerBuffer> tooltip, out int newSelection, float previewWidth, float itemHeight,
|
||||||
|
ComboFlags flags = ComboFlags.None)
|
||||||
|
{
|
||||||
|
Flags = flags;
|
||||||
|
_currentDyeChannel = currentDyeChannel;
|
||||||
|
_currentSelection = currentSelection;
|
||||||
|
using var font = Im.Font.PushMono();
|
||||||
|
if (!base.Draw(label, $"{_currentSelection,4}", tooltip, previewWidth, out var selection))
|
||||||
|
{
|
||||||
|
newSelection = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSelection = selection.Key;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<StainTemplate> GetItems()
|
||||||
|
{
|
||||||
|
var dye = _stainCombos[_currentDyeChannel].CurrentSelection.Id;
|
||||||
|
foreach (var key in _stmFile.Entries.Keys.Prepend(0))
|
||||||
|
{
|
||||||
|
if (dye > 0 && _stmFile.TryGetValue(key, dye, out var dyes))
|
||||||
|
yield return new StainTemplate(new StringPair($"{key,4}"),
|
||||||
|
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1),
|
||||||
|
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1),
|
||||||
|
new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1), key.Int, true);
|
||||||
|
else
|
||||||
|
yield return new StainTemplate(new StringPair($"{key,4}"), Vector4.Zero, Vector4.Zero, Vector4.Zero, key.Int, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override float ItemHeight
|
||||||
|
=> Im.Style.TextHeightWithSpacing;
|
||||||
|
|
||||||
|
protected override bool DrawItem(in StainTemplate item, int globalIndex, bool selected)
|
||||||
|
{
|
||||||
|
var ret = Im.Selectable(item.Id.Utf8, selected);
|
||||||
|
if (item.Found)
|
||||||
|
{
|
||||||
|
Im.Line.SameInner();
|
||||||
|
var frame = new Vector2(Im.Style.TextHeight);
|
||||||
|
Im.Color.Button("D"u8, item.Diffuse, 0, frame);
|
||||||
|
Im.Line.SameInner();
|
||||||
|
Im.Color.Button("S"u8, item.Specular, 0, frame);
|
||||||
|
Im.Line.SameInner();
|
||||||
|
Im.Color.Button("E"u8, item.Emissive, 0, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsSelected(StainTemplate item, int globalIndex)
|
||||||
|
=> item.Key == _currentSelection;
|
||||||
|
|
||||||
|
protected override bool DrawFilter(float width, FilterComboBaseCache<StainTemplate> cache)
|
||||||
|
{
|
||||||
|
using var font = Im.Font.PushDefault();
|
||||||
|
return base.DrawFilter(width, cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StainService : Luna.IService
|
||||||
|
{
|
||||||
public const int ChannelCount = 2;
|
public const int ChannelCount = 2;
|
||||||
|
|
||||||
public readonly DictStain StainData;
|
public readonly StainCombo StainCombo1;
|
||||||
public readonly FilterComboColors StainCombo1;
|
public readonly StainCombo StainCombo2; // FIXME is there a better way to handle this?
|
||||||
public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this?
|
|
||||||
public readonly StmFile<LegacyDyePack> LegacyStmFile;
|
public readonly StmFile<LegacyDyePack> LegacyStmFile;
|
||||||
public readonly StmFile<DyePack> GudStmFile;
|
public readonly StmFile<DyePack> GudStmFile;
|
||||||
public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo;
|
public readonly StainTemplateCombo<LegacyDyePack> LegacyTemplateCombo;
|
||||||
|
|
@ -130,9 +189,8 @@ public class StainService : Luna.IService
|
||||||
|
|
||||||
public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData)
|
public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData)
|
||||||
{
|
{
|
||||||
StainData = stainData;
|
StainCombo1 = new StainCombo(stainData);
|
||||||
StainCombo1 = CreateStainCombo();
|
StainCombo2 = new StainCombo(stainData);
|
||||||
StainCombo2 = CreateStainCombo();
|
|
||||||
|
|
||||||
if (characterUtility.Address == null)
|
if (characterUtility.Address == null)
|
||||||
{
|
{
|
||||||
|
|
@ -146,14 +204,14 @@ public class StainService : Luna.IService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FilterComboColors[] stainCombos = [StainCombo1, StainCombo2];
|
StainCombo[] stainCombos = [StainCombo1, StainCombo2];
|
||||||
|
|
||||||
LegacyTemplateCombo = new StainTemplateCombo<LegacyDyePack>(stainCombos, LegacyStmFile);
|
LegacyTemplateCombo = new StainTemplateCombo<LegacyDyePack>(stainCombos, LegacyStmFile);
|
||||||
GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile);
|
GudTemplateCombo = new StainTemplateCombo<DyePack>(stainCombos, GudStmFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Retrieves the <see cref="FilterComboColors"/> instance for the given channel. Indexing is zero-based. </summary>
|
/// <summary> Retrieves the <see cref="FilterComboColors"/> instance for the given channel. Indexing is zero-based. </summary>
|
||||||
public FilterComboColors GetStainCombo(int channel)
|
public StainCombo GetStainCombo(int channel)
|
||||||
=> channel switch
|
=> channel switch
|
||||||
{
|
{
|
||||||
0 => StainCombo1,
|
0 => StainCombo1,
|
||||||
|
|
@ -180,8 +238,9 @@ public class StainService : Luna.IService
|
||||||
return new StmFile<TDyePack>(dataManager);
|
return new StmFile<TDyePack>(dataManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FilterComboColors CreateStainCombo()
|
public sealed class StainCombo(DictStain stainData) : Luna.FilterComboColors
|
||||||
=> new(140, MouseWheelType.None,
|
{
|
||||||
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(),
|
protected override IEnumerable<Item> GetItems()
|
||||||
Penumbra.Log);
|
=> stainData.Value.Select(t => new Item(new StringPair(t.Value.Name), t.Value.Dye, t.Key, t.Value.Gloss)).Prepend(None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
using OtterGui.Classes;
|
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
|
|
@ -11,7 +9,6 @@ using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
|
@ -24,7 +21,7 @@ public class FileEditor<T>(
|
||||||
FileDialogService fileDialog,
|
FileDialogService fileDialog,
|
||||||
string tabName,
|
string tabName,
|
||||||
string fileType,
|
string fileType,
|
||||||
Func<IReadOnlyList<FileRegistry>> getFiles,
|
Func<IEnumerable<FileRegistry>> getFiles,
|
||||||
Func<T, bool, bool> drawEdit,
|
Func<T, bool, bool> drawEdit,
|
||||||
Func<string> getInitialPath,
|
Func<string> getInitialPath,
|
||||||
Func<byte[], string, bool, T?> parseFile)
|
Func<byte[], string, bool, T?> parseFile)
|
||||||
|
|
@ -74,10 +71,15 @@ public class FileEditor<T>(
|
||||||
_defaultFile = null;
|
_defaultFile = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileRegistry? _currentPath;
|
private FileRegistry? CurrentPath
|
||||||
private T? _currentFile;
|
{
|
||||||
private Exception? _currentException;
|
get => _combo.Selected;
|
||||||
private bool _changed;
|
set => _combo.Selected = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? _currentFile;
|
||||||
|
private Exception? _currentException;
|
||||||
|
private bool _changed;
|
||||||
|
|
||||||
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
|
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
|
||||||
private bool _inInput;
|
private bool _inInput;
|
||||||
|
|
@ -163,7 +165,7 @@ public class FileEditor<T>(
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_currentException = null;
|
_currentException = null;
|
||||||
_currentPath = null;
|
CurrentPath = null;
|
||||||
(_currentFile as IDisposable)?.Dispose();
|
(_currentFile as IDisposable)?.Dispose();
|
||||||
_currentFile = null;
|
_currentFile = null;
|
||||||
_changed = false;
|
_changed = false;
|
||||||
|
|
@ -171,26 +173,32 @@ public class FileEditor<T>(
|
||||||
|
|
||||||
private void DrawFileSelectCombo()
|
private void DrawFileSelectCombo()
|
||||||
{
|
{
|
||||||
if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty,
|
if (CurrentPath is not null)
|
||||||
Im.ContentRegion.Available.X, Im.Style.TextHeight)
|
{
|
||||||
&& _combo.CurrentSelection != null)
|
if (_combo.Draw("##select"u8, CurrentPath.RelPath.Path.Span, StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection))
|
||||||
UpdateCurrentFile(_combo.CurrentSelection);
|
UpdateCurrentFile(newSelection);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_combo.Draw("##select"u8, $"Select {fileType} File...", StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection))
|
||||||
|
UpdateCurrentFile(newSelection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCurrentFile(FileRegistry path)
|
private void UpdateCurrentFile(FileRegistry path)
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(_currentPath, path))
|
if (ReferenceEquals(CurrentPath, path))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_changed = false;
|
_changed = false;
|
||||||
_currentPath = path;
|
CurrentPath = path;
|
||||||
_currentException = null;
|
_currentException = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
|
var bytes = File.ReadAllBytes(CurrentPath.File.FullName);
|
||||||
(_currentFile as IDisposable)?.Dispose();
|
(_currentFile as IDisposable)?.Dispose();
|
||||||
_currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
|
_currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file.
|
||||||
_currentFile = parseFile(bytes, _currentPath.File.FullName, true);
|
_currentFile = parseFile(bytes, CurrentPath.File.FullName, true);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -210,9 +218,9 @@ public class FileEditor<T>(
|
||||||
|
|
||||||
public void SaveFile()
|
public void SaveFile()
|
||||||
{
|
{
|
||||||
compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write());
|
compactor.WriteAllBytes(CurrentPath!.File.FullName, _currentFile!.Write());
|
||||||
if (owner.Mod is not null)
|
if (owner.Mod is not null)
|
||||||
communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, _currentPath));
|
communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, CurrentPath));
|
||||||
_changed = false;
|
_changed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,8 +229,8 @@ public class FileEditor<T>(
|
||||||
if (ImEx.Button("Reset Changes"u8, Vector2.Zero,
|
if (ImEx.Button("Reset Changes"u8, Vector2.Zero,
|
||||||
$"Reset all changes made to the {fileType} file.", !_changed))
|
$"Reset all changes made to the {fileType} file.", !_changed))
|
||||||
{
|
{
|
||||||
var tmp = _currentPath;
|
var tmp = CurrentPath;
|
||||||
_currentPath = null;
|
CurrentPath = null;
|
||||||
UpdateCurrentFile(tmp!);
|
UpdateCurrentFile(tmp!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +241,7 @@ public class FileEditor<T>(
|
||||||
if (!child)
|
if (!child)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_currentPath is not null)
|
if (CurrentPath is not null)
|
||||||
{
|
{
|
||||||
if (_currentFile is null)
|
if (_currentFile is null)
|
||||||
{
|
{
|
||||||
|
|
@ -253,7 +261,7 @@ public class FileEditor<T>(
|
||||||
|
|
||||||
if (!_inInput && _defaultPath.Length > 0)
|
if (!_inInput && _defaultPath.Length > 0)
|
||||||
{
|
{
|
||||||
if (_currentPath is not null)
|
if (CurrentPath is not null)
|
||||||
{
|
{
|
||||||
Im.Line.New();
|
Im.Line.New();
|
||||||
Im.Line.New();
|
Im.Line.New();
|
||||||
|
|
@ -278,47 +286,70 @@ public class FileEditor<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Combo(Func<IReadOnlyList<FileRegistry>> generator)
|
private sealed class Combo : FilterComboBase<FileRegistry>
|
||||||
: FilterComboCache<FileRegistry>(generator, MouseWheelType.None, Penumbra.Log)
|
|
||||||
{
|
{
|
||||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
private sealed class FileFilter : RegexFilterBase<FileRegistry>
|
||||||
|
{
|
||||||
|
// TODO: Avoid ToString.
|
||||||
|
public override bool WouldBeVisible(in FileRegistry item, int globalIndex)
|
||||||
|
=> WouldBeVisible(item.File.FullName) || item.SubModUsage.Any(f => WouldBeVisible(f.Item2.ToString()));
|
||||||
|
|
||||||
|
/// <summary> Unused. </summary>
|
||||||
|
protected override string ToFilterString(in FileRegistry item, int globalIndex)
|
||||||
|
=> string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Func<IEnumerable<FileRegistry>> _getFiles;
|
||||||
|
|
||||||
|
public FileRegistry? Selected;
|
||||||
|
|
||||||
|
public Combo(Func<IEnumerable<FileRegistry>> getFiles)
|
||||||
|
{
|
||||||
|
_getFiles = getFiles;
|
||||||
|
Filter = new FileFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<FileRegistry> GetItems()
|
||||||
|
=> _getFiles();
|
||||||
|
|
||||||
|
protected override float ItemHeight
|
||||||
|
=> Im.Style.TextHeightWithSpacing;
|
||||||
|
|
||||||
|
protected override bool DrawItem(in FileRegistry item, int globalIndex, bool selected)
|
||||||
{
|
{
|
||||||
var file = Items[globalIdx];
|
|
||||||
bool ret;
|
bool ret;
|
||||||
using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), file.IsOnPlayer))
|
using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), item.IsOnPlayer))
|
||||||
{
|
{
|
||||||
ret = Im.Selectable(file.RelPath.ToString(), selected);
|
ret = Im.Selectable(item.RelPath.Path.Span, selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Im.Item.Hovered())
|
if (Im.Item.Hovered())
|
||||||
{
|
{
|
||||||
using var tt = Im.Tooltip.Begin();
|
using var style = Im.Style.PushDefault(ImStyleDouble.WindowPadding);
|
||||||
|
using var tt = Im.Tooltip.Begin();
|
||||||
Im.Text("All Game Paths"u8);
|
Im.Text("All Game Paths"u8);
|
||||||
Im.Separator();
|
Im.Separator();
|
||||||
using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit);
|
using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit);
|
||||||
if (t)
|
if (t)
|
||||||
{
|
foreach (var (option, gamePath) in item.SubModUsage)
|
||||||
foreach (var (option, gamePath) in file.SubModUsage)
|
|
||||||
{
|
{
|
||||||
t.DrawColumn(gamePath.Path.Span);
|
t.DrawColumn(gamePath.Path.Span);
|
||||||
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
||||||
t.DrawColumn(option.GetFullName());
|
t.DrawColumn(option.GetFullName());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.SubModUsage.Count > 0)
|
if (item.SubModUsage.Count > 0)
|
||||||
{
|
{
|
||||||
Im.Line.Same();
|
Im.Line.Same();
|
||||||
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value());
|
||||||
ImEx.TextRightAligned($"{file.SubModUsage[0].Item2.Path}");
|
ImEx.TextRightAligned($"{item.SubModUsage[0].Item2.Path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool IsVisible(int globalIndex, LowerString filter)
|
protected override bool IsSelected(FileRegistry item, int globalIndex)
|
||||||
=> filter.IsContained(Items[globalIndex].File.FullName)
|
=> item.Equals(Selected);
|
||||||
|| Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
using Luna.Generators;
|
using Luna.Generators;
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
|
|
@ -12,7 +11,6 @@ using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Editor;
|
|
||||||
using Penumbra.Mods.Groups;
|
using Penumbra.Mods.Groups;
|
||||||
using Penumbra.Mods.ItemSwap;
|
using Penumbra.Mods.ItemSwap;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
|
|
@ -22,8 +20,6 @@ using Penumbra.Mods.SubMods;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
using Penumbra.UI.ModsTab;
|
using Penumbra.UI.ModsTab;
|
||||||
using ITab = OtterGui.Widgets.ITab;
|
|
||||||
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
|
@ -152,44 +148,6 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
|
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type)
|
|
||||||
: FilterComboCache<(EquipItem Item, bool InMod, SingleArray<IMod> InCollection)>(() =>
|
|
||||||
{
|
|
||||||
var list = data.ByType[type];
|
|
||||||
var enumerable = selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)
|
|
||||||
? list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name),
|
|
||||||
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
|
|
||||||
.OrderByDescending(p => p.Item2).ThenByDescending(p => p.Item3.Count)
|
|
||||||
: selector is null
|
|
||||||
? list.Select(i => (i, false,
|
|
||||||
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
|
|
||||||
.OrderBy(p => p.Item3.Count)
|
|
||||||
: list.Select(i => (i, false,
|
|
||||||
collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray<IMod>()))
|
|
||||||
.OrderByDescending(p => p.Item3.Count);
|
|
||||||
return enumerable.ToList();
|
|
||||||
}, MouseWheelType.None, Penumbra.Log)
|
|
||||||
{
|
|
||||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
|
||||||
{
|
|
||||||
var (_, inMod, inCollection) = Items[globalIdx];
|
|
||||||
using var color = inMod
|
|
||||||
? ImGuiColor.Text.Push(ColorId.ResTreeLocalPlayer.Value())
|
|
||||||
: inCollection.Count > 0
|
|
||||||
? ImGuiColor.Text.Push(ColorId.ResTreeNonNetworked.Value())
|
|
||||||
: null;
|
|
||||||
var ret = base.DrawSelectable(globalIdx, selected);
|
|
||||||
if (inCollection.Count > 0)
|
|
||||||
Im.Tooltip.OnHover(string.Join('\n', inCollection.Select(m => m.Name)));
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string ToString((EquipItem Item, bool InMod, SingleArray<IMod> InCollection) obj)
|
|
||||||
=> obj.Item.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, StringU8 TextFrom, StringU8 TextTo)> _selectors;
|
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, StringU8 TextFrom, StringU8 TextTo)> _selectors;
|
||||||
private readonly ItemSwapContainer _swapData;
|
private readonly ItemSwapContainer _swapData;
|
||||||
|
|
||||||
|
|
@ -241,17 +199,17 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
case SwapType.Ring:
|
case SwapType.Ring:
|
||||||
case SwapType.Glasses:
|
case SwapType.Glasses:
|
||||||
var values = _selectors[_lastTab];
|
var values = _selectors[_lastTab];
|
||||||
if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown
|
if (values.Source.CurrentSelection.Type is not FullEquipType.Unknown
|
||||||
&& values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown)
|
&& values.Target.CurrentSelection.Type is not FullEquipType.Unknown)
|
||||||
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item,
|
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection,
|
||||||
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
|
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
|
||||||
break;
|
break;
|
||||||
case SwapType.BetweenSlots:
|
case SwapType.BetweenSlots:
|
||||||
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
|
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
|
||||||
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
|
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
|
||||||
if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid)
|
if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid)
|
||||||
_affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom),
|
_affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection, ToEquipSlot(_slotFrom),
|
||||||
selectorFrom.CurrentSelection.Item,
|
selectorFrom.CurrentSelection,
|
||||||
_useCurrentCollection ? _collectionManager.Active.Current : null);
|
_useCurrentCollection ? _collectionManager.Active.Current : null);
|
||||||
break;
|
break;
|
||||||
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
|
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
|
||||||
|
|
@ -315,10 +273,10 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
$"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}";
|
$"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}";
|
||||||
case SwapType.BetweenSlots:
|
case SwapType.BetweenSlots:
|
||||||
return
|
return
|
||||||
$"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}";
|
$"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}";
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
$"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}";
|
$"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -543,9 +501,7 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
}
|
}
|
||||||
|
|
||||||
table.NextColumn();
|
table.NextColumn();
|
||||||
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name, string.Empty,
|
_dirty |= selector.Draw("##itemSource"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
|
||||||
InputWidth * 2 * Im.Style.GlobalScale,
|
|
||||||
Im.Style.TextHeightWithSpacing);
|
|
||||||
|
|
||||||
(article1, _, selector) = GetAccessorySelector(_slotTo, false);
|
(article1, _, selector) = GetAccessorySelector(_slotTo, false);
|
||||||
table.DrawFrameColumn($"and put {article2} on {article1}");
|
table.DrawFrameColumn($"and put {article2} on {article1}");
|
||||||
|
|
@ -566,8 +522,7 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
}
|
}
|
||||||
|
|
||||||
table.NextColumn();
|
table.NextColumn();
|
||||||
_dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * Im.Style.GlobalScale,
|
_dirty |= selector.Draw("##itemTarget"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
|
||||||
Im.Style.TextHeightWithSpacing);
|
|
||||||
if (_affectedItems is not { Count: > 1 })
|
if (_affectedItems is not { Count: > 1 })
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -576,7 +531,7 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
if (Im.Item.Hovered())
|
if (Im.Item.Hovered())
|
||||||
{
|
{
|
||||||
using var tt = Im.Tooltip.Begin();
|
using var tt = Im.Tooltip.Begin();
|
||||||
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name)))
|
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name)))
|
||||||
Im.Text(item.Name);
|
Im.Text(item.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -610,8 +565,7 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
return;
|
return;
|
||||||
table.DrawFrameColumn(text1);
|
table.DrawFrameColumn(text1);
|
||||||
table.NextColumn();
|
table.NextColumn();
|
||||||
_dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item.Name, string.Empty,
|
_dirty |= sourceSelector.Draw("##itemSource"u8, sourceSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
|
||||||
InputWidth * 2 * Im.Style.GlobalScale, Im.Style.TextHeightWithSpacing);
|
|
||||||
|
|
||||||
if (type is SwapType.Ring)
|
if (type is SwapType.Ring)
|
||||||
{
|
{
|
||||||
|
|
@ -621,9 +575,7 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
|
|
||||||
table.DrawFrameColumn(text2);
|
table.DrawFrameColumn(text2);
|
||||||
table.NextColumn();
|
table.NextColumn();
|
||||||
_dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item.Name, string.Empty,
|
_dirty |= targetSelector.Draw("##itemTarget"u8, targetSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _);
|
||||||
InputWidth * 2 * Im.Style.GlobalScale,
|
|
||||||
Im.Style.TextHeightWithSpacing);
|
|
||||||
if (type is SwapType.Ring)
|
if (type is SwapType.Ring)
|
||||||
{
|
{
|
||||||
Im.Line.Same();
|
Im.Line.Same();
|
||||||
|
|
@ -638,7 +590,7 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
if (Im.Item.Hovered())
|
if (Im.Item.Hovered())
|
||||||
{
|
{
|
||||||
using var tt = Im.Tooltip.Begin();
|
using var tt = Im.Tooltip.Begin();
|
||||||
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name)))
|
foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name)))
|
||||||
Im.Text(item.Name);
|
Im.Text(item.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
using Penumbra.GameData.Files.MaterialStructs;
|
using Penumbra.GameData.Files.MaterialStructs;
|
||||||
|
|
@ -87,8 +86,8 @@ public partial class MtrlTab
|
||||||
var rowBIdx = rowAIdx | 1;
|
var rowBIdx = rowAIdx | 1;
|
||||||
var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default;
|
var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default;
|
||||||
var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default;
|
var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default;
|
||||||
var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key;
|
var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Id;
|
||||||
var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key;
|
var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Id;
|
||||||
var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA);
|
var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA);
|
||||||
var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB);
|
var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB);
|
||||||
using (var columns = Im.Columns(2, "ColorTable"u8))
|
using (var columns = Im.Columns(2, "ColorTable"u8))
|
||||||
|
|
@ -599,11 +598,10 @@ public partial class MtrlTab
|
||||||
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
|
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
|
||||||
Im.Line.Same(subColWidth);
|
Im.Line.Same(subColWidth);
|
||||||
Im.Item.SetNextWidth(scalarSize);
|
Im.Item.SetNextWidth(scalarSize);
|
||||||
_stainService.GudTemplateCombo.CurrentDyeChannel = dye.Channel;
|
if (_stainService.GudTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection,
|
||||||
if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty,
|
scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
|
||||||
scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton))
|
|
||||||
{
|
{
|
||||||
dye.Template = _stainService.GudTemplateCombo.CurrentSelection.UShort;
|
dye.Template = (ushort) newSelection;
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -613,8 +611,8 @@ public partial class MtrlTab
|
||||||
using var dis = Im.Disabled(!dyePack.HasValue);
|
using var dis = Im.Disabled(!dyePack.HasValue);
|
||||||
if (Im.Button("Apply Preview Dye"u8))
|
if (Im.Button("Apply Preview Dye"u8))
|
||||||
ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [
|
ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [
|
||||||
_stainService.StainCombo1.CurrentSelection.Key,
|
_stainService.StainCombo1.CurrentSelection.Id,
|
||||||
_stainService.StainCombo2.CurrentSelection.Key,
|
_stainService.StainCombo2.CurrentSelection.Id,
|
||||||
], rowIdx);
|
], rowIdx);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ public partial class MtrlTab
|
||||||
|
|
||||||
private bool DrawPreviewDye(bool disabled)
|
private bool DrawPreviewDye(bool disabled)
|
||||||
{
|
{
|
||||||
var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection;
|
var (name1, _, dyeId1, _) = _stainService.StainCombo1.CurrentSelection;
|
||||||
var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection;
|
var (name2, _, dyeId2, _) = _stainService.StainCombo2.CurrentSelection;
|
||||||
var tt = dyeId1 is 0 && dyeId2 is 0
|
var tt = dyeId1 is 0 && dyeId2 is 0
|
||||||
? "Select a preview dye first."u8
|
? "Select a preview dye first."u8
|
||||||
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8;
|
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8;
|
||||||
|
|
@ -103,12 +103,10 @@ public partial class MtrlTab
|
||||||
}
|
}
|
||||||
|
|
||||||
Im.Line.Same();
|
Im.Line.Same();
|
||||||
var label = dyeId1 is 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1";
|
if (_stainService.StainCombo1.Draw(dyeId1 is 0 ? "Preview Dye 1###pd1" : $"{name1} (Preview 1)###pd1"))
|
||||||
if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1))
|
|
||||||
UpdateColorTablePreview();
|
UpdateColorTablePreview();
|
||||||
Im.Line.Same();
|
Im.Line.Same();
|
||||||
label = dyeId2 is 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2";
|
if (_stainService.StainCombo2.Draw(dyeId2 is 0 ? "Preview Dye 2###pd2" : $"{name2} (Preview 2)###pd2"))
|
||||||
if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2))
|
|
||||||
UpdateColorTablePreview();
|
UpdateColorTablePreview();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
using Penumbra.GameData.Files.MaterialStructs;
|
using Penumbra.GameData.Files.MaterialStructs;
|
||||||
|
|
@ -177,13 +176,13 @@ public partial class MtrlTab
|
||||||
ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false,
|
ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false,
|
||||||
m => table[rowIdx].TileTransform = m);
|
m => table[rowIdx].TileTransform = m);
|
||||||
|
|
||||||
if (dyeTable != null)
|
if (dyeTable is not null)
|
||||||
{
|
{
|
||||||
Im.Table.NextColumn();
|
Im.Table.NextColumn();
|
||||||
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
|
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, 0, StringU8.Empty, out var newSelection,
|
||||||
+ Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton))
|
intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
|
||||||
{
|
{
|
||||||
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort;
|
dyeTable[rowIdx].Template = (ushort)newSelection;
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,11 +293,10 @@ public partial class MtrlTab
|
||||||
ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f,
|
ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f,
|
||||||
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
|
value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1));
|
||||||
Im.Line.SameInner();
|
Im.Line.SameInner();
|
||||||
_stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel;
|
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection,
|
||||||
if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
|
intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton))
|
||||||
+ Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton))
|
|
||||||
{
|
{
|
||||||
dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort;
|
dyeTable[rowIdx].Template = (ushort)newSelection;
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,8 +311,8 @@ public partial class MtrlTab
|
||||||
|
|
||||||
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize)
|
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize)
|
||||||
{
|
{
|
||||||
var stain = _stainService.StainCombo1.CurrentSelection.Key;
|
var stain = _stainService.StainCombo1.CurrentSelection.Id;
|
||||||
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
|
if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
|
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
|
||||||
|
|
@ -331,8 +329,8 @@ public partial class MtrlTab
|
||||||
|
|
||||||
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize)
|
private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize)
|
||||||
{
|
{
|
||||||
var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key;
|
var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Id;
|
||||||
if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
|
if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
|
using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2);
|
||||||
|
|
@ -341,8 +339,8 @@ public partial class MtrlTab
|
||||||
|
|
||||||
ret = ret
|
ret = ret
|
||||||
&& Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [
|
&& Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [
|
||||||
_stainService.StainCombo1.CurrentSelection.Key,
|
_stainService.StainCombo1.CurrentSelection.Id,
|
||||||
_stainService.StainCombo2.CurrentSelection.Key,
|
_stainService.StainCombo2.CurrentSelection.Id,
|
||||||
], rowIdx);
|
], rowIdx);
|
||||||
|
|
||||||
Im.Line.Same();
|
Im.Line.Same();
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ public partial class MtrlTab
|
||||||
};
|
};
|
||||||
if (dyeRow.Channel < StainService.ChannelCount)
|
if (dyeRow.Channel < StainService.ChannelCount)
|
||||||
{
|
{
|
||||||
StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key;
|
StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Id;
|
||||||
if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes))
|
if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes))
|
||||||
row.ApplyDye(dyeRow, legacyDyes);
|
row.ApplyDye(dyeRow, legacyDyes);
|
||||||
if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes))
|
if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes))
|
||||||
|
|
@ -260,8 +260,8 @@ public partial class MtrlTab
|
||||||
{
|
{
|
||||||
ReadOnlySpan<StainId> stainIds =
|
ReadOnlySpan<StainId> stainIds =
|
||||||
[
|
[
|
||||||
_stainService.StainCombo1.CurrentSelection.Key,
|
_stainService.StainCombo1.CurrentSelection.Id,
|
||||||
_stainService.StainCombo2.CurrentSelection.Key,
|
_stainService.StainCombo2.CurrentSelection.Id,
|
||||||
];
|
];
|
||||||
rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows);
|
rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows);
|
||||||
rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows);
|
rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows);
|
||||||
|
|
|
||||||
|
|
@ -89,17 +89,17 @@ public sealed partial class MtrlTab : IWritable, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawMaterialLivePreviewRebind(disabled);
|
DrawMaterialLivePreviewRebind(disabled);
|
||||||
|
|
||||||
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
||||||
var ret = DrawBackFaceAndTransparency(disabled);
|
var ret = DrawBackFaceAndTransparency(disabled);
|
||||||
|
|
||||||
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
||||||
ret |= DrawShaderSection(disabled);
|
ret |= DrawShaderSection(disabled);
|
||||||
|
|
||||||
ret |= DrawTextureSection(disabled);
|
ret |= DrawTextureSection(disabled);
|
||||||
ret |= DrawColorTableSection(disabled);
|
ret |= DrawColorTableSection(disabled);
|
||||||
ret |= DrawConstantsSection(disabled);
|
ret |= DrawConstantsSection(disabled);
|
||||||
|
|
||||||
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
|
||||||
DrawOtherMaterialDetails(disabled);
|
DrawOtherMaterialDetails(disabled);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,12 @@ public abstract class MetaDrawer<TIdentifier, TEntry>(ModMetaEditor editor, Meta
|
||||||
|
|
||||||
var height = ColumnHeight;
|
var height = ColumnHeight;
|
||||||
using var clipper = new Im.ListClipper(Count, height);
|
using var clipper = new Im.ListClipper(Count, height);
|
||||||
foreach (var (identifier, value) in clipper.Iterate(Enumerate()))
|
foreach (var (index, (identifier, value)) in clipper.Iterate(Enumerate().Index()))
|
||||||
|
{
|
||||||
|
id.Push(index);
|
||||||
DrawEntry(identifier, value);
|
DrawEntry(identifier, value);
|
||||||
|
id.Pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract ReadOnlySpan<byte> Label { get; }
|
public abstract ReadOnlySpan<byte> Label { get; }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using OtterGui;
|
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
|
|
@ -61,17 +60,19 @@ public partial class ModEditWindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class BoneCache(PbdData pbdData) : BasicFilterCache<string>(pbdData.BoneFilter)
|
||||||
|
{
|
||||||
|
protected override IEnumerable<string> GetItems()
|
||||||
|
=> pbdData.SelectedDeformer is null || pbdData.SelectedDeformer.IsEmpty ? [] : pbdData.SelectedDeformer.DeformMatrices.Keys;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawBoneSelector()
|
private void DrawBoneSelector()
|
||||||
{
|
{
|
||||||
|
var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Get((int)_pbdData.SelectedRaceCode), () => new BoneCache(_pbdData));
|
||||||
using var group = Im.Group();
|
using var group = Im.Group();
|
||||||
var width = 200 * Im.Style.GlobalScale;
|
var width = 200 * Im.Style.GlobalScale;
|
||||||
using (ImStyleSingle.FrameRounding.Push(0)
|
_pbdData.BoneFilter.DrawFilter("Filter..."u8, new Vector2(width, Im.Style.FrameHeight));
|
||||||
.Push(ImStyleDouble.ItemSpacing, Vector2.Zero))
|
Im.Cursor.Y -= Im.Style.ItemSpacing.Y;
|
||||||
{
|
|
||||||
Im.Item.SetNextWidth(width);
|
|
||||||
Im.Input.Text("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var child = Im.Child.Begin("Bone"u8,
|
using var child = Im.Child.Begin("Bone"u8,
|
||||||
new Vector2(width, Im.ContentRegion.Maximum.Y - Im.Style.FrameHeight - Im.Style.WindowPadding.Y), true);
|
new Vector2(width, Im.ContentRegion.Maximum.Y - Im.Style.FrameHeight - Im.Style.WindowPadding.Y), true);
|
||||||
if (!child)
|
if (!child)
|
||||||
|
|
@ -80,23 +81,14 @@ public partial class ModEditWindow
|
||||||
if (_pbdData.SelectedDeformer is null)
|
if (_pbdData.SelectedDeformer is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_pbdData.SelectedDeformer.IsEmpty)
|
if (cache.AllItems.Count is 0)
|
||||||
{
|
|
||||||
Im.Text("<Empty>"u8);
|
Im.Text("<Empty>"u8);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
foreach (var item in cache)
|
||||||
var height = Im.Style.TextHeightWithSpacing;
|
{
|
||||||
var skips = ImGuiClip.GetNecessarySkips(height);
|
if (Im.Selectable(item, item == _pbdData.SelectedBone))
|
||||||
var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips,
|
_pbdData.SelectedBone = item;
|
||||||
b => b.Contains(_pbdData.BoneFilter), bone
|
}
|
||||||
=>
|
|
||||||
{
|
|
||||||
if (Im.Selectable(bone, bone == _pbdData.SelectedBone))
|
|
||||||
_pbdData.SelectedBone = bone;
|
|
||||||
});
|
|
||||||
ImGuiClip.DrawEndDummy(remainder, height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DrawBoneData(PbdTab tab, bool disabled)
|
private bool DrawBoneData(PbdTab tab, bool disabled)
|
||||||
|
|
@ -324,8 +316,8 @@ public partial class ModEditWindow
|
||||||
public GenderRace SelectedRaceCode = GenderRace.Unknown;
|
public GenderRace SelectedRaceCode = GenderRace.Unknown;
|
||||||
public RacialDeformer? SelectedDeformer;
|
public RacialDeformer? SelectedDeformer;
|
||||||
public string? SelectedBone;
|
public string? SelectedBone;
|
||||||
|
public TextFilter BoneFilter = new();
|
||||||
public string NewBoneName = string.Empty;
|
public string NewBoneName = string.Empty;
|
||||||
public string BoneFilter = string.Empty;
|
|
||||||
public string RaceCodeFilter = string.Empty;
|
public string RaceCodeFilter = string.Empty;
|
||||||
|
|
||||||
public TransformMatrix? CopiedMatrix;
|
public TransformMatrix? CopiedMatrix;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
using OtterGui;
|
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
@ -20,10 +19,7 @@ public partial class ModEditWindow
|
||||||
private int _pathIdx = -1;
|
private int _pathIdx = -1;
|
||||||
private int _folderSkip;
|
private int _folderSkip;
|
||||||
private bool _overviewMode;
|
private bool _overviewMode;
|
||||||
|
private readonly OverviewTable _overviewTable;
|
||||||
private string _fileOverviewFilter1 = string.Empty;
|
|
||||||
private string _fileOverviewFilter2 = string.Empty;
|
|
||||||
private string _fileOverviewFilter3 = string.Empty;
|
|
||||||
|
|
||||||
private bool CheckFilter(FileRegistry registry)
|
private bool CheckFilter(FileRegistry registry)
|
||||||
=> _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase);
|
=> _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
@ -40,9 +36,7 @@ public partial class ModEditWindow
|
||||||
DrawOptionSelectHeader();
|
DrawOptionSelectHeader();
|
||||||
DrawButtonHeader();
|
DrawButtonHeader();
|
||||||
|
|
||||||
if (_overviewMode)
|
if (!_overviewMode)
|
||||||
DrawFileManagementOverview();
|
|
||||||
else
|
|
||||||
DrawFileManagementNormal();
|
DrawFileManagementNormal();
|
||||||
|
|
||||||
using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true);
|
using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true);
|
||||||
|
|
@ -50,65 +44,11 @@ public partial class ModEditWindow
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_overviewMode)
|
if (_overviewMode)
|
||||||
DrawFilesOverviewMode();
|
_overviewTable.Draw();
|
||||||
else
|
else
|
||||||
DrawFilesNormalMode();
|
DrawFilesNormalMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawFilesOverviewMode()
|
|
||||||
{
|
|
||||||
var height = Im.Style.TextHeightWithSpacing + 2 * Im.Style.CellPadding.Y;
|
|
||||||
var skips = ImGuiClip.GetNecessarySkips(height);
|
|
||||||
|
|
||||||
using var table = Im.Table.Begin("##table"u8, 3, TableFlags.RowBackground | TableFlags.BordersInnerVertical, Im.ContentRegion.Available);
|
|
||||||
|
|
||||||
if (!table)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var width = Im.ContentRegion.Available.X / 8;
|
|
||||||
|
|
||||||
table.SetupColumn("##file"u8, TableColumnFlags.WidthFixed, width * 3);
|
|
||||||
table.SetupColumn("##path"u8, TableColumnFlags.WidthFixed, width * 3 + Im.Style.FrameBorderThickness);
|
|
||||||
table.SetupColumn("##option"u8, TableColumnFlags.WidthFixed, width * 2);
|
|
||||||
|
|
||||||
var idx = 0;
|
|
||||||
|
|
||||||
var files = _editor.Files.Available.SelectMany(f =>
|
|
||||||
{
|
|
||||||
var file = f.RelPath.ToString();
|
|
||||||
return f.SubModUsage.Count == 0
|
|
||||||
? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1)
|
|
||||||
: f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(),
|
|
||||||
_editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u));
|
|
||||||
});
|
|
||||||
|
|
||||||
void DrawLine((string, string, string, uint) data)
|
|
||||||
{
|
|
||||||
using var id = Im.Id.Push(idx++);
|
|
||||||
if (data.Item4 is not 0)
|
|
||||||
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
|
|
||||||
|
|
||||||
ImEx.CopyOnClickSelectable(data.Item1);
|
|
||||||
Im.Table.NextColumn();
|
|
||||||
if (data.Item4 is not 0)
|
|
||||||
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
|
|
||||||
|
|
||||||
ImEx.CopyOnClickSelectable(data.Item2);
|
|
||||||
Im.Table.NextColumn();
|
|
||||||
if (data.Item4 is not 0)
|
|
||||||
Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4);
|
|
||||||
|
|
||||||
ImEx.CopyOnClickSelectable(data.Item3);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Filter((string, string, string, uint) data)
|
|
||||||
=> data.Item1.Contains(_fileOverviewFilter1, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& data.Item2.Contains(_fileOverviewFilter2, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& data.Item3.Contains(_fileOverviewFilter3, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var end = ImGuiClip.FilteredClippedDraw(files, skips, Filter, DrawLine);
|
|
||||||
ImGuiClip.DrawEndDummy(end, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawFilesNormalMode()
|
private void DrawFilesNormalMode()
|
||||||
{
|
{
|
||||||
|
|
@ -154,7 +94,7 @@ public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
using var tt = Im.Tooltip.Begin();
|
using var tt = Im.Tooltip.Begin();
|
||||||
using var c = ImGuiColor.Text.PushDefault();
|
using var c = ImGuiColor.Text.PushDefault();
|
||||||
Im.Text(StringU8.Join((byte) '\n', text));
|
Im.Text(StringU8.Join((byte)'\n', text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -255,7 +195,7 @@ public partial class ModEditWindow
|
||||||
var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString();
|
var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString();
|
||||||
var pos = Im.Cursor.X - Im.Style.FrameHeight;
|
var pos = Im.Cursor.X - Im.Style.FrameHeight;
|
||||||
Im.Item.SetNextWidth(-1);
|
Im.Item.SetNextWidth(-1);
|
||||||
if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength:Utf8GamePath.MaxGamePathLength))
|
if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength: Utf8GamePath.MaxGamePathLength))
|
||||||
{
|
{
|
||||||
_fileIdx = i;
|
_fileIdx = i;
|
||||||
_pathIdx = j;
|
_pathIdx = j;
|
||||||
|
|
@ -341,7 +281,8 @@ public partial class ModEditWindow
|
||||||
if (Im.Button("Add Paths"u8))
|
if (Im.Button("Add Paths"u8))
|
||||||
_editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip);
|
_editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip);
|
||||||
|
|
||||||
Im.Tooltip.OnHover("Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8);
|
Im.Tooltip.OnHover(
|
||||||
|
"Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8);
|
||||||
|
|
||||||
|
|
||||||
Im.Line.Same();
|
Im.Line.Same();
|
||||||
|
|
@ -365,7 +306,7 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
Im.Line.Same();
|
Im.Line.Same();
|
||||||
var changes = _editor.FileEditor.Changes;
|
var changes = _editor.FileEditor.Changes;
|
||||||
var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8;
|
var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8;
|
||||||
if (ImEx.Button("Apply Changes"u8, Vector2.Zero, tt2, !changes))
|
if (ImEx.Button("Apply Changes"u8, Vector2.Zero, tt2, !changes))
|
||||||
{
|
{
|
||||||
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
|
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
|
||||||
|
|
@ -412,22 +353,4 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
ImEx.TextRightAligned($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected");
|
ImEx.TextRightAligned($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawFileManagementOverview()
|
|
||||||
{
|
|
||||||
using var style = ImStyleSingle.FrameRounding.Push(0)
|
|
||||||
.Push(ImStyleDouble.ItemSpacing, Vector2.Zero)
|
|
||||||
.Push(ImStyleSingle.FrameBorderThickness, Im.Style.ChildBorderThickness);
|
|
||||||
|
|
||||||
var width = Im.ContentRegion.Available.X / 8;
|
|
||||||
|
|
||||||
Im.Item.SetNextWidth(width * 3);
|
|
||||||
Im.Input.Text("##fileFilter"u8, ref _fileOverviewFilter1, "Filter file..."u8);
|
|
||||||
Im.Line.Same();
|
|
||||||
Im.Item.SetNextWidth(width * 3);
|
|
||||||
Im.Input.Text("##pathFilter"u8, ref _fileOverviewFilter2, "Filter path..."u8);
|
|
||||||
Im.Line.Same();
|
|
||||||
Im.Item.SetNextWidth(width * 2);
|
|
||||||
Im.Input.Text("##optionFilter"u8, ref _fileOverviewFilter3, "Filter option..."u8);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ public partial class ModEditWindow
|
||||||
private void DrawEditHeader(MetaManipulationType type)
|
private void DrawEditHeader(MetaManipulationType type)
|
||||||
{
|
{
|
||||||
var drawer = _metaDrawers.Get(type);
|
var drawer = _metaDrawers.Get(type);
|
||||||
if (drawer == null)
|
if (drawer is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var oldPos = Im.Cursor.Y;
|
var oldPos = Im.Cursor.Y;
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,7 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
|
||||||
_fileDialog = fileDialog;
|
_fileDialog = fileDialog;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_metaDrawers = metaDrawers;
|
_metaDrawers = metaDrawers;
|
||||||
|
_overviewTable = new OverviewTable(_editor);
|
||||||
_optionSelect = new OptionSelectCombo(editor, this);
|
_optionSelect = new OptionSelectCombo(editor, this);
|
||||||
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
_materialTab = new FileEditor<MtrlTab>(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
||||||
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
|
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty,
|
||||||
|
|
|
||||||
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);
|
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
|
||||||
|
|
||||||
if (!_activeCollections.CurrentCollectionInUse)
|
if (!_activeCollections.CurrentCollectionInUse)
|
||||||
ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg);
|
ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 },
|
||||||
|
Colors.PressEnterWarningBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawTemporaryCheckbox()
|
private void DrawTemporaryCheckbox()
|
||||||
|
|
@ -185,6 +186,7 @@ public class CollectionSelectHeader(
|
||||||
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
|
tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
|
||||||
|
|
||||||
if (!_activeCollections.CurrentCollectionInUse)
|
if (!_activeCollections.CurrentCollectionInUse)
|
||||||
ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg);
|
ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 },
|
||||||
|
Colors.PressEnterWarningBg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public class IncognitoService(TutorialService tutorial, Configuration config) :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hold)
|
if (!hold)
|
||||||
Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle.");
|
Im.Tooltip.OnHover($"\nHold {config.IncognitoModifier} while clicking to toggle.", HoveredFlags.AllowWhenDisabled, true);
|
||||||
|
|
||||||
tutorial.OpenTutorial(BasicTutorialSteps.Incognito);
|
tutorial.OpenTutorial(BasicTutorialSteps.Incognito);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.Components;
|
||||||
using Dalamud.Interface.GameFonts;
|
using Dalamud.Interface.GameFonts;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
|
@ -14,6 +13,7 @@ using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
|
using Penumbra.UI.Tabs;
|
||||||
|
|
||||||
namespace Penumbra.UI.CollectionTab;
|
namespace Penumbra.UI.CollectionTab;
|
||||||
|
|
||||||
|
|
@ -26,8 +26,9 @@ public sealed class CollectionPanel(
|
||||||
ITargetManager targets,
|
ITargetManager targets,
|
||||||
ModStorage mods,
|
ModStorage mods,
|
||||||
SaveService saveService,
|
SaveService saveService,
|
||||||
IncognitoService incognito)
|
IncognitoService incognito,
|
||||||
: IDisposable
|
Configuration config)
|
||||||
|
: IDisposable, IPanel
|
||||||
{
|
{
|
||||||
private readonly CollectionStorage _collections = manager.Storage;
|
private readonly CollectionStorage _collections = manager.Storage;
|
||||||
private readonly ActiveCollections _active = manager.Active;
|
private readonly ActiveCollections _active = manager.Active;
|
||||||
|
|
@ -223,13 +224,8 @@ public sealed class CollectionPanel(
|
||||||
{
|
{
|
||||||
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f));
|
using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f));
|
||||||
Im.Item.SetNextWidth(width);
|
Im.Item.SetNextWidth(width);
|
||||||
if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName)
|
if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName))
|
||||||
&& newName != collection.Identity.Name)
|
_collections.RenameCollection(collection, newName);
|
||||||
{
|
|
||||||
collection.Identity.Name = newName;
|
|
||||||
saveService.QueueSave(new ModCollectionSave(mods, collection));
|
|
||||||
selector.RestoreCollections();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_collections.DefaultNamed == collection)
|
if (_collections.DefaultNamed == collection)
|
||||||
|
|
@ -770,4 +766,18 @@ public sealed class CollectionPanel(
|
||||||
ret.Add((type, localPre, post, name, border));
|
ret.Add((type, localPre, post, name, border));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadOnlySpan<byte> Id
|
||||||
|
=> "cp"u8;
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
switch (config.Ephemeral.CollectionPanel)
|
||||||
|
{
|
||||||
|
case CollectionPanelMode.SimpleAssignment: DrawSimple(); break;
|
||||||
|
case CollectionPanelMode.IndividualAssignment: DrawIndividualPanel(); break;
|
||||||
|
case CollectionPanelMode.GroupAssignment: DrawGroupPanel(); break;
|
||||||
|
case CollectionPanelMode.Details: DrawDetailsPanel(); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using OtterGui;
|
using Luna;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
|
|
@ -9,87 +9,20 @@ using Penumbra.UI.Classes;
|
||||||
|
|
||||||
namespace Penumbra.UI.CollectionTab;
|
namespace Penumbra.UI.CollectionTab;
|
||||||
|
|
||||||
public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposable
|
public sealed class CollectionSelector(ActiveCollections active, TutorialService tutorial, IncognitoService incognito) : IPanel
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
public ReadOnlySpan<byte> Id
|
||||||
private readonly CommunicatorService _communicator;
|
=> "##cs"u8;
|
||||||
private readonly CollectionStorage _storage;
|
|
||||||
private readonly ActiveCollections _active;
|
|
||||||
private readonly TutorialService _tutorial;
|
|
||||||
private readonly IncognitoService _incognito;
|
|
||||||
|
|
||||||
private ModCollection? _dragging;
|
private ModCollection? _dragging;
|
||||||
|
|
||||||
public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active,
|
public record struct Entry(ModCollection Collection, StringU8 Name, StringPair AnonymousName)
|
||||||
TutorialService tutorial, IncognitoService incognito)
|
|
||||||
: base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
|
|
||||||
{
|
{
|
||||||
_config = config;
|
public Entry(ModCollection collection)
|
||||||
_communicator = communicator;
|
: this(collection,
|
||||||
_storage = storage;
|
collection.Identity.Name.Length > 0 ? new StringU8(collection.Identity.Name) : new StringU8(collection.Identity.AnonymizedName),
|
||||||
_active = active;
|
new StringPair(collection.Identity.AnonymizedName))
|
||||||
_tutorial = tutorial;
|
{ }
|
||||||
_incognito = incognito;
|
|
||||||
|
|
||||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector);
|
|
||||||
// Set items.
|
|
||||||
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty));
|
|
||||||
// Set selection.
|
|
||||||
OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnDelete(int idx)
|
|
||||||
{
|
|
||||||
if (idx < 0 || idx >= Items.Count)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Always return false since we handle the selection update ourselves.
|
|
||||||
_storage.RemoveCollection(Items[idx]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool DeleteButtonEnabled()
|
|
||||||
=> _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
|
|
||||||
|
|
||||||
protected override string DeleteButtonTooltip()
|
|
||||||
=> _storage.DefaultNamed == Current
|
|
||||||
? $"The selected collection {Name(Current)} can not be deleted."
|
|
||||||
: $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete.";
|
|
||||||
|
|
||||||
protected override bool OnAdd(string name)
|
|
||||||
=> _storage.AddCollection(name, null);
|
|
||||||
|
|
||||||
protected override bool OnDuplicate(string name, int idx)
|
|
||||||
{
|
|
||||||
if (idx < 0 || idx >= Items.Count)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return _storage.AddCollection(name, Items[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool Filtered(int idx)
|
|
||||||
=> !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
protected override bool OnDraw(int idx)
|
|
||||||
{
|
|
||||||
using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
|
|
||||||
var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx);
|
|
||||||
using var source = Im.DragDrop.Source();
|
|
||||||
|
|
||||||
if (idx == CurrentIdx)
|
|
||||||
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
|
|
||||||
|
|
||||||
if (source)
|
|
||||||
{
|
|
||||||
_dragging = Items[idx];
|
|
||||||
source.SetPayload("Collection"u8);
|
|
||||||
Im.Text($"Assigning {Name(_dragging)} to...");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ret)
|
|
||||||
_active.SetCollection(Items[idx], CollectionType.Current);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
|
public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
|
||||||
|
|
@ -98,45 +31,72 @@ public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposabl
|
||||||
if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8))
|
if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
|
active.SetCollection(_dragging, type, active.Individuals.GetGroup(identifier));
|
||||||
_dragging = null;
|
_dragging = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Draw()
|
||||||
{
|
{
|
||||||
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
|
Im.Cursor.Y += Im.Style.FramePadding.Y;
|
||||||
}
|
var cache = CacheManager.Instance.GetOrCreateCache<Cache>(Im.Id.Current);
|
||||||
|
using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
|
||||||
private string Name(ModCollection collection)
|
foreach (var item in cache)
|
||||||
=> _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name;
|
|
||||||
|
|
||||||
public void RestoreCollections()
|
|
||||||
{
|
|
||||||
Items.Clear();
|
|
||||||
Items.Add(_storage.DefaultNamed);
|
|
||||||
foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
|
|
||||||
Items.Add(c);
|
|
||||||
SetFilterDirty();
|
|
||||||
SetCurrent(_active.Current);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCollectionChange(in CollectionChange.Arguments arguments)
|
|
||||||
{
|
|
||||||
switch (arguments.Type)
|
|
||||||
{
|
{
|
||||||
case CollectionType.Temporary: return;
|
Im.Cursor.X += Im.Style.FramePadding.X;
|
||||||
case CollectionType.Current:
|
var ret = Im.Selectable(incognito.IncognitoMode ? item.AnonymousName : item.Name, active.Current == item.Collection);
|
||||||
if (arguments.NewCollection is not null)
|
using var source = Im.DragDrop.Source();
|
||||||
SetCurrent(arguments.NewCollection);
|
|
||||||
SetFilterDirty();
|
if (active.Current == item.Collection)
|
||||||
return;
|
tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
|
||||||
case CollectionType.Inactive:
|
|
||||||
RestoreCollections();
|
if (source)
|
||||||
SetFilterDirty();
|
{
|
||||||
return;
|
_dragging = item.Collection;
|
||||||
default:
|
source.SetPayload("Collection"u8);
|
||||||
SetFilterDirty();
|
Im.Text($"Assigning {(incognito.IncognitoMode ? item.AnonymousName : item.Name)} to...");
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
if (ret)
|
||||||
|
active.SetCollection(item.Collection, CollectionType.Current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class Cache : BasicFilterCache<Entry>, IService
|
||||||
|
{
|
||||||
|
private readonly CollectionStorage _collections;
|
||||||
|
private readonly CommunicatorService _communicator;
|
||||||
|
|
||||||
|
public Cache(CollectionFilter filter, CollectionStorage collections, CommunicatorService communicator)
|
||||||
|
: base(filter)
|
||||||
|
{
|
||||||
|
_collections = collections;
|
||||||
|
_communicator = communicator;
|
||||||
|
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelectorCache);
|
||||||
|
_communicator.CollectionRename.Subscribe(OnCollectionRename, CollectionRename.Priority.CollectionSelectorCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCollectionRename(in CollectionRename.Arguments arguments)
|
||||||
|
=> Dirty |= IManagedCache.DirtyFlags.Custom;
|
||||||
|
|
||||||
|
private void OnCollectionChange(in CollectionChange.Arguments arguments)
|
||||||
|
{
|
||||||
|
if (arguments.Type is CollectionType.Inactive)
|
||||||
|
Dirty |= IManagedCache.DirtyFlags.Custom;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<Entry> GetItems()
|
||||||
|
{
|
||||||
|
yield return new Entry(_collections.DefaultNamed);
|
||||||
|
|
||||||
|
foreach (var collection in _collections.Where(c => c != _collections.DefaultNamed).OrderBy(c => c.Identity.Name))
|
||||||
|
yield return new Entry(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
|
||||||
|
_communicator.CollectionRename.Unsubscribe(OnCollectionRename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 Luna;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
using Penumbra.UI.Tabs;
|
|
||||||
using TabType = Penumbra.Api.Enums.TabType;
|
using TabType = Penumbra.Api.Enums.TabType;
|
||||||
|
|
||||||
namespace Penumbra.UI.MainWindow;
|
namespace Penumbra.UI.MainWindow;
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,22 @@
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Groups;
|
using Penumbra.Mods.Groups;
|
||||||
using Penumbra.Mods.Settings;
|
using Penumbra.Mods.Settings;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.ModsTab.Groups;
|
namespace Penumbra.UI.ModsTab.Groups;
|
||||||
|
|
||||||
public sealed class ModGroupDrawer : IUiService
|
public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager, SingleGroupCombo combo)
|
||||||
|
: IUiService
|
||||||
{
|
{
|
||||||
private readonly List<(IModGroup, int)> _blockGroupCache = [];
|
private readonly List<(IModGroup, int)> _blockGroupCache = [];
|
||||||
private bool _temporary;
|
private bool _temporary;
|
||||||
private bool _locked;
|
private bool _locked;
|
||||||
private TemporaryModSettings? _tempSettings;
|
private TemporaryModSettings? _tempSettings;
|
||||||
private ModSettings? _settings;
|
private ModSettings? _settings;
|
||||||
private readonly SingleGroupCombo _combo;
|
|
||||||
private readonly Configuration _config;
|
|
||||||
private readonly CollectionManager _collectionManager;
|
|
||||||
|
|
||||||
public ModGroupDrawer(Configuration config, CollectionManager collectionManager)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_collectionManager = collectionManager;
|
|
||||||
_combo = new SingleGroupCombo(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class SingleGroupCombo(ModGroupDrawer parent)
|
|
||||||
: FilterComboCache<IModOption>(() => _group!.Options, MouseWheelType.Control, Penumbra.Log)
|
|
||||||
{
|
|
||||||
private static IModGroup? _group;
|
|
||||||
private static int _groupIdx;
|
|
||||||
|
|
||||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
|
||||||
{
|
|
||||||
var option = _group!.Options[globalIdx];
|
|
||||||
var ret = Im.Selectable(option.Name, globalIdx == CurrentSelectionIdx);
|
|
||||||
|
|
||||||
if (option.Description.Length > 0)
|
|
||||||
LunaStyle.DrawHelpMarker(option.Description, treatAsHovered: Im.Item.Hovered());
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string ToString(IModOption obj)
|
|
||||||
=> obj.Name;
|
|
||||||
|
|
||||||
public void Draw(IModGroup group, int groupIndex, int currentOption)
|
|
||||||
{
|
|
||||||
_group = group;
|
|
||||||
_groupIdx = groupIndex;
|
|
||||||
CurrentSelectionIdx = currentOption;
|
|
||||||
CurrentSelection = _group.Options[CurrentSelectionIdx];
|
|
||||||
if (Draw(string.Empty, CurrentSelection.Name, string.Empty, ref CurrentSelectionIdx, UiHelpers.InputTextWidth.X * 3 / 4,
|
|
||||||
Im.Style.TextHeightWithSpacing))
|
|
||||||
parent.SetModSetting(_group, _groupIdx, Setting.Single(CurrentSelectionIdx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings)
|
public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings)
|
||||||
{
|
{
|
||||||
|
|
@ -79,7 +36,7 @@ public sealed class ModGroupDrawer : IUiService
|
||||||
|
|
||||||
switch (group.Behaviour)
|
switch (group.Behaviour)
|
||||||
{
|
{
|
||||||
case GroupDrawBehaviour.SingleSelection when group.Options.Count <= _config.SingleGroupRadioMax:
|
case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax:
|
||||||
case GroupDrawBehaviour.MultiSelection:
|
case GroupDrawBehaviour.MultiSelection:
|
||||||
_blockGroupCache.Add((group, idx));
|
_blockGroupCache.Add((group, idx));
|
||||||
break;
|
break;
|
||||||
|
|
@ -120,9 +77,8 @@ public sealed class ModGroupDrawer : IUiService
|
||||||
private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting)
|
private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting)
|
||||||
{
|
{
|
||||||
using var id = Im.Id.Push(groupIdx);
|
using var id = Im.Id.Push(groupIdx);
|
||||||
var selectedOption = setting.AsIndex;
|
|
||||||
using var disabled = Im.Disabled(_locked);
|
using var disabled = Im.Disabled(_locked);
|
||||||
_combo.Draw(group, groupIdx, selectedOption);
|
combo.Draw(this, (SingleModGroup)group, groupIdx, setting);
|
||||||
if (group.Description.Length > 0)
|
if (group.Description.Length > 0)
|
||||||
{
|
{
|
||||||
LunaStyle.DrawHelpMarkerLabel(group.Name, group.Description);
|
LunaStyle.DrawHelpMarkerLabel(group.Name, group.Description);
|
||||||
|
|
@ -227,7 +183,7 @@ public sealed class ModGroupDrawer : IUiService
|
||||||
|
|
||||||
private void DrawCollapseHandling(IReadOnlyList<IModOption> options, float minWidth, Action draw)
|
private void DrawCollapseHandling(IReadOnlyList<IModOption> options, float minWidth, Action draw)
|
||||||
{
|
{
|
||||||
if (options.Count <= _config.OptionGroupCollapsibleMin)
|
if (options.Count <= config.OptionGroupCollapsibleMin)
|
||||||
{
|
{
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
@ -272,21 +228,21 @@ public sealed class ModGroupDrawer : IUiService
|
||||||
}
|
}
|
||||||
|
|
||||||
private ModCollection Current
|
private ModCollection Current
|
||||||
=> _collectionManager.Active.Current;
|
=> collectionManager.Active.Current;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
private void SetModSetting(IModGroup group, int groupIdx, Setting setting)
|
internal void SetModSetting(IModGroup group, int groupIdx, Setting setting)
|
||||||
{
|
{
|
||||||
if (_temporary || _config.DefaultTemporaryMode)
|
if (_temporary || config.DefaultTemporaryMode)
|
||||||
{
|
{
|
||||||
_tempSettings ??= new TemporaryModSettings(group.Mod, _settings);
|
_tempSettings ??= new TemporaryModSettings(group.Mod, _settings);
|
||||||
_tempSettings!.ForceInherit = false;
|
_tempSettings!.ForceInherit = false;
|
||||||
_tempSettings!.Settings[groupIdx] = setting;
|
_tempSettings!.Settings[groupIdx] = setting;
|
||||||
_collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings);
|
collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting);
|
collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.Collections.Cache;
|
using Penumbra.Collections.Cache;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,15 @@ public sealed class ModFileSystemCache(ModFileSystemDrawer parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update()
|
public override void Update()
|
||||||
{ }
|
{
|
||||||
|
if (ColorsDirty)
|
||||||
|
{
|
||||||
|
CollapsedFolderColor = ColorId.FolderCollapsed.Value().ToVector();
|
||||||
|
ExpandedFolderColor = ColorId.FolderExpanded.Value().ToVector();
|
||||||
|
LineColor = ColorId.FolderLine.Value().ToVector();
|
||||||
|
Dirty &= ~IManagedCache.DirtyFlags.Colors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override ModData ConvertNode(in IFileSystemNode node)
|
protected override ModData ConvertNode(in IFileSystemNode node)
|
||||||
=> new((IFileSystemData<Mod>)node);
|
=> new((IFileSystemData<Mod>)node);
|
||||||
|
|
|
||||||
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 ImSharp;
|
||||||
using Dalamud.Plugin;
|
using Luna;
|
||||||
using ImSharp;
|
using Penumbra.Api.Enums;
|
||||||
using Luna;
|
using Penumbra.UI.Classes;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.UI.CollectionTab;
|
||||||
using Penumbra.Collections.Manager;
|
|
||||||
using Penumbra.GameData.Actors;
|
namespace Penumbra.UI.Tabs;
|
||||||
using Penumbra.Mods.Manager;
|
|
||||||
using Penumbra.Services;
|
public sealed class CollectionsTab : TwoPanelLayout, ITab<TabType>
|
||||||
using Penumbra.UI.Classes;
|
{
|
||||||
using Penumbra.UI.CollectionTab;
|
private readonly TutorialService _tutorial;
|
||||||
|
|
||||||
namespace Penumbra.UI.Tabs;
|
public TabType Identifier
|
||||||
|
=> TabType.Collections;
|
||||||
public sealed class CollectionsTab : ITab<TabType>, IDisposable
|
|
||||||
{
|
public CollectionsTab(TutorialService tutorial, CollectionButtonFooter leftFooter, CollectionSelector leftPanel, CollectionFilter filter,
|
||||||
private readonly EphemeralConfig _config;
|
CollectionModeHeader rightHeader, CollectionPanel rightPanel)
|
||||||
private readonly CollectionSelector _selector;
|
{
|
||||||
private readonly CollectionPanel _panel;
|
LeftHeader = new FilterHeader<CollectionSelector.Entry>(filter, new StringU8("Filter..."u8));
|
||||||
private readonly TutorialService _tutorial;
|
LeftPanel = leftPanel;
|
||||||
private readonly IncognitoService _incognito;
|
LeftFooter = leftFooter;
|
||||||
|
RightHeader = rightHeader;
|
||||||
public enum PanelMode
|
RightPanel = rightPanel;
|
||||||
{
|
RightFooter = NopHeaderFooter.Instance;
|
||||||
SimpleAssignment,
|
_tutorial = tutorial;
|
||||||
IndividualAssignment,
|
}
|
||||||
GroupAssignment,
|
|
||||||
Details,
|
public override ReadOnlySpan<byte> Label
|
||||||
};
|
=> "Collections"u8;
|
||||||
|
|
||||||
public PanelMode Mode
|
protected override void DrawLeftGroup()
|
||||||
{
|
{
|
||||||
get => _config.CollectionPanel;
|
base.DrawLeftGroup();
|
||||||
set
|
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
|
||||||
{
|
}
|
||||||
_config.CollectionPanel = value;
|
|
||||||
_config.Save();
|
public void DrawContent()
|
||||||
}
|
=> Draw();
|
||||||
}
|
|
||||||
|
public void PostTabButton()
|
||||||
public TabType Identifier
|
=> _tutorial.OpenTutorial(BasicTutorialSteps.Collections);
|
||||||
=> TabType.Collections;
|
}
|
||||||
|
|
||||||
public CollectionsTab(IDalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito,
|
|
||||||
CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService)
|
|
||||||
{
|
|
||||||
_config = configuration.Ephemeral;
|
|
||||||
_tutorial = tutorial;
|
|
||||||
_incognito = incognito;
|
|
||||||
_selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito);
|
|
||||||
_panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, saveService, incognito);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_selector.Dispose();
|
|
||||||
_panel.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Label
|
|
||||||
=> "Collections"u8;
|
|
||||||
|
|
||||||
public void DrawContent()
|
|
||||||
{
|
|
||||||
var width = Im.Font.CalculateSize("nnnnnnnnnnnnnnnnnnnnnnnnnn"u8).X;
|
|
||||||
using (Im.Group())
|
|
||||||
{
|
|
||||||
_selector.Draw(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
|
|
||||||
|
|
||||||
Im.Line.Same();
|
|
||||||
using (Im.Group())
|
|
||||||
{
|
|
||||||
DrawHeaderLine();
|
|
||||||
DrawPanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DrawHeader()
|
|
||||||
{
|
|
||||||
_tutorial.OpenTutorial(BasicTutorialSteps.Collections);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawHeaderLine()
|
|
||||||
{
|
|
||||||
var withSpacing = Im.Style.FrameHeightWithSpacing;
|
|
||||||
using var style = ImStyleSingle.FrameRounding.Push(0).Push(ImStyleDouble.ItemSpacing, Vector2.Zero);
|
|
||||||
var buttonSize = new Vector2((Im.ContentRegion.Available.X - withSpacing) / 4f, Im.Style.FrameHeight);
|
|
||||||
|
|
||||||
using var _ = Im.Group();
|
|
||||||
var tabSelectedColor = Im.Style[ImGuiColor.TabSelected];
|
|
||||||
using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is PanelMode.SimpleAssignment);
|
|
||||||
if (Im.Button("Simple Assignments"u8, buttonSize))
|
|
||||||
Mode = PanelMode.SimpleAssignment;
|
|
||||||
color.Pop();
|
|
||||||
_tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments);
|
|
||||||
Im.Line.Same();
|
|
||||||
|
|
||||||
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.IndividualAssignment);
|
|
||||||
if (Im.Button("Individual Assignments"u8, buttonSize))
|
|
||||||
Mode = PanelMode.IndividualAssignment;
|
|
||||||
color.Pop();
|
|
||||||
_tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments);
|
|
||||||
Im.Line.Same();
|
|
||||||
|
|
||||||
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.GroupAssignment);
|
|
||||||
if (Im.Button("Group Assignments"u8, buttonSize))
|
|
||||||
Mode = PanelMode.GroupAssignment;
|
|
||||||
color.Pop();
|
|
||||||
_tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments);
|
|
||||||
Im.Line.Same();
|
|
||||||
|
|
||||||
color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.Details);
|
|
||||||
if (Im.Button("Collection Details"u8, buttonSize))
|
|
||||||
Mode = PanelMode.Details;
|
|
||||||
color.Pop();
|
|
||||||
_tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails);
|
|
||||||
Im.Line.Same();
|
|
||||||
|
|
||||||
_incognito.DrawToggle(withSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawPanel()
|
|
||||||
{
|
|
||||||
using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero);
|
|
||||||
using var child = Im.Child.Begin("##CollectionSettings"U8, Im.ContentRegion.Available with { Y = 0 }, true);
|
|
||||||
if (!child)
|
|
||||||
return;
|
|
||||||
|
|
||||||
style.Pop();
|
|
||||||
switch (Mode)
|
|
||||||
{
|
|
||||||
case PanelMode.SimpleAssignment:
|
|
||||||
_panel.DrawSimple();
|
|
||||||
break;
|
|
||||||
case PanelMode.IndividualAssignment:
|
|
||||||
_panel.DrawIndividualPanel();
|
|
||||||
break;
|
|
||||||
case PanelMode.GroupAssignment:
|
|
||||||
_panel.DrawGroupPanel();
|
|
||||||
break;
|
|
||||||
case PanelMode.Details:
|
|
||||||
_panel.DrawDetailsPanel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
style.Push(ImStyleDouble.ItemSpacing, Vector2.Zero);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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.Interface;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using ImSharp;
|
using ImSharp;
|
||||||
using Luna;
|
using Luna;
|
||||||
|
|
@ -15,7 +13,6 @@ using Penumbra.Api;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.DataContainers;
|
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Interop;
|
using Penumbra.GameData.Interop;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
|
|
@ -28,7 +25,6 @@ using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
using ImGuiClip = OtterGui.ImGuiClip;
|
|
||||||
using Penumbra.Api.IpcTester;
|
using Penumbra.Api.IpcTester;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
|
|
@ -65,54 +61,54 @@ public class Diagnostics(ServiceManager provider) : IUiService
|
||||||
|
|
||||||
public sealed class DebugTab : Window, ITab<TabType>
|
public sealed class DebugTab : Window, ITab<TabType>
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly CollectionManager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly ModManager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly ValidityChecker _validityChecker;
|
private readonly ValidityChecker _validityChecker;
|
||||||
private readonly HttpApi _httpApi;
|
private readonly HttpApi _httpApi;
|
||||||
private readonly ActorManager _actors;
|
private readonly ActorManager _actors;
|
||||||
private readonly StainService _stains;
|
private readonly StainService _stains;
|
||||||
private readonly GlobalVariablesDrawer _globalVariablesDrawer;
|
private readonly GlobalVariablesDrawer _globalVariablesDrawer;
|
||||||
private readonly ResourceManagerService _resourceManager;
|
private readonly ResourceManagerService _resourceManager;
|
||||||
private readonly ResourceLoader _resourceLoader;
|
private readonly ResourceLoader _resourceLoader;
|
||||||
private readonly CollectionResolver _collectionResolver;
|
private readonly CollectionResolver _collectionResolver;
|
||||||
private readonly DrawObjectState _drawObjectState;
|
private readonly DrawObjectState _drawObjectState;
|
||||||
private readonly PathState _pathState;
|
private readonly PathState _pathState;
|
||||||
private readonly SubfileHelper _subfileHelper;
|
private readonly SubfileHelper _subfileHelper;
|
||||||
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
|
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
|
||||||
private readonly CutsceneService _cutsceneService;
|
private readonly CutsceneService _cutsceneService;
|
||||||
private readonly ModImportManager _modImporter;
|
private readonly ModImportManager _modImporter;
|
||||||
private readonly ImportPopup _importPopup;
|
private readonly ImportPopup _importPopup;
|
||||||
private readonly FrameworkManager _framework;
|
private readonly FrameworkManager _framework;
|
||||||
private readonly TextureManager _textureManager;
|
private readonly TextureManager _textureManager;
|
||||||
private readonly ShaderReplacementFixer _shaderReplacementFixer;
|
private readonly ShaderReplacementFixer _shaderReplacementFixer;
|
||||||
private readonly RedrawService _redraws;
|
private readonly RedrawService _redraws;
|
||||||
private readonly DictEmote _emotes;
|
private readonly EmoteListDrawer _emotes;
|
||||||
private readonly Diagnostics _diagnostics;
|
private readonly Diagnostics _diagnostics;
|
||||||
private readonly ObjectManager _objects;
|
private readonly ObjectManager _objects;
|
||||||
private readonly IDataManager _dataManager;
|
private readonly IDataManager _dataManager;
|
||||||
private readonly IpcTester _ipcTester;
|
private readonly IpcTester _ipcTester;
|
||||||
private readonly CrashHandlerPanel _crashHandlerPanel;
|
private readonly CrashHandlerPanel _crashHandlerPanel;
|
||||||
private readonly TexHeaderDrawer _texHeaderDrawer;
|
private readonly TexHeaderDrawer _texHeaderDrawer;
|
||||||
private readonly HookOverrideDrawer _hookOverrides;
|
private readonly HookOverrideDrawer _hookOverrides;
|
||||||
private readonly RsfService _rsfService;
|
private readonly RsfService _rsfService;
|
||||||
private readonly SchedulerResourceManagementService _schedulerService;
|
private readonly ActionTmbListDrawer _actionTmbs;
|
||||||
private readonly ObjectIdentification _objectIdentification;
|
private readonly ObjectIdentification _objectIdentification;
|
||||||
private readonly RenderTargetDrawer _renderTargetDrawer;
|
private readonly RenderTargetDrawer _renderTargetDrawer;
|
||||||
private readonly ModMigratorDebug _modMigratorDebug;
|
private readonly ModMigratorDebug _modMigratorDebug;
|
||||||
private readonly ShapeInspector _shapeInspector;
|
private readonly ShapeInspector _shapeInspector;
|
||||||
private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer;
|
private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer;
|
||||||
private readonly DragDropManager _dragDropManager;
|
private readonly DragDropManager _dragDropManager;
|
||||||
|
|
||||||
public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager,
|
public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager,
|
||||||
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
|
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
|
||||||
ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver,
|
ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver,
|
||||||
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
||||||
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
|
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
|
||||||
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes,
|
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, EmoteListDrawer emotes,
|
||||||
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
|
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
|
||||||
HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer,
|
HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer,
|
||||||
SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer,
|
ActionTmbListDrawer actionTmbs, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer,
|
||||||
ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer,
|
ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer,
|
||||||
DragDropManager dragDropManager)
|
DragDropManager dragDropManager)
|
||||||
: base("Penumbra Debug Window", WindowFlags.NoCollapse)
|
: base("Penumbra Debug Window", WindowFlags.NoCollapse)
|
||||||
|
|
@ -152,7 +148,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
||||||
_hookOverrides = hookOverrides;
|
_hookOverrides = hookOverrides;
|
||||||
_rsfService = rsfService;
|
_rsfService = rsfService;
|
||||||
_globalVariablesDrawer = globalVariablesDrawer;
|
_globalVariablesDrawer = globalVariablesDrawer;
|
||||||
_schedulerService = schedulerService;
|
_actionTmbs = actionTmbs;
|
||||||
_objectIdentification = objectIdentification;
|
_objectIdentification = objectIdentification;
|
||||||
_renderTargetDrawer = renderTargetDrawer;
|
_renderTargetDrawer = renderTargetDrawer;
|
||||||
_modMigratorDebug = modMigratorDebug;
|
_modMigratorDebug = modMigratorDebug;
|
||||||
|
|
@ -205,7 +201,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
||||||
_globalVariablesDrawer.Draw();
|
_globalVariablesDrawer.Draw();
|
||||||
DrawCloudApi();
|
DrawCloudApi();
|
||||||
DrawDebugTabIpc();
|
DrawDebugTabIpc();
|
||||||
if(Im.Tree.Header("Drag & Drop Manager"u8))
|
if (Im.Tree.Header("Drag & Drop Manager"u8))
|
||||||
_dragDropManager.DrawDebugInfo();
|
_dragDropManager.DrawDebugInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -742,7 +738,7 @@ public sealed class DebugTab : Window, ITab<TabType>
|
||||||
{
|
{
|
||||||
using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit);
|
using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit);
|
||||||
if (table)
|
if (table)
|
||||||
foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key))
|
foreach (var (id, name) in _actionTmbs.Service.ListedTmbs.OrderBy(kvp => kvp.Key))
|
||||||
table.DrawDataPair($"{id:D6}", name.Span);
|
table.DrawDataPair($"{id:D6}", name.Span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -814,11 +810,6 @@ public sealed class DebugTab : Window, ITab<TabType>
|
||||||
Im.Selectable(item.Key);
|
Im.Selectable(item.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private string _emoteSearchFile = string.Empty;
|
|
||||||
private string _emoteSearchName = string.Empty;
|
|
||||||
|
|
||||||
|
|
||||||
private AtchFile? _atchFile;
|
private AtchFile? _atchFile;
|
||||||
|
|
||||||
private void DrawAtch()
|
private void DrawAtch()
|
||||||
|
|
@ -842,57 +833,19 @@ public sealed class DebugTab : Window, ITab<TabType>
|
||||||
AtchDrawer.Draw(_atchFile);
|
AtchDrawer.Draw(_atchFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void DrawEmotes()
|
private void DrawEmotes()
|
||||||
{
|
{
|
||||||
using var mainTree = Im.Tree.Node("Emotes"u8);
|
using var mainTree = Im.Tree.Node("Emotes"u8);
|
||||||
if (!mainTree)
|
if (mainTree)
|
||||||
return;
|
_emotes.Draw();
|
||||||
|
|
||||||
Im.Input.Text("File Name"u8, ref _emoteSearchFile);
|
|
||||||
Im.Input.Text("Emote Name"u8, ref _emoteSearchName);
|
|
||||||
using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit,
|
|
||||||
new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing));
|
|
||||||
if (!table)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing);
|
|
||||||
var dummy = ImGuiClip.FilteredClippedDraw(_emotes, skips,
|
|
||||||
p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& (_emoteSearchName.Length == 0
|
|
||||||
|| p.Value.Any(s => s.Name.ToDalamudString().TextValue.Contains(_emoteSearchName, StringComparison.OrdinalIgnoreCase))),
|
|
||||||
p =>
|
|
||||||
{
|
|
||||||
Im.Table.DrawColumn(p.Key);
|
|
||||||
Im.Table.DrawColumn(StringU8.Join(", "u8, p.Value.Select(v => v.Name.ToDalamudString().TextValue)));
|
|
||||||
});
|
|
||||||
ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _tmbKeyFilter = string.Empty;
|
|
||||||
private CiByteString _tmbKeyFilterU8 = CiByteString.Empty;
|
|
||||||
|
|
||||||
private void DrawActionTmbs()
|
private void DrawActionTmbs()
|
||||||
{
|
{
|
||||||
using var mainTree = Im.Tree.Node("Action TMBs"u8);
|
using var mainTree = Im.Tree.Node("Action TMBs"u8);
|
||||||
if (!mainTree)
|
if (mainTree)
|
||||||
return;
|
_actionTmbs.Draw();
|
||||||
|
|
||||||
if (Im.Input.Text("Key"u8, ref _tmbKeyFilter))
|
|
||||||
_tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty;
|
|
||||||
using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit,
|
|
||||||
new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing));
|
|
||||||
if (!table)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing);
|
|
||||||
var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips,
|
|
||||||
kvp => kvp.Key.Contains(_tmbKeyFilterU8),
|
|
||||||
p =>
|
|
||||||
{
|
|
||||||
Im.Table.DrawColumn($"{p.Value}");
|
|
||||||
Im.Table.DrawColumn(p.Key.Span);
|
|
||||||
});
|
|
||||||
ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawStainTemplates()
|
private void DrawStainTemplates()
|
||||||
|
|
|
||||||
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