mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'master' into dtme
# Conflicts: # Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs # Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs
This commit is contained in:
commit
30d10d5a26
44 changed files with 1740 additions and 443 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 86249598afb71601b247f9629d9c29dbecfe6eb1
|
||||
Subproject commit 759a8e9dc50b3453cdb7c3cba76de7174c94aba0
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a
|
||||
Subproject commit f2734d543d9b2debecb8feb6d6fa928801eb2bcb
|
||||
|
|
@ -36,7 +36,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
|
|||
collection = ModCollection.Empty;
|
||||
|
||||
if (collection.HasCache)
|
||||
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2);
|
||||
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject());
|
||||
|
||||
Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -134,6 +134,6 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
|
||||
public Dictionary<string, object?> GetChangedItems(string modDirectory, string modName)
|
||||
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject())
|
||||
: [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.UI;
|
||||
|
|
@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
|
|||
public void CloseMainWindow()
|
||||
=> _configWindow.IsOpen = false;
|
||||
|
||||
private void OnChangedItemClick(MouseButton button, object? data)
|
||||
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data)
|
||||
{
|
||||
if (ChangedItemClicked == null)
|
||||
return;
|
||||
|
||||
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
|
||||
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
|
||||
ChangedItemClicked.Invoke(button, type, id);
|
||||
}
|
||||
|
||||
private void OnChangedItemHover(object? data)
|
||||
private void OnChangedItemHover(IIdentifiedObjectData? data)
|
||||
{
|
||||
if (ChangedItemTooltip == null)
|
||||
return;
|
||||
|
||||
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
|
||||
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
|
||||
ChangedItemTooltip.Invoke(type, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using OtterGui.Services;
|
|||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Data;
|
||||
using ImGuiClip = OtterGui.ImGuiClip;
|
||||
|
||||
namespace Penumbra.Api.IpcTester;
|
||||
|
|
@ -17,10 +17,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
|||
{
|
||||
private int _objectIdx;
|
||||
private string _collectionIdString = string.Empty;
|
||||
private Guid? _collectionId = null;
|
||||
private bool _allowCreation = true;
|
||||
private bool _allowDeletion = true;
|
||||
private ApiCollectionType _type = ApiCollectionType.Yourself;
|
||||
private Guid? _collectionId;
|
||||
private bool _allowCreation = true;
|
||||
private bool _allowDeletion = true;
|
||||
private ApiCollectionType _type = ApiCollectionType.Yourself;
|
||||
|
||||
private Dictionary<Guid, string> _collections = [];
|
||||
private (string, ChangedItemType, uint)[] _changedItems = [];
|
||||
|
|
@ -116,7 +116,7 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
|||
var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
|
||||
_changedItems = items.Select(kvp =>
|
||||
{
|
||||
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value);
|
||||
var (type, id) = kvp.Value.ToApiObject();
|
||||
return (kvp.Key, type, id);
|
||||
}).ToArray();
|
||||
ImGui.OpenPopup("Changed Item List");
|
||||
|
|
@ -130,9 +130,9 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
|||
if (!p)
|
||||
return;
|
||||
|
||||
using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
|
||||
using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
|
||||
{
|
||||
if (t)
|
||||
if (table)
|
||||
ImGuiClip.ClippedDraw(_changedItems, t =>
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn(t.Item1);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Penumbra.Communication;
|
|||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.GameData.Data;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
|
|
@ -18,14 +19,14 @@ public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority,
|
|||
/// </summary>
|
||||
public sealed class CollectionCache : IDisposable
|
||||
{
|
||||
private readonly CollectionCacheManager _manager;
|
||||
private readonly ModCollection _collection;
|
||||
public readonly CollectionModData ModData = new();
|
||||
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
|
||||
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
|
||||
public readonly CustomResourceCache CustomResources;
|
||||
public readonly MetaCache Meta;
|
||||
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
|
||||
private readonly CollectionCacheManager _manager;
|
||||
private readonly ModCollection _collection;
|
||||
public readonly CollectionModData ModData = new();
|
||||
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> _changedItems = [];
|
||||
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
|
||||
public readonly CustomResourceCache CustomResources;
|
||||
public readonly MetaCache Meta;
|
||||
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
|
||||
|
||||
public int Calculating = -1;
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ public sealed class CollectionCache : IDisposable
|
|||
private int _changedItemsSaveCounter = -1;
|
||||
|
||||
// Obtain currently changed items. Computes them if they haven't been computed before.
|
||||
public IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
|
||||
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -412,7 +413,7 @@ public sealed class CollectionCache : IDisposable
|
|||
// Skip IMCs because they would result in far too many false-positive items,
|
||||
// since they are per set instead of per item-slot/item/variant.
|
||||
var identifier = _manager.MetaFileManager.Identifier;
|
||||
var items = new SortedList<string, object?>(512);
|
||||
var items = new SortedList<string, IIdentifiedObjectData?>(512);
|
||||
|
||||
void AddItems(IMod mod)
|
||||
{
|
||||
|
|
@ -421,8 +422,8 @@ public sealed class CollectionCache : IDisposable
|
|||
if (!_changedItems.TryGetValue(name, out var data))
|
||||
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
|
||||
else if (!data.Item1.Contains(mod))
|
||||
_changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj);
|
||||
else if (obj is int x && data.Item2 is int y)
|
||||
_changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
|
||||
else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y)
|
||||
_changedItems[name] = (data.Item1, x + y);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods.Editor;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
|
@ -46,8 +46,8 @@ public partial class ModCollection
|
|||
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
|
||||
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
|
||||
|
||||
internal IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
|
||||
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, object?)>();
|
||||
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
|
||||
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)>();
|
||||
|
||||
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
|
||||
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Data;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ namespace Penumbra.Communication;
|
|||
/// <item>Parameter is the clicked object data if any. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class ChangedItemClick() : EventWrapper<MouseButton, object?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
|
||||
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.GameData.Data;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ namespace Penumbra.Communication;
|
|||
/// <item>Parameter is the hovered object data if any. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
|
||||
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,24 +23,24 @@ public class EphemeralConfig : ISavable, IDisposable, IService
|
|||
[JsonIgnore]
|
||||
private readonly ModPathChanged _modPathChanged;
|
||||
|
||||
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
||||
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
|
||||
public bool DebugSeparateWindow { get; set; } = false;
|
||||
public int TutorialStep { get; set; } = 0;
|
||||
public bool EnableResourceLogging { get; set; } = false;
|
||||
public string ResourceLoggingFilter { get; set; } = string.Empty;
|
||||
public bool EnableResourceWatcher { get; set; } = false;
|
||||
public bool OnlyAddMatchingResources { get; set; } = true;
|
||||
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
|
||||
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
|
||||
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
|
||||
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
|
||||
public TabType SelectedTab { get; set; } = TabType.Settings;
|
||||
public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags;
|
||||
public bool FixMainWindow { get; set; } = false;
|
||||
public string LastModPath { get; set; } = string.Empty;
|
||||
public bool AdvancedEditingOpen { get; set; } = false;
|
||||
public bool ForceRedrawOnFileChange { get; set; } = false;
|
||||
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
||||
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
|
||||
public bool DebugSeparateWindow { get; set; } = false;
|
||||
public int TutorialStep { get; set; } = 0;
|
||||
public bool EnableResourceLogging { get; set; } = false;
|
||||
public string ResourceLoggingFilter { get; set; } = string.Empty;
|
||||
public bool EnableResourceWatcher { get; set; } = false;
|
||||
public bool OnlyAddMatchingResources { get; set; } = true;
|
||||
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
|
||||
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
|
||||
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
|
||||
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
|
||||
public TabType SelectedTab { get; set; } = TabType.Settings;
|
||||
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
|
||||
public bool FixMainWindow { get; set; } = false;
|
||||
public string LastModPath { get; set; } = string.Empty;
|
||||
public bool AdvancedEditingOpen { get; set; } = false;
|
||||
public bool ForceRedrawOnFileChange { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Load the current configuration.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using OtterGui.Text.HelperObjects;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
|
@ -8,26 +9,33 @@ using Penumbra.Meta.Manipulations;
|
|||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
using static Penumbra.Interop.Structs.StructExtensions;
|
||||
using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
|
||||
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||
|
||||
namespace Penumbra.Interop.ResourceTree;
|
||||
|
||||
internal partial record ResolveContext
|
||||
{
|
||||
private static bool IsEquipmentOrAccessorySlot(uint slotIndex)
|
||||
=> slotIndex is < 10 or 16 or 17;
|
||||
|
||||
private static bool IsEquipmentSlot(uint slotIndex)
|
||||
=> slotIndex is < 5 or 16 or 17;
|
||||
|
||||
private Utf8GamePath ResolveModelPath()
|
||||
{
|
||||
// Correctness:
|
||||
// Resolving a model path through the game's code can use EQDP metadata for human equipment models.
|
||||
return ModelType switch
|
||||
{
|
||||
ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(),
|
||||
_ => ResolveModelPathNative(),
|
||||
ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) => ResolveEquipmentModelPath(),
|
||||
_ => ResolveModelPathNative(),
|
||||
};
|
||||
}
|
||||
|
||||
private Utf8GamePath ResolveEquipmentModelPath()
|
||||
{
|
||||
var path = SlotIndex < 5
|
||||
var path = IsEquipmentSlot(SlotIndex)
|
||||
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot)
|
||||
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot);
|
||||
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
|
|
@ -39,7 +47,7 @@ internal partial record ResolveContext
|
|||
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId)
|
||||
{
|
||||
var slotIndex = slot.ToIndex();
|
||||
if (slotIndex >= 10 || ModelType != ModelType.Human)
|
||||
if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human)
|
||||
return GenderRace.MidlanderMale;
|
||||
|
||||
var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId;
|
||||
|
|
@ -80,7 +88,7 @@ internal partial record ResolveContext
|
|||
// Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata.
|
||||
return ModelType switch
|
||||
{
|
||||
ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b'
|
||||
ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) && mtrlFileName[8] != (byte)'b'
|
||||
=> ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
|
||||
|
|
@ -95,7 +103,7 @@ internal partial record ResolveContext
|
|||
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
|
||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||
|
||||
Span<byte> pathBuffer = stackalloc byte[260];
|
||||
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
|
||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
||||
|
||||
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||
|
|
@ -109,31 +117,25 @@ internal partial record ResolveContext
|
|||
if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c')
|
||||
return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty;
|
||||
|
||||
// MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand
|
||||
if (setIdHigh is 3 or 16 or 18 or 26)
|
||||
// Some offhands share materials with the corresponding mainhand
|
||||
if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId))
|
||||
{
|
||||
var setIdLow = Equipment.Set.Id % 100;
|
||||
if (setIdLow > 50)
|
||||
{
|
||||
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
|
||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
|
||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||
|
||||
var mirroredSetId = (ushort)(Equipment.Set.Id - 50);
|
||||
Span<byte> mirroredFileName = stackalloc byte[32];
|
||||
mirroredFileName = mirroredFileName[..fileName.Length];
|
||||
fileName.CopyTo(mirroredFileName);
|
||||
WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId.Id);
|
||||
|
||||
Span<byte> mirroredFileName = stackalloc byte[32];
|
||||
mirroredFileName = mirroredFileName[..fileName.Length];
|
||||
fileName.CopyTo(mirroredFileName);
|
||||
WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId);
|
||||
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
|
||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName);
|
||||
|
||||
Span<byte> pathBuffer = stackalloc byte[260];
|
||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName);
|
||||
var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8);
|
||||
if (weaponPosition >= 0)
|
||||
WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId.Id);
|
||||
|
||||
var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8);
|
||||
if (weaponPosition >= 0)
|
||||
WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId);
|
||||
|
||||
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||
}
|
||||
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
|
||||
|
|
@ -144,7 +146,7 @@ internal partial record ResolveContext
|
|||
var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant);
|
||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||
|
||||
Span<byte> pathBuffer = stackalloc byte[260];
|
||||
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
|
||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
||||
|
||||
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
||||
|
|
@ -175,13 +177,21 @@ internal partial record ResolveContext
|
|||
|
||||
var baseDirectory = modelPath[..modelPosition];
|
||||
|
||||
baseDirectory.CopyTo(materialPathBuffer);
|
||||
"/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]);
|
||||
WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant);
|
||||
materialPathBuffer[baseDirectory.Length + 15] = (byte)'/';
|
||||
mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]);
|
||||
var writer = new SpanTextWriter(materialPathBuffer);
|
||||
writer.Append(baseDirectory);
|
||||
writer.Append("/material/v"u8);
|
||||
WriteZeroPaddedNumber(ref writer, 4, variant);
|
||||
writer.Append((byte)'/');
|
||||
writer.Append(mtrlFileName);
|
||||
writer.EnsureNullTerminated();
|
||||
|
||||
return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)];
|
||||
return materialPathBuffer[..writer.Position];
|
||||
}
|
||||
|
||||
private static void WriteZeroPaddedNumber(ref SpanTextWriter writer, int width, ushort number)
|
||||
{
|
||||
WriteZeroPaddedNumber(writer.GetRemainingSpan()[..width], number);
|
||||
writer.Advance(width);
|
||||
}
|
||||
|
||||
private static void WriteZeroPaddedNumber(Span<byte> destination, ushort number)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using OtterGui;
|
||||
using OtterGui.Text.HelperObjects;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Data;
|
||||
|
|
@ -16,7 +16,7 @@ using Penumbra.String;
|
|||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI;
|
||||
using static Penumbra.Interop.Structs.StructExtensions;
|
||||
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||
using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
|
||||
|
||||
namespace Penumbra.Interop.ResourceTree;
|
||||
|
||||
|
|
@ -29,25 +29,25 @@ internal record GlobalResolveContext(
|
|||
{
|
||||
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
|
||||
|
||||
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
|
||||
public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
|
||||
EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default)
|
||||
=> new(this, characterBase, slotIndex, slot, equipment, secondaryId);
|
||||
}
|
||||
|
||||
internal unsafe partial record ResolveContext(
|
||||
GlobalResolveContext Global,
|
||||
Pointer<CharacterBase> CharacterBasePointer,
|
||||
Pointer<CharaBase> CharacterBasePointer,
|
||||
uint SlotIndex,
|
||||
EquipSlot Slot,
|
||||
CharacterArmor Equipment,
|
||||
SecondaryId SecondaryId)
|
||||
{
|
||||
public CharacterBase* CharacterBase
|
||||
public CharaBase* CharacterBase
|
||||
=> CharacterBasePointer.Value;
|
||||
|
||||
private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
|
||||
|
||||
private ModelType ModelType
|
||||
private CharaBase.ModelType ModelType
|
||||
=> CharacterBase->GetModelType();
|
||||
|
||||
private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath)
|
||||
|
|
@ -75,11 +75,14 @@ internal unsafe partial record ResolveContext(
|
|||
if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3)
|
||||
return null;
|
||||
|
||||
Span<byte> prefixed = stackalloc byte[260];
|
||||
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
||||
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
||||
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
||||
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
|
||||
Span<byte> prefixed = stackalloc byte[CharaBase.PathBufferSize];
|
||||
|
||||
var writer = new SpanTextWriter(prefixed);
|
||||
writer.Append(gamePath.Span[..(lastDirectorySeparator + 1)]);
|
||||
writer.Append((byte)'-');
|
||||
writer.Append((byte)'-');
|
||||
writer.Append(gamePath.Span[(lastDirectorySeparator + 1)..]);
|
||||
writer.EnsureNullTerminated();
|
||||
|
||||
if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp))
|
||||
return null;
|
||||
|
|
@ -350,7 +353,7 @@ internal unsafe partial record ResolveContext(
|
|||
_ => string.Empty,
|
||||
}
|
||||
+ item.Name;
|
||||
return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item));
|
||||
return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag());
|
||||
}
|
||||
|
||||
var dataFromPath = GuessUiDataFromPath(gamePath);
|
||||
|
|
@ -358,8 +361,8 @@ internal unsafe partial record ResolveContext(
|
|||
return dataFromPath;
|
||||
|
||||
return isEquipment
|
||||
? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot()))
|
||||
: new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown);
|
||||
? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag())
|
||||
: new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
|
||||
}
|
||||
|
||||
internal ResourceNode.UiData GuessUiDataFromPath(Utf8GamePath gamePath)
|
||||
|
|
@ -367,13 +370,13 @@ internal unsafe partial record ResolveContext(
|
|||
foreach (var obj in Global.Identifier.Identify(gamePath.ToString()))
|
||||
{
|
||||
var name = obj.Key;
|
||||
if (name.StartsWith("Customization:"))
|
||||
if (obj.Value is IdentifiedCustomization)
|
||||
name = name[14..].Trim();
|
||||
if (name != "Unknown")
|
||||
return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value));
|
||||
return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag());
|
||||
}
|
||||
|
||||
return new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown);
|
||||
return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
|
||||
}
|
||||
|
||||
private static string? SafeGet(ReadOnlySpan<string> array, Index index)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Penumbra.Api.Enums;
|
||||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon;
|
||||
using Penumbra.UI;
|
||||
|
||||
namespace Penumbra.Interop.ResourceTree;
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ public class ResourceNode : ICloneable
|
|||
{
|
||||
public string? Name;
|
||||
public string? FallbackName;
|
||||
public ChangedItemIcon Icon;
|
||||
public ChangedItemIconFlag IconFlag;
|
||||
public readonly ResourceType Type;
|
||||
public readonly nint ObjectAddress;
|
||||
public readonly nint ResourceHandle;
|
||||
|
|
@ -51,7 +51,7 @@ public class ResourceNode : ICloneable
|
|||
{
|
||||
Name = other.Name;
|
||||
FallbackName = other.FallbackName;
|
||||
Icon = other.Icon;
|
||||
IconFlag = other.IconFlag;
|
||||
Type = other.Type;
|
||||
ObjectAddress = other.ObjectAddress;
|
||||
ResourceHandle = other.ResourceHandle;
|
||||
|
|
@ -79,7 +79,7 @@ public class ResourceNode : ICloneable
|
|||
public void SetUiData(UiData uiData)
|
||||
{
|
||||
Name = uiData.Name;
|
||||
Icon = uiData.Icon;
|
||||
IconFlag = uiData.IconFlag;
|
||||
}
|
||||
|
||||
public void PrependName(string prefix)
|
||||
|
|
@ -88,9 +88,9 @@ public class ResourceNode : ICloneable
|
|||
Name = prefix + Name;
|
||||
}
|
||||
|
||||
public readonly record struct UiData(string? Name, ChangedItemIcon Icon)
|
||||
public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag)
|
||||
{
|
||||
public UiData PrependName(string prefix)
|
||||
=> Name == null ? this : new UiData(prefix + Name, Icon);
|
||||
=> Name == null ? this : new UiData(prefix + Name, IconFlag);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Penumbra.Interop.Hooks.PostProcessing;
|
|||
using Penumbra.UI;
|
||||
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
|
||||
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
|
||||
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||
|
||||
namespace Penumbra.Interop.ResourceTree;
|
||||
|
||||
|
|
@ -44,8 +45,8 @@ public class ResourceTree
|
|||
PlayerRelated = playerRelated;
|
||||
CollectionName = collectionName;
|
||||
AnonymizedCollectionName = anonymizedCollectionName;
|
||||
Nodes = new List<ResourceNode>();
|
||||
FlatNodes = new HashSet<ResourceNode>();
|
||||
Nodes = [];
|
||||
FlatNodes = [];
|
||||
}
|
||||
|
||||
public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action)
|
||||
|
|
@ -59,13 +60,13 @@ public class ResourceTree
|
|||
var character = (Character*)GameObjectAddress;
|
||||
var model = (CharacterBase*)DrawObjectAddress;
|
||||
var modelType = model->GetModelType();
|
||||
var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null;
|
||||
var human = modelType == ModelType.Human ? (Human*)model : null;
|
||||
var equipment = modelType switch
|
||||
{
|
||||
CharacterBase.ModelType.Human => new ReadOnlySpan<CharacterArmor>(&human->Head, 10),
|
||||
CharacterBase.ModelType.DemiHuman => new ReadOnlySpan<CharacterArmor>(
|
||||
ModelType.Human => new ReadOnlySpan<CharacterArmor>(&human->Head, 12),
|
||||
ModelType.DemiHuman => new ReadOnlySpan<CharacterArmor>(
|
||||
Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10),
|
||||
_ => ReadOnlySpan<CharacterArmor>.Empty,
|
||||
_ => [],
|
||||
};
|
||||
ModelId = character->CharacterData.ModelCharaId;
|
||||
CustomizeData = character->DrawData.CustomizeData;
|
||||
|
|
@ -75,9 +76,18 @@ public class ResourceTree
|
|||
|
||||
for (var i = 0u; i < model->SlotCount; ++i)
|
||||
{
|
||||
var slotContext = i < equipment.Length
|
||||
? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i])
|
||||
: globalContext.CreateContext(model, i);
|
||||
var slotContext = modelType switch
|
||||
{
|
||||
ModelType.Human => i switch
|
||||
{
|
||||
< 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]),
|
||||
16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]),
|
||||
_ => globalContext.CreateContext(model, i),
|
||||
},
|
||||
_ => i < equipment.Length
|
||||
? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i])
|
||||
: globalContext.CreateContext(model, i),
|
||||
};
|
||||
|
||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||
|
|
@ -117,7 +127,7 @@ public class ResourceTree
|
|||
|
||||
var subObject = (CharacterBase*)baseSubObject;
|
||||
|
||||
if (subObject->GetModelType() != CharacterBase.ModelType.Weapon)
|
||||
if (subObject->GetModelType() != ModelType.Weapon)
|
||||
continue;
|
||||
|
||||
var weapon = (Weapon*)subObject;
|
||||
|
|
@ -174,7 +184,7 @@ public class ResourceTree
|
|||
{
|
||||
pbdNode = pbdNode.Clone();
|
||||
pbdNode.FallbackName = "Racial Deformer";
|
||||
pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization;
|
||||
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(pbdNode);
|
||||
|
|
@ -192,7 +202,7 @@ public class ResourceTree
|
|||
{
|
||||
decalNode = decalNode.Clone();
|
||||
decalNode.FallbackName = "Face Decal";
|
||||
decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization;
|
||||
decalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(decalNode);
|
||||
|
|
@ -209,7 +219,7 @@ public class ResourceTree
|
|||
{
|
||||
legacyDecalNode = legacyDecalNode.Clone();
|
||||
legacyDecalNode.FallbackName = "Legacy Body Decal";
|
||||
legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization;
|
||||
legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization;
|
||||
}
|
||||
|
||||
Nodes.Add(legacyDecalNode);
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ internal static class ResourceTreeApiHelper
|
|||
continue;
|
||||
|
||||
var fullPath = node.FullPath.ToPath();
|
||||
resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon)));
|
||||
resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)node.IconFlag.ToApiIcon()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ internal static class ResourceTreeApiHelper
|
|||
var ret = new JObject
|
||||
{
|
||||
[nameof(ResourceNodeDto.Type)] = new JValue(node.Type),
|
||||
[nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)),
|
||||
[nameof(ResourceNodeDto.Icon)] = new JValue(node.IconFlag.ToApiIcon()),
|
||||
[nameof(ResourceNodeDto.Name)] = node.Name,
|
||||
[nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(),
|
||||
[nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge
|
|||
public Gender Gender
|
||||
=> GenderRace.Split().Item1;
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
=> identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot));
|
||||
|
||||
public MetaIndex FileIndex()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
|
|||
|
||||
public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable<EqpIdentifier>
|
||||
{
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
=> identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot));
|
||||
|
||||
public MetaIndex FileIndex()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende
|
|||
public Gender Gender
|
||||
=> GenderRace.Split().Item1;
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
{
|
||||
switch (Slot)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
|
|||
public override string ToString()
|
||||
=> $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}";
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
{
|
||||
var path = Type switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
|
|||
|
||||
public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable<GmpIdentifier>
|
||||
{
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
=> identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head));
|
||||
|
||||
public MetaIndex FileIndex()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public enum MetaManipulationType : byte
|
|||
|
||||
public interface IMetaIdentifier
|
||||
{
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems);
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems);
|
||||
|
||||
public MetaIndex FileIndex();
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier(
|
|||
: this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown)
|
||||
{ }
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
=> AddChangedItems(identifier, changedItems, false);
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems, bool allVariants)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems, bool allVariants)
|
||||
{
|
||||
var path = ObjectType switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Lumina.Excel.GeneratedSheets;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Data;
|
||||
|
|
@ -9,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
|
|||
|
||||
public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier
|
||||
{
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
=> changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null);
|
||||
|
||||
public MetaIndex FileIndex()
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public interface IModGroup
|
|||
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer);
|
||||
|
||||
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations);
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems);
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems);
|
||||
|
||||
/// <summary> Ensure that a value is valid for a group. </summary>
|
||||
public Setting FixSetting(Setting setting);
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ public class ImcModGroup(Mod mod) : IModGroup
|
|||
}
|
||||
}
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
=> Identifier.AddChangedItems(identifier, changedItems, AllVariants);
|
||||
|
||||
public Setting FixSetting(Setting setting)
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
}
|
||||
}
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
{
|
||||
foreach (var container in DataContainers)
|
||||
identifier.AddChangedItems(container, changedItems);
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
|
|||
OptionData[setting.AsIndex].AddDataTo(redirections, manipulations);
|
||||
}
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
{
|
||||
foreach (var container in DataContainers)
|
||||
identifier.AddChangedItems(container, changedItems);
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ public static class EquipmentSwap
|
|||
PrimaryId idTo, byte mtrlTo)
|
||||
{
|
||||
var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr);
|
||||
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
|
||||
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
|
||||
var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom);
|
||||
var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo);
|
||||
var meta = new MetaSwap<EqdpIdentifier, EqdpEntryInternal>(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier,
|
||||
|
|
@ -255,7 +255,8 @@ public static class EquipmentSwap
|
|||
{
|
||||
items = identifier.Identify(slotFrom.IsEquipment()
|
||||
? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)
|
||||
: GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType<EquipItem>()
|
||||
: GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom))
|
||||
.Select(kvp => kvp.Value).OfType<IdentifiedItem>().Select(i => i.Item)
|
||||
.ToArray();
|
||||
variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Groups;
|
||||
|
|
@ -100,7 +101,7 @@ public sealed class Mod : IMod
|
|||
}
|
||||
|
||||
// Cache
|
||||
public readonly SortedList<string, object?> ChangedItems = new();
|
||||
public readonly SortedList<string, IIdentifiedObjectData?> ChangedItems = new();
|
||||
|
||||
public string LowerChangedItemsString { get; internal set; } = string.Empty;
|
||||
public string AllTagsLower { get; internal set; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ using Penumbra.GameData.Enums;
|
|||
using Penumbra.UI;
|
||||
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
|
||||
using System.Xml.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||
|
||||
|
|
@ -109,16 +111,17 @@ public class Penumbra : IDalamudPlugin
|
|||
private void SetupApi()
|
||||
{
|
||||
_services.GetService<IpcProviders>();
|
||||
var itemSheet = _services.GetService<IDataManager>().GetExcelSheet<Item>()!;
|
||||
_communicatorService.ChangedItemHover.Subscribe(it =>
|
||||
{
|
||||
if (it is (Item, FullEquipType))
|
||||
if (it is IdentifiedItem)
|
||||
ImGui.TextUnformatted("Left Click to create an item link in chat.");
|
||||
}, ChangedItemHover.Priority.Link);
|
||||
|
||||
_communicatorService.ChangedItemClick.Subscribe((button, it) =>
|
||||
{
|
||||
if (button == MouseButton.Left && it is (Item item, FullEquipType type))
|
||||
Messager.LinkItem(item);
|
||||
if (button == MouseButton.Left && it is IdentifiedItem item && itemSheet.GetRow(item.Item.ItemId.Id) is { } i)
|
||||
Messager.LinkItem(i);
|
||||
}, ChangedItemClick.Priority.Link);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu
|
|||
_data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
|
||||
_config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionsTab.PanelMode>() ?? _config.Ephemeral.CollectionPanel;
|
||||
_config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab;
|
||||
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemDrawer.ChangedItemIcon>()
|
||||
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()
|
||||
?? _config.Ephemeral.ChangedItemFilter;
|
||||
_config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject<bool>() ?? _config.Ephemeral.FixMainWindow;
|
||||
_config.Ephemeral.Save();
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
|
|||
: FilterComboCache<(EquipItem Item, bool InMod)>(() =>
|
||||
{
|
||||
var list = data.ByType[type];
|
||||
if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is EquipItem i && i.Type == type))
|
||||
if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type))
|
||||
return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList();
|
||||
|
||||
return list.Select(i => (i, false)).ToList();
|
||||
|
|
|
|||
538
Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs
Normal file
538
Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
using Penumbra.String.Functions;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private static readonly float HalfMinValue = (float)Half.MinValue;
|
||||
private static readonly float HalfMaxValue = (float)Half.MaxValue;
|
||||
private static readonly float HalfEpsilon = (float)Half.Epsilon;
|
||||
|
||||
private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled)
|
||||
{
|
||||
if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable)
|
||||
return false;
|
||||
|
||||
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
||||
if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen))
|
||||
return false;
|
||||
|
||||
ColorTableCopyAllClipboardButton(tab.Mtrl);
|
||||
ImGui.SameLine();
|
||||
var ret = ColorTablePasteAllClipboardButton(tab, disabled);
|
||||
if (!disabled)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
|
||||
ImGui.SameLine();
|
||||
ret |= ColorTableDyeableCheckbox(tab);
|
||||
}
|
||||
|
||||
var hasDyeTable = tab.Mtrl.HasDyeTable;
|
||||
if (hasDyeTable)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0));
|
||||
ImGui.SameLine();
|
||||
ret |= DrawPreviewDye(tab, disabled);
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV);
|
||||
if (!table)
|
||||
return false;
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader(string.Empty);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Row");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Diffuse");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Specular");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Emissive");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Gloss");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Tile");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Repeat");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Skew");
|
||||
if (hasDyeTable)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Dye");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Dye Preview");
|
||||
}
|
||||
|
||||
for (var i = 0; i < ColorTable.NumRows; ++i)
|
||||
{
|
||||
ret |= DrawColorTableRow(tab, i, disabled);
|
||||
ImGui.TableNextRow();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
private static void ColorTableCopyAllClipboardButton(MtrlFile file)
|
||||
{
|
||||
if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0)))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var data1 = file.Table.AsBytes();
|
||||
var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan<byte>.Empty;
|
||||
var array = new byte[data1.Length + data2.Length];
|
||||
data1.TryCopyTo(array);
|
||||
data2.TryCopyTo(array.AsSpan(data1.Length));
|
||||
var text = Convert.ToBase64String(array);
|
||||
ImGui.SetClipboardText(text);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private bool DrawPreviewDye(MtrlTab tab, bool disabled)
|
||||
{
|
||||
var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection;
|
||||
var tt = dyeId == 0
|
||||
? "Select a preview dye first."
|
||||
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0))
|
||||
{
|
||||
var ret = false;
|
||||
if (tab.Mtrl.HasDyeTable)
|
||||
for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i)
|
||||
ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0);
|
||||
|
||||
tab.UpdateColorTablePreview();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye";
|
||||
if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss))
|
||||
tab.UpdateColorTablePreview();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled)
|
||||
{
|
||||
if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled)
|
||||
|| !tab.Mtrl.HasTable)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var text = ImGui.GetClipboardText();
|
||||
var data = Convert.FromBase64String(text);
|
||||
if (data.Length < Marshal.SizeOf<ColorTable>())
|
||||
return false;
|
||||
|
||||
ref var rows = ref tab.Mtrl.Table;
|
||||
fixed (void* ptr = data, output = &rows)
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf<ColorTable>());
|
||||
if (data.Length >= Marshal.SizeOf<ColorTable>() + Marshal.SizeOf<ColorDyeTable>()
|
||||
&& tab.Mtrl.HasDyeTable)
|
||||
{
|
||||
ref var dyeRows = ref tab.Mtrl.DyeTable;
|
||||
fixed (void* output2 = &dyeRows)
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf<ColorTable>(),
|
||||
Marshal.SizeOf<ColorDyeTable>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tab.UpdateColorTablePreview();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow dye)
|
||||
{
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||
"Export this row to your clipboard.", false, true))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Span<byte> data = stackalloc byte[ColorTableRow.Size + ColorDyeTableRow.Size];
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size);
|
||||
MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.Size);
|
||||
}
|
||||
|
||||
var text = Convert.ToBase64String(data);
|
||||
ImGui.SetClipboardText(text);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ColorTableDyeableCheckbox(MtrlTab tab)
|
||||
{
|
||||
var dyeable = tab.Mtrl.HasDyeTable;
|
||||
var ret = ImGui.Checkbox("Dyeable", ref dyeable);
|
||||
|
||||
if (ret)
|
||||
{
|
||||
tab.Mtrl.HasDyeTable = dyeable;
|
||||
tab.UpdateColorTablePreview();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, int rowIdx, bool disabled)
|
||||
{
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||
"Import an exported row from your clipboard onto this row.", disabled, true))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var text = ImGui.GetClipboardText();
|
||||
var data = Convert.FromBase64String(text);
|
||||
if (data.Length != ColorTableRow.Size + ColorDyeTableRow.Size
|
||||
|| !tab.Mtrl.HasTable)
|
||||
return false;
|
||||
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr;
|
||||
if (tab.Mtrl.HasDyeTable)
|
||||
tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.Size);
|
||||
}
|
||||
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled)
|
||||
{
|
||||
ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||
"Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
tab.HighlightColorTableRow(rowIdx);
|
||||
else if (tab.HighlightedColorTableRow == rowIdx)
|
||||
tab.CancelColorTableHighlight();
|
||||
}
|
||||
|
||||
private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled)
|
||||
{
|
||||
static bool FixFloat(ref float val, float current)
|
||||
{
|
||||
val = (float)(Half)val;
|
||||
return val != current;
|
||||
}
|
||||
|
||||
using var id = ImRaii.PushId(rowIdx);
|
||||
ref var row = ref tab.Mtrl.Table[rowIdx];
|
||||
var hasDye = tab.Mtrl.HasDyeTable;
|
||||
ref var dye = ref tab.Mtrl.DyeTable[rowIdx];
|
||||
var floatSize = 70 * UiHelpers.Scale;
|
||||
var intSize = 45 * UiHelpers.Scale;
|
||||
ImGui.TableNextColumn();
|
||||
ColorTableCopyClipboardButton(row, dye);
|
||||
ImGui.SameLine();
|
||||
var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled);
|
||||
ImGui.SameLine();
|
||||
ColorTableHighlightButton(tab, rowIdx, disabled);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"#{rowIdx + 1:D2}");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using var dis = ImRaii.Disabled(disabled);
|
||||
ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c =>
|
||||
{
|
||||
tab.Mtrl.Table[rowIdx].Diffuse = c;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
});
|
||||
if (hasDye)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse,
|
||||
b =>
|
||||
{
|
||||
tab.Mtrl.DyeTable[rowIdx].Diffuse = b;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}, ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c =>
|
||||
{
|
||||
tab.Mtrl.Table[rowIdx].Specular = c;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
});
|
||||
ImGui.SameLine();
|
||||
var tmpFloat = row.SpecularStrength;
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f")
|
||||
&& FixFloat(ref tmpFloat, row.SpecularStrength))
|
||||
{
|
||||
row.SpecularStrength = tmpFloat;
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
|
||||
if (hasDye)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular,
|
||||
b =>
|
||||
{
|
||||
tab.Mtrl.DyeTable[rowIdx].Specular = b;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}, ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength,
|
||||
b =>
|
||||
{
|
||||
tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}, ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c =>
|
||||
{
|
||||
tab.Mtrl.Table[rowIdx].Emissive = c;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
});
|
||||
if (hasDye)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive,
|
||||
b =>
|
||||
{
|
||||
tab.Mtrl.DyeTable[rowIdx].Emissive = b;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}, ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
tmpFloat = row.GlossStrength;
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon;
|
||||
if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f")
|
||||
&& FixFloat(ref tmpFloat, row.GlossStrength))
|
||||
{
|
||||
row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin);
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
if (hasDye)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss,
|
||||
b =>
|
||||
{
|
||||
tab.Mtrl.DyeTable[rowIdx].Gloss = b;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}, ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
int tmpInt = row.TileSet;
|
||||
ImGui.SetNextItemWidth(intSize);
|
||||
if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue)
|
||||
{
|
||||
row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63);
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
tmpFloat = row.MaterialRepeat.X;
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f")
|
||||
&& FixFloat(ref tmpFloat, row.MaterialRepeat.X))
|
||||
{
|
||||
row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
ImGui.SameLine();
|
||||
tmpFloat = row.MaterialRepeat.Y;
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f")
|
||||
&& FixFloat(ref tmpFloat, row.MaterialRepeat.Y))
|
||||
{
|
||||
row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
tmpFloat = row.MaterialSkew.X;
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X))
|
||||
{
|
||||
row.MaterialSkew = row.MaterialSkew with { X = tmpFloat };
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
|
||||
ImGui.SameLine();
|
||||
tmpFloat = row.MaterialSkew.Y;
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y))
|
||||
{
|
||||
row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
|
||||
if (hasDye)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
|
||||
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton))
|
||||
{
|
||||
dye.Template = _stainService.TemplateCombo.CurrentSelection;
|
||||
ret = true;
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize);
|
||||
}
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize)
|
||||
{
|
||||
var stain = _stainService.StainCombo.CurrentSelection.Key;
|
||||
if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry))
|
||||
return false;
|
||||
|
||||
var values = entry[(int)stain];
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2);
|
||||
|
||||
var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
|
||||
"Apply the selected dye to this row.", disabled, true);
|
||||
|
||||
ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0);
|
||||
if (ret)
|
||||
tab.UpdateColorTableRowPreview(rowIdx);
|
||||
|
||||
ImGui.SameLine();
|
||||
ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D");
|
||||
ImGui.SameLine();
|
||||
ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S");
|
||||
ImGui.SameLine();
|
||||
ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E");
|
||||
ImGui.SameLine();
|
||||
using var dis = ImRaii.Disabled();
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G");
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(floatSize);
|
||||
ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S");
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool ColorPicker(string label, string tooltip, Vector3 input, Action<Vector3> setter, string letter = "")
|
||||
{
|
||||
var ret = false;
|
||||
var inputSqrt = PseudoSqrtRgb(input);
|
||||
var tmp = inputSqrt;
|
||||
if (ImGui.ColorEdit3(label, ref tmp,
|
||||
ImGuiColorEditFlags.NoInputs
|
||||
| ImGuiColorEditFlags.DisplayRGB
|
||||
| ImGuiColorEditFlags.InputRGB
|
||||
| ImGuiColorEditFlags.NoTooltip
|
||||
| ImGuiColorEditFlags.HDR)
|
||||
&& tmp != inputSqrt)
|
||||
{
|
||||
setter(PseudoSquareRgb(tmp));
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if (letter.Length > 0 && ImGui.IsItemVisible())
|
||||
{
|
||||
var textSize = ImGui.CalcTextSize(letter);
|
||||
var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2;
|
||||
var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u;
|
||||
ImGui.GetWindowDrawList().AddText(center, textColor, letter);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Functions to deal with squared RGB values without making negatives useless.
|
||||
|
||||
private static float PseudoSquareRgb(float x)
|
||||
=> x < 0.0f ? -(x * x) : x * x;
|
||||
|
||||
private static Vector3 PseudoSquareRgb(Vector3 vec)
|
||||
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z));
|
||||
|
||||
private static Vector4 PseudoSquareRgb(Vector4 vec)
|
||||
=> new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W);
|
||||
|
||||
private static float PseudoSqrtRgb(float x)
|
||||
=> x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x);
|
||||
|
||||
internal static Vector3 PseudoSqrtRgb(Vector3 vec)
|
||||
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z));
|
||||
|
||||
private static Vector4 PseudoSqrtRgb(Vector4 vec)
|
||||
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W);
|
||||
}
|
||||
783
Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs
Normal file
783
Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs
Normal file
|
|
@ -0,0 +1,783 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using ImGuiNET;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Hooks.Objects;
|
||||
using Penumbra.Interop.MaterialPreview;
|
||||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.Classes;
|
||||
using static Penumbra.GameData.Files.ShpkFile;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private sealed class MtrlTab : IWritable, IDisposable
|
||||
{
|
||||
private const int ShpkPrefixLength = 16;
|
||||
|
||||
private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true);
|
||||
|
||||
private readonly ModEditWindow _edit;
|
||||
public readonly MtrlFile Mtrl;
|
||||
public readonly string FilePath;
|
||||
public readonly bool Writable;
|
||||
|
||||
private string[]? _shpkNames;
|
||||
|
||||
public string ShaderHeader = "Shader###Shader";
|
||||
public FullPath LoadedShpkPath = FullPath.Empty;
|
||||
public string LoadedShpkPathName = string.Empty;
|
||||
public string LoadedShpkDevkitPathName = string.Empty;
|
||||
public string ShaderComment = string.Empty;
|
||||
public ShpkFile? AssociatedShpk;
|
||||
public JObject? AssociatedShpkDevkit;
|
||||
|
||||
public readonly string LoadedBaseDevkitPathName;
|
||||
public readonly JObject? AssociatedBaseDevkit;
|
||||
|
||||
// Shader Key State
|
||||
public readonly
|
||||
List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)>
|
||||
Values)> ShaderKeys = new(16);
|
||||
|
||||
public readonly HashSet<int> VertexShaders = new(16);
|
||||
public readonly HashSet<int> PixelShaders = new(16);
|
||||
public bool ShadersKnown;
|
||||
public string VertexShadersString = "Vertex Shaders: ???";
|
||||
public string PixelShadersString = "Pixel Shaders: ???";
|
||||
|
||||
// Textures & Samplers
|
||||
public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4);
|
||||
|
||||
public readonly HashSet<int> UnfoldedTextures = new(4);
|
||||
public readonly HashSet<uint> SamplerIds = new(16);
|
||||
public float TextureLabelWidth;
|
||||
|
||||
// Material Constants
|
||||
public readonly
|
||||
List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)>
|
||||
Constants)> Constants = new(16);
|
||||
|
||||
// Live-Previewers
|
||||
public readonly List<LiveMaterialPreviewer> MaterialPreviewers = new(4);
|
||||
public readonly List<LiveColorTablePreviewer> ColorTablePreviewers = new(4);
|
||||
public int HighlightedColorTableRow = -1;
|
||||
public readonly Stopwatch HighlightTime = new();
|
||||
|
||||
public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath)
|
||||
{
|
||||
defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name);
|
||||
if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath))
|
||||
return FullPath.Empty;
|
||||
|
||||
return _edit.FindBestMatch(defaultGamePath);
|
||||
}
|
||||
|
||||
public string[] GetShpkNames()
|
||||
{
|
||||
if (null != _shpkNames)
|
||||
return _shpkNames;
|
||||
|
||||
var names = new HashSet<string>(StandardShaderPackages);
|
||||
names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..]));
|
||||
|
||||
_shpkNames = names.ToArray();
|
||||
Array.Sort(_shpkNames);
|
||||
|
||||
return _shpkNames;
|
||||
}
|
||||
|
||||
public void LoadShpk(FullPath path)
|
||||
{
|
||||
ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader";
|
||||
|
||||
try
|
||||
{
|
||||
LoadedShpkPath = path;
|
||||
var data = LoadedShpkPath.IsRooted
|
||||
? File.ReadAllBytes(LoadedShpkPath.FullName)
|
||||
: _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data;
|
||||
AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data.");
|
||||
LoadedShpkPathName = path.ToPath();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LoadedShpkPath = FullPath.Empty;
|
||||
LoadedShpkPathName = string.Empty;
|
||||
AssociatedShpk = null;
|
||||
Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false);
|
||||
}
|
||||
|
||||
if (LoadedShpkPath.InternalName.IsEmpty)
|
||||
{
|
||||
AssociatedShpkDevkit = null;
|
||||
LoadedShpkDevkitPathName = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
AssociatedShpkDevkit =
|
||||
TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName);
|
||||
}
|
||||
|
||||
UpdateShaderKeys();
|
||||
Update();
|
||||
}
|
||||
|
||||
private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath))
|
||||
throw new Exception("Could not assemble ShPk dev-kit path.");
|
||||
|
||||
var devkitFullPath = _edit.FindBestMatch(devkitPath);
|
||||
if (!devkitFullPath.IsRooted)
|
||||
throw new Exception("Could not resolve ShPk dev-kit path.");
|
||||
|
||||
devkitPathName = devkitFullPath.FullName;
|
||||
return JObject.Parse(File.ReadAllText(devkitFullPath.FullName));
|
||||
}
|
||||
catch
|
||||
{
|
||||
devkitPathName = string.Empty;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private T? TryGetShpkDevkitData<T>(string category, uint? id, bool mayVary) where T : class
|
||||
=> TryGetShpkDevkitData<T>(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary)
|
||||
?? TryGetShpkDevkitData<T>(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary);
|
||||
|
||||
private T? TryGetShpkDevkitData<T>(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class
|
||||
{
|
||||
if (devkit == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var data = devkit[category];
|
||||
if (id.HasValue)
|
||||
data = data?[id.Value.ToString()];
|
||||
|
||||
if (mayVary && (data as JObject)?["Vary"] != null)
|
||||
{
|
||||
var selector = BuildSelector(data!["Vary"]!
|
||||
.Select(key => (uint)key)
|
||||
.Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue));
|
||||
var index = (int)data["Selectors"]![selector.ToString()]!;
|
||||
data = data["Items"]![index];
|
||||
}
|
||||
|
||||
return data?.ToObject(typeof(T)) as T;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …)
|
||||
Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateShaderKeys()
|
||||
{
|
||||
ShaderKeys.Clear();
|
||||
if (AssociatedShpk != null)
|
||||
foreach (var key in AssociatedShpk.MaterialKeys)
|
||||
{
|
||||
var dkData = TryGetShpkDevkitData<DevkitShaderKey>("ShaderKeys", key.Id, false);
|
||||
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
|
||||
|
||||
var valueSet = new HashSet<uint>(key.Values);
|
||||
if (dkData != null)
|
||||
valueSet.UnionWith(dkData.Values.Keys);
|
||||
|
||||
var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue);
|
||||
var values = valueSet.Select<uint, (string Label, uint Value, string Description)>(value =>
|
||||
{
|
||||
if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue))
|
||||
return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description);
|
||||
|
||||
return ($"0x{value:X8}", value, string.Empty);
|
||||
}).ToArray();
|
||||
Array.Sort(values, (x, y) =>
|
||||
{
|
||||
if (x.Value == key.DefaultValue)
|
||||
return -1;
|
||||
if (y.Value == key.DefaultValue)
|
||||
return 1;
|
||||
|
||||
return string.Compare(x.Label, y.Label, StringComparison.Ordinal);
|
||||
});
|
||||
ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty,
|
||||
!hasDkLabel, values));
|
||||
}
|
||||
else
|
||||
foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex())
|
||||
ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>()));
|
||||
}
|
||||
|
||||
private void UpdateShaders()
|
||||
{
|
||||
VertexShaders.Clear();
|
||||
PixelShaders.Clear();
|
||||
if (AssociatedShpk == null)
|
||||
{
|
||||
ShadersKnown = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShadersKnown = true;
|
||||
var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray();
|
||||
var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray();
|
||||
var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray();
|
||||
var materialKeySelector =
|
||||
BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value));
|
||||
foreach (var systemKeySelector in systemKeySelectors)
|
||||
{
|
||||
foreach (var sceneKeySelector in sceneKeySelectors)
|
||||
{
|
||||
foreach (var subViewKeySelector in subViewKeySelectors)
|
||||
{
|
||||
var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector);
|
||||
var node = AssociatedShpk.GetNodeBySelector(selector);
|
||||
if (node.HasValue)
|
||||
foreach (var pass in node.Value.Passes)
|
||||
{
|
||||
VertexShaders.Add((int)pass.VertexShader);
|
||||
PixelShaders.Add((int)pass.PixelShader);
|
||||
}
|
||||
else
|
||||
ShadersKnown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}");
|
||||
var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}");
|
||||
|
||||
VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}";
|
||||
PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}";
|
||||
|
||||
ShaderComment = TryGetShpkDevkitData<string>("Comment", null, true) ?? string.Empty;
|
||||
}
|
||||
|
||||
private void UpdateTextures()
|
||||
{
|
||||
Textures.Clear();
|
||||
SamplerIds.Clear();
|
||||
if (AssociatedShpk == null)
|
||||
{
|
||||
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
|
||||
if (Mtrl.HasTable)
|
||||
SamplerIds.Add(TableSamplerId);
|
||||
|
||||
foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex())
|
||||
Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var index in VertexShaders)
|
||||
SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id));
|
||||
foreach (var index in PixelShaders)
|
||||
SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id));
|
||||
if (!ShadersKnown)
|
||||
{
|
||||
SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId));
|
||||
if (Mtrl.HasTable)
|
||||
SamplerIds.Add(TableSamplerId);
|
||||
}
|
||||
|
||||
foreach (var samplerId in SamplerIds)
|
||||
{
|
||||
var shpkSampler = AssociatedShpk.GetSamplerById(samplerId);
|
||||
if (shpkSampler is not { Slot: 2 })
|
||||
continue;
|
||||
|
||||
var dkData = TryGetShpkDevkitData<DevkitSampler>("Samplers", samplerId, true);
|
||||
var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label);
|
||||
|
||||
var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex);
|
||||
Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex,
|
||||
dkData?.Description ?? string.Empty, !hasDkLabel));
|
||||
}
|
||||
|
||||
if (SamplerIds.Contains(TableSamplerId))
|
||||
Mtrl.HasTable = true;
|
||||
}
|
||||
|
||||
Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label));
|
||||
|
||||
TextureLabelWidth = 50f * UiHelpers.Scale;
|
||||
|
||||
float helpWidth;
|
||||
using (var _ = ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X;
|
||||
}
|
||||
|
||||
foreach (var (label, _, _, description, monoFont) in Textures)
|
||||
{
|
||||
if (!monoFont)
|
||||
TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
|
||||
}
|
||||
|
||||
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
foreach (var (label, _, _, description, monoFont) in Textures)
|
||||
{
|
||||
if (monoFont)
|
||||
TextureLabelWidth = Math.Max(TextureLabelWidth,
|
||||
ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
|
||||
}
|
||||
|
||||
private void UpdateConstants()
|
||||
{
|
||||
static List<T> FindOrAddGroup<T>(List<(string, List<T>)> groups, string name)
|
||||
{
|
||||
foreach (var (groupName, group) in groups)
|
||||
{
|
||||
if (string.Equals(name, groupName, StringComparison.Ordinal))
|
||||
return group;
|
||||
}
|
||||
|
||||
var newGroup = new List<T>(16);
|
||||
groups.Add((name, newGroup));
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
Constants.Clear();
|
||||
if (AssociatedShpk == null)
|
||||
{
|
||||
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
|
||||
foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex())
|
||||
{
|
||||
var values = Mtrl.GetConstantValues(constant);
|
||||
for (var i = 0; i < values.Length; i += 4)
|
||||
{
|
||||
fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true,
|
||||
FloatConstantEditor.Default));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty;
|
||||
foreach (var shpkConstant in AssociatedShpk.MaterialParams)
|
||||
{
|
||||
if ((shpkConstant.ByteSize & 0x3) != 0)
|
||||
continue;
|
||||
|
||||
var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex);
|
||||
var values = Mtrl.GetConstantValues(constant);
|
||||
var handledElements = new IndexSet(values.Length, false);
|
||||
|
||||
var dkData = TryGetShpkDevkitData<DevkitConstant[]>("Constants", shpkConstant.Id, true);
|
||||
if (dkData != null)
|
||||
foreach (var dkConstant in dkData)
|
||||
{
|
||||
var offset = (int)dkConstant.Offset;
|
||||
var length = values.Length - offset;
|
||||
if (dkConstant.Length.HasValue)
|
||||
length = Math.Min(length, (int)dkConstant.Length.Value);
|
||||
if (length <= 0)
|
||||
continue;
|
||||
|
||||
var editor = dkConstant.CreateEditor();
|
||||
if (editor != null)
|
||||
FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants")
|
||||
.Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor));
|
||||
handledElements.AddRange(offset, length);
|
||||
}
|
||||
|
||||
var fcGroup = FindOrAddGroup(Constants, "Further Constants");
|
||||
foreach (var (start, end) in handledElements.Ranges(complement:true))
|
||||
{
|
||||
if ((shpkConstant.ByteOffset & 0x3) == 0)
|
||||
{
|
||||
var offset = shpkConstant.ByteOffset >> 2;
|
||||
for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j)
|
||||
{
|
||||
var rangeStart = Math.Max(i, start);
|
||||
var rangeEnd = Math.Min(i + 4, end);
|
||||
if (rangeEnd > rangeStart)
|
||||
fcGroup.Add((
|
||||
$"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})",
|
||||
constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = start; i < end; i += 4)
|
||||
{
|
||||
fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true,
|
||||
FloatConstantEditor.Default));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Constants.RemoveAll(group => group.Constants.Count == 0);
|
||||
Constants.Sort((x, y) =>
|
||||
{
|
||||
if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal))
|
||||
return 1;
|
||||
if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal))
|
||||
return -1;
|
||||
|
||||
return string.Compare(x.Header, y.Header, StringComparison.Ordinal);
|
||||
});
|
||||
// HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme
|
||||
foreach (var (_, group) in Constants)
|
||||
{
|
||||
group.Sort((x, y) => string.CompareOrdinal(
|
||||
x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label,
|
||||
y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label));
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void BindToMaterialInstances()
|
||||
{
|
||||
UnbindFromMaterialInstances();
|
||||
|
||||
var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address),
|
||||
FilePath);
|
||||
|
||||
var foundMaterials = new HashSet<nint>();
|
||||
foreach (var materialInfo in instances)
|
||||
{
|
||||
var material = materialInfo.GetDrawObjectMaterial(_edit._objects);
|
||||
if (foundMaterials.Contains((nint)material))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo));
|
||||
foundMaterials.Add((nint)material);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Carry on without that previewer.
|
||||
}
|
||||
}
|
||||
|
||||
UpdateMaterialPreview();
|
||||
|
||||
if (!Mtrl.HasTable)
|
||||
return;
|
||||
|
||||
foreach (var materialInfo in instances)
|
||||
{
|
||||
try
|
||||
{
|
||||
ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Carry on without that previewer.
|
||||
}
|
||||
}
|
||||
|
||||
UpdateColorTablePreview();
|
||||
}
|
||||
|
||||
private void UnbindFromMaterialInstances()
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.Dispose();
|
||||
MaterialPreviewers.Clear();
|
||||
|
||||
foreach (var previewer in ColorTablePreviewers)
|
||||
previewer.Dispose();
|
||||
ColorTablePreviewers.Clear();
|
||||
}
|
||||
|
||||
private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase)
|
||||
{
|
||||
for (var i = MaterialPreviewers.Count; i-- > 0;)
|
||||
{
|
||||
var previewer = MaterialPreviewers[i];
|
||||
if (previewer.DrawObject != characterBase)
|
||||
continue;
|
||||
|
||||
previewer.Dispose();
|
||||
MaterialPreviewers.RemoveAt(i);
|
||||
}
|
||||
|
||||
for (var i = ColorTablePreviewers.Count; i-- > 0;)
|
||||
{
|
||||
var previewer = ColorTablePreviewers[i];
|
||||
if (previewer.DrawObject != characterBase)
|
||||
continue;
|
||||
|
||||
previewer.Dispose();
|
||||
ColorTablePreviewers.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetShaderPackageFlags(uint shPkFlags)
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.SetShaderPackageFlags(shPkFlags);
|
||||
}
|
||||
|
||||
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.SetMaterialParameter(parameterCrc, offset, value);
|
||||
}
|
||||
|
||||
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
|
||||
{
|
||||
foreach (var previewer in MaterialPreviewers)
|
||||
previewer.SetSamplerFlags(samplerCrc, samplerFlags);
|
||||
}
|
||||
|
||||
private void UpdateMaterialPreview()
|
||||
{
|
||||
SetShaderPackageFlags(Mtrl.ShaderPackage.Flags);
|
||||
foreach (var constant in Mtrl.ShaderPackage.Constants)
|
||||
{
|
||||
var values = Mtrl.GetConstantValues(constant);
|
||||
if (values != null)
|
||||
SetMaterialParameter(constant.Id, 0, values);
|
||||
}
|
||||
|
||||
foreach (var sampler in Mtrl.ShaderPackage.Samplers)
|
||||
SetSamplerFlags(sampler.SamplerId, sampler.Flags);
|
||||
}
|
||||
|
||||
public void HighlightColorTableRow(int rowIdx)
|
||||
{
|
||||
var oldRowIdx = HighlightedColorTableRow;
|
||||
|
||||
if (HighlightedColorTableRow != rowIdx)
|
||||
{
|
||||
HighlightedColorTableRow = rowIdx;
|
||||
HighlightTime.Restart();
|
||||
}
|
||||
|
||||
if (oldRowIdx >= 0)
|
||||
UpdateColorTableRowPreview(oldRowIdx);
|
||||
if (rowIdx >= 0)
|
||||
UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
public void CancelColorTableHighlight()
|
||||
{
|
||||
var rowIdx = HighlightedColorTableRow;
|
||||
|
||||
HighlightedColorTableRow = -1;
|
||||
HighlightTime.Reset();
|
||||
|
||||
if (rowIdx >= 0)
|
||||
UpdateColorTableRowPreview(rowIdx);
|
||||
}
|
||||
|
||||
public void UpdateColorTableRowPreview(int rowIdx)
|
||||
{
|
||||
if (ColorTablePreviewers.Count == 0)
|
||||
return;
|
||||
|
||||
if (!Mtrl.HasTable)
|
||||
return;
|
||||
|
||||
var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]);
|
||||
if (Mtrl.HasDyeTable)
|
||||
{
|
||||
var stm = _edit._stainService.StmFile;
|
||||
var dye = new LegacyColorDyeTableRow(Mtrl.DyeTable[rowIdx]);
|
||||
if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes))
|
||||
row.ApplyDyeTemplate(dye, dyes);
|
||||
}
|
||||
|
||||
if (HighlightedColorTableRow == rowIdx)
|
||||
ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds);
|
||||
|
||||
foreach (var previewer in ColorTablePreviewers)
|
||||
{
|
||||
row.AsHalves().CopyTo(previewer.ColorTable.AsSpan()
|
||||
.Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4));
|
||||
previewer.ScheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateColorTablePreview()
|
||||
{
|
||||
if (ColorTablePreviewers.Count == 0)
|
||||
return;
|
||||
|
||||
if (!Mtrl.HasTable)
|
||||
return;
|
||||
|
||||
var rows = new LegacyColorTable(Mtrl.Table);
|
||||
var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable);
|
||||
if (Mtrl.HasDyeTable)
|
||||
{
|
||||
var stm = _edit._stainService.StmFile;
|
||||
var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key;
|
||||
for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i)
|
||||
{
|
||||
ref var row = ref rows[i];
|
||||
var dye = dyeRows[i];
|
||||
if (stm.TryGetValue(dye.Template, stainId, out var dyes))
|
||||
row.ApplyDyeTemplate(dye, dyes);
|
||||
}
|
||||
}
|
||||
|
||||
if (HighlightedColorTableRow >= 0)
|
||||
ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds);
|
||||
|
||||
foreach (var previewer in ColorTablePreviewers)
|
||||
{
|
||||
// TODO: Dawntrail
|
||||
rows.AsHalves().CopyTo(previewer.ColorTable);
|
||||
previewer.ScheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyHighlight(ref LegacyColorTableRow row, float time)
|
||||
{
|
||||
var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f;
|
||||
var baseColor = ColorId.InGameHighlight.Value();
|
||||
var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF);
|
||||
|
||||
row.Diffuse = Vector3.Zero;
|
||||
row.Specular = Vector3.Zero;
|
||||
row.Emissive = color * color;
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
UpdateShaders();
|
||||
UpdateTextures();
|
||||
UpdateConstants();
|
||||
}
|
||||
|
||||
public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
|
||||
{
|
||||
_edit = edit;
|
||||
Mtrl = file;
|
||||
FilePath = filePath;
|
||||
Writable = writable;
|
||||
AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName);
|
||||
LoadShpk(FindAssociatedShpk(out _, out _));
|
||||
if (writable)
|
||||
{
|
||||
_edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab);
|
||||
BindToMaterialInstances();
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
UnbindFromMaterialInstances();
|
||||
if (Writable)
|
||||
_edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances);
|
||||
}
|
||||
|
||||
// TODO Readd ShadersKnown
|
||||
public bool Valid
|
||||
=> (true || ShadersKnown) && Mtrl.Valid;
|
||||
|
||||
public byte[] Write()
|
||||
{
|
||||
var output = Mtrl.Clone();
|
||||
output.GarbageCollect(AssociatedShpk, SamplerIds);
|
||||
|
||||
return output.Write();
|
||||
}
|
||||
|
||||
private sealed class DevkitShaderKeyValue
|
||||
{
|
||||
public string Label = string.Empty;
|
||||
public string Description = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class DevkitShaderKey
|
||||
{
|
||||
public string Label = string.Empty;
|
||||
public string Description = string.Empty;
|
||||
public Dictionary<uint, DevkitShaderKeyValue> Values = new();
|
||||
}
|
||||
|
||||
private sealed class DevkitSampler
|
||||
{
|
||||
public string Label = string.Empty;
|
||||
public string Description = string.Empty;
|
||||
public string DefaultTexture = string.Empty;
|
||||
}
|
||||
|
||||
private enum DevkitConstantType
|
||||
{
|
||||
Hidden = -1,
|
||||
Float = 0,
|
||||
Integer = 1,
|
||||
Color = 2,
|
||||
Enum = 3,
|
||||
}
|
||||
|
||||
private sealed class DevkitConstantValue
|
||||
{
|
||||
public string Label = string.Empty;
|
||||
public string Description = string.Empty;
|
||||
public float Value = 0;
|
||||
}
|
||||
|
||||
private sealed class DevkitConstant
|
||||
{
|
||||
public uint Offset = 0;
|
||||
public uint? Length = null;
|
||||
public string Group = string.Empty;
|
||||
public string Label = string.Empty;
|
||||
public string Description = string.Empty;
|
||||
public DevkitConstantType Type = DevkitConstantType.Float;
|
||||
|
||||
public float? Minimum = null;
|
||||
public float? Maximum = null;
|
||||
public float? Speed = null;
|
||||
public float RelativeSpeed = 0.0f;
|
||||
public float Factor = 1.0f;
|
||||
public float Bias = 0.0f;
|
||||
public byte Precision = 3;
|
||||
public string Unit = string.Empty;
|
||||
|
||||
public bool SquaredRgb = false;
|
||||
public bool Clamped = false;
|
||||
|
||||
public DevkitConstantValue[] Values = Array.Empty<DevkitConstantValue>();
|
||||
|
||||
public IConstantEditor? CreateEditor()
|
||||
=> Type switch
|
||||
{
|
||||
DevkitConstantType.Hidden => null,
|
||||
DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision,
|
||||
Unit),
|
||||
DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed,
|
||||
Factor, Bias, Unit),
|
||||
DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped),
|
||||
DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values,
|
||||
value => (value.Label, value.Value, value.Description))),
|
||||
_ => FloatConstantEditor.Default,
|
||||
};
|
||||
|
||||
private static int? ToInteger(float? value)
|
||||
=> value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ public class ResourceTreeViewer
|
|||
private readonly Dictionary<nint, NodeVisibility> _filterCache;
|
||||
|
||||
private TreeCategory _categoryFilter;
|
||||
private ChangedItemDrawer.ChangedItemIcon _typeFilter;
|
||||
private ChangedItemIconFlag _typeFilter;
|
||||
private string _nameFilter;
|
||||
private string _nodeFilter;
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ public class ResourceTreeViewer
|
|||
_filterCache = [];
|
||||
|
||||
_categoryFilter = AllCategories;
|
||||
_typeFilter = ChangedItemDrawer.AllFlags;
|
||||
_typeFilter = ChangedItemFlagExtensions.AllFlags;
|
||||
_nameFilter = string.Empty;
|
||||
_nodeFilter = string.Empty;
|
||||
}
|
||||
|
|
@ -184,13 +184,13 @@ public class ResourceTreeViewer
|
|||
});
|
||||
|
||||
private void DrawNodes(IEnumerable<ResourceNode> resourceNodes, int level, nint pathHash,
|
||||
ChangedItemDrawer.ChangedItemIcon parentFilterIcon)
|
||||
ChangedItemIconFlag parentFilterIconFlag)
|
||||
{
|
||||
var debugMode = _config.DebugMode;
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f;
|
||||
|
||||
bool MatchesFilter(ResourceNode node, ChangedItemDrawer.ChangedItemIcon filterIcon)
|
||||
bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon)
|
||||
{
|
||||
if (!_typeFilter.HasFlag(filterIcon))
|
||||
return false;
|
||||
|
|
@ -204,12 +204,12 @@ public class ResourceTreeViewer
|
|||
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon)
|
||||
NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
|
||||
{
|
||||
if (node.Internal && !debugMode)
|
||||
return NodeVisibility.Hidden;
|
||||
|
||||
var filterIcon = node.Icon != 0 ? node.Icon : parentFilterIcon;
|
||||
var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon;
|
||||
if (MatchesFilter(node, filterIcon))
|
||||
return NodeVisibility.Visible;
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ public class ResourceTreeViewer
|
|||
return NodeVisibility.Hidden;
|
||||
}
|
||||
|
||||
NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon)
|
||||
NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
|
||||
{
|
||||
if (!_filterCache.TryGetValue(nodePathHash, out var visibility))
|
||||
{
|
||||
|
|
@ -240,13 +240,13 @@ public class ResourceTreeViewer
|
|||
{
|
||||
var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle);
|
||||
|
||||
var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIcon);
|
||||
var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIconFlag);
|
||||
if (visibility == NodeVisibility.Hidden)
|
||||
continue;
|
||||
|
||||
using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfTransparentText(), resourceNode.Internal);
|
||||
|
||||
var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon;
|
||||
var filterIcon = resourceNode.IconFlag != 0 ? resourceNode.IconFlag : parentFilterIconFlag;
|
||||
|
||||
using var id = ImRaii.PushId(index);
|
||||
ImGui.TableNextColumn();
|
||||
|
|
@ -277,7 +277,7 @@ public class ResourceTreeViewer
|
|||
ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X);
|
||||
}
|
||||
|
||||
_changedItemDrawer.DrawCategoryIcon(resourceNode.Icon);
|
||||
_changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag);
|
||||
ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X);
|
||||
ImGui.TableHeader(resourceNode.Name);
|
||||
if (ImGui.IsItemClicked() && unfoldable)
|
||||
|
|
|
|||
|
|
@ -3,72 +3,24 @@ using Dalamud.Interface.Textures;
|
|||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data.Files;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.UI.Classes;
|
||||
using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon;
|
||||
|
||||
namespace Penumbra.UI;
|
||||
|
||||
public class ChangedItemDrawer : IDisposable, IUiService
|
||||
{
|
||||
[Flags]
|
||||
public enum ChangedItemIcon : uint
|
||||
{
|
||||
Head = 0x00_00_01,
|
||||
Body = 0x00_00_02,
|
||||
Hands = 0x00_00_04,
|
||||
Legs = 0x00_00_08,
|
||||
Feet = 0x00_00_10,
|
||||
Ears = 0x00_00_20,
|
||||
Neck = 0x00_00_40,
|
||||
Wrists = 0x00_00_80,
|
||||
Finger = 0x00_01_00,
|
||||
Monster = 0x00_02_00,
|
||||
Demihuman = 0x00_04_00,
|
||||
Customization = 0x00_08_00,
|
||||
Action = 0x00_10_00,
|
||||
Mainhand = 0x00_20_00,
|
||||
Offhand = 0x00_40_00,
|
||||
Unknown = 0x00_80_00,
|
||||
Emote = 0x01_00_00,
|
||||
}
|
||||
private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToLowerInvariant()).ToArray();
|
||||
|
||||
private static readonly ChangedItemIcon[] Order =
|
||||
[
|
||||
ChangedItemIcon.Head,
|
||||
ChangedItemIcon.Body,
|
||||
ChangedItemIcon.Hands,
|
||||
ChangedItemIcon.Legs,
|
||||
ChangedItemIcon.Feet,
|
||||
ChangedItemIcon.Ears,
|
||||
ChangedItemIcon.Neck,
|
||||
ChangedItemIcon.Wrists,
|
||||
ChangedItemIcon.Finger,
|
||||
ChangedItemIcon.Mainhand,
|
||||
ChangedItemIcon.Offhand,
|
||||
ChangedItemIcon.Customization,
|
||||
ChangedItemIcon.Action,
|
||||
ChangedItemIcon.Emote,
|
||||
ChangedItemIcon.Monster,
|
||||
ChangedItemIcon.Demihuman,
|
||||
ChangedItemIcon.Unknown,
|
||||
];
|
||||
|
||||
private static readonly string[] LowerNames = Order.Select(f => ToDescription(f).ToLowerInvariant()).ToArray();
|
||||
|
||||
public static bool TryParseIndex(ReadOnlySpan<char> input, out ChangedItemIcon slot)
|
||||
public static bool TryParseIndex(ReadOnlySpan<char> input, out ChangedItemIconFlag slot)
|
||||
{
|
||||
// Handle numeric cases before TryParse because numbers
|
||||
// are not logical otherwise.
|
||||
|
|
@ -77,15 +29,15 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
// We assume users will use 1-based index, but if they enter 0, just use the first.
|
||||
if (idx == 0)
|
||||
{
|
||||
slot = Order[0];
|
||||
slot = ChangedItemFlagExtensions.Order[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use 1-based index.
|
||||
--idx;
|
||||
if (idx >= 0 && idx < Order.Length)
|
||||
if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count)
|
||||
{
|
||||
slot = Order[idx];
|
||||
slot = ChangedItemFlagExtensions.Order[idx];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,13 +46,13 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
return false;
|
||||
}
|
||||
|
||||
public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot)
|
||||
public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot)
|
||||
{
|
||||
if (TryParseIndex(lowerInput, out slot))
|
||||
return true;
|
||||
|
||||
slot = 0;
|
||||
foreach (var (item, flag) in LowerNames.Zip(Order))
|
||||
foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order))
|
||||
{
|
||||
if (item.Contains(lowerInput, StringComparison.Ordinal))
|
||||
slot |= flag;
|
||||
|
|
@ -109,15 +61,11 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
return slot != 0;
|
||||
}
|
||||
|
||||
public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF;
|
||||
public static readonly int NumCategories = Order.Length;
|
||||
public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand;
|
||||
|
||||
private readonly Configuration _config;
|
||||
private readonly ExcelSheet<Item> _items;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly Dictionary<ChangedItemIcon, IDalamudTextureWrap> _icons = new(16);
|
||||
private float _smallestIconWidth;
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly Dictionary<ChangedItemIconFlag, IDalamudTextureWrap> _icons = new(16);
|
||||
private float _smallestIconWidth;
|
||||
|
||||
public static Vector2 TypeFilterIconSize
|
||||
=> new(2 * ImGui.GetTextLineHeight());
|
||||
|
|
@ -125,7 +73,6 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator,
|
||||
Configuration config)
|
||||
{
|
||||
_items = gameData.GetExcelSheet<Item>()!;
|
||||
uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true);
|
||||
_communicator = communicator;
|
||||
_config = config;
|
||||
|
|
@ -139,18 +86,19 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
}
|
||||
|
||||
/// <summary> Check if a changed item should be drawn based on its category. </summary>
|
||||
public bool FilterChangedItem(string name, object? data, LowerString filter)
|
||||
=> (_config.Ephemeral.ChangedItemFilter == AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data)))
|
||||
&& (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data)));
|
||||
public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter)
|
||||
=> (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags
|
||||
|| _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag()))
|
||||
&& (filter.IsEmpty || !data.IsFilteredOut(name, filter));
|
||||
|
||||
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
|
||||
public void DrawCategoryIcon(string name, object? data)
|
||||
=> DrawCategoryIcon(GetCategoryIcon(name, data));
|
||||
public void DrawCategoryIcon(IIdentifiedObjectData? data)
|
||||
=> DrawCategoryIcon(data.GetIcon().ToFlag());
|
||||
|
||||
public void DrawCategoryIcon(ChangedItemIcon iconType)
|
||||
public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType)
|
||||
{
|
||||
var height = ImGui.GetFrameHeight();
|
||||
if (!_icons.TryGetValue(iconType, out var icon))
|
||||
if (!_icons.TryGetValue(iconFlagType, out var icon))
|
||||
{
|
||||
ImGui.Dummy(new Vector2(height));
|
||||
return;
|
||||
|
|
@ -162,18 +110,18 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
using var tt = ImRaii.Tooltip();
|
||||
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0);
|
||||
ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw a changed item, invoking the Api-Events for clicks and tooltips.
|
||||
/// Also draw the item Id in grey if requested.
|
||||
/// Also draw the item ID in grey if requested.
|
||||
/// </summary>
|
||||
public void DrawChangedItem(string name, object? data)
|
||||
public void DrawChangedItem(string name, IIdentifiedObjectData? data)
|
||||
{
|
||||
name = ChangedItemName(name, data);
|
||||
using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f))
|
||||
name = data?.ToName(name) ?? name;
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f))
|
||||
.Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2)))
|
||||
{
|
||||
var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
|
||||
|
|
@ -182,31 +130,34 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret;
|
||||
ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret;
|
||||
if (ret != MouseButton.None)
|
||||
_communicator.ChangedItemClick.Invoke(ret, Convert(data));
|
||||
_communicator.ChangedItemClick.Invoke(ret, data);
|
||||
}
|
||||
|
||||
if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered())
|
||||
{
|
||||
// We can not be sure that any subscriber actually prints something in any case.
|
||||
// Circumvent ugly blank tooltip with less-ugly useless tooltip.
|
||||
using var tt = ImRaii.Tooltip();
|
||||
using var group = ImRaii.Group();
|
||||
_communicator.ChangedItemHover.Invoke(Convert(data));
|
||||
group.Dispose();
|
||||
using var tt = ImRaii.Tooltip();
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
_communicator.ChangedItemHover.Invoke(data);
|
||||
}
|
||||
|
||||
if (ImGui.GetItemRectSize() == Vector2.Zero)
|
||||
ImGui.TextUnformatted("No actions available.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Draw the model information, right-justified. </summary>
|
||||
public void DrawModelData(object? data)
|
||||
public static void DrawModelData(IIdentifiedObjectData? data)
|
||||
{
|
||||
if (!GetChangedItemObject(data, out var text))
|
||||
var additionalData = data?.AdditionalData ?? string.Empty;
|
||||
if (additionalData.Length == 0)
|
||||
return;
|
||||
|
||||
ImGui.SameLine(ImGui.GetContentRegionAvail().X);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGuiUtil.RightJustify(text, ColorId.ItemId.Value());
|
||||
ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value());
|
||||
}
|
||||
|
||||
/// <summary> Draw a header line with the different icon types to filter them. </summary>
|
||||
|
|
@ -224,7 +175,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
}
|
||||
|
||||
/// <summary> Draw a header line with the different icon types to filter them. </summary>
|
||||
public bool DrawTypeFilter(ref ChangedItemIcon typeFilter)
|
||||
public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter)
|
||||
{
|
||||
var ret = false;
|
||||
using var _ = ImRaii.PushId("ChangedItemIconFilter");
|
||||
|
|
@ -232,16 +183,38 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
|
||||
bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter)
|
||||
foreach (var iconType in ChangedItemFlagExtensions.Order)
|
||||
{
|
||||
var ret = false;
|
||||
var icon = _icons[type];
|
||||
var flag = typeFilter.HasFlag(type);
|
||||
ret |= DrawIcon(iconType, ref typeFilter);
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X);
|
||||
ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One,
|
||||
typeFilter switch
|
||||
{
|
||||
0 => new Vector4(0.6f, 0.3f, 0.3f, 1f),
|
||||
ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f),
|
||||
_ => new Vector4(0.5f, 0.5f, 1f, 1f),
|
||||
});
|
||||
if (ImGui.IsItemClicked())
|
||||
{
|
||||
typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter)
|
||||
{
|
||||
var localRet = false;
|
||||
var icon = _icons[type];
|
||||
var flag = typeFilter.HasFlag(type);
|
||||
ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f));
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
typeFilter = flag ? typeFilter & ~type : typeFilter | type;
|
||||
ret = true;
|
||||
localRet = true;
|
||||
}
|
||||
|
||||
using var popup = ImRaii.ContextPopupItem(type.ToString());
|
||||
|
|
@ -249,7 +222,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
if (ImGui.MenuItem("Enable Only This"))
|
||||
{
|
||||
typeFilter = type;
|
||||
ret = true;
|
||||
localRet = true;
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
|
|
@ -258,165 +231,13 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
using var tt = ImRaii.Tooltip();
|
||||
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0);
|
||||
ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
foreach (var iconType in Order)
|
||||
{
|
||||
ret |= DrawIcon(iconType, ref typeFilter);
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X);
|
||||
ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One,
|
||||
typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) :
|
||||
typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f));
|
||||
if (ImGui.IsItemClicked())
|
||||
{
|
||||
typeFilter = typeFilter == AllFlags ? 0 : AllFlags;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary> Obtain the icon category corresponding to a changed item. </summary>
|
||||
internal static ChangedItemIcon GetCategoryIcon(string name, object? obj)
|
||||
{
|
||||
var iconType = ChangedItemIcon.Unknown;
|
||||
switch (obj)
|
||||
{
|
||||
case EquipItem it:
|
||||
iconType = GetCategoryIcon(it.Type.ToSlot());
|
||||
break;
|
||||
case ModelChara m:
|
||||
iconType = (CharacterBase.ModelType)m.Type switch
|
||||
{
|
||||
CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman,
|
||||
CharacterBase.ModelType.Monster => ChangedItemIcon.Monster,
|
||||
_ => ChangedItemIcon.Unknown,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (name.StartsWith("Action: "))
|
||||
iconType = ChangedItemIcon.Action;
|
||||
else if (name.StartsWith("Emote: "))
|
||||
iconType = ChangedItemIcon.Emote;
|
||||
else if (name.StartsWith("Customization: "))
|
||||
iconType = ChangedItemIcon.Customization;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return iconType;
|
||||
}
|
||||
|
||||
internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.MainHand => ChangedItemIcon.Mainhand,
|
||||
EquipSlot.OffHand => ChangedItemIcon.Offhand,
|
||||
EquipSlot.Head => ChangedItemIcon.Head,
|
||||
EquipSlot.Body => ChangedItemIcon.Body,
|
||||
EquipSlot.Hands => ChangedItemIcon.Hands,
|
||||
EquipSlot.Legs => ChangedItemIcon.Legs,
|
||||
EquipSlot.Feet => ChangedItemIcon.Feet,
|
||||
EquipSlot.Ears => ChangedItemIcon.Ears,
|
||||
EquipSlot.Neck => ChangedItemIcon.Neck,
|
||||
EquipSlot.Wrists => ChangedItemIcon.Wrists,
|
||||
EquipSlot.RFinger => ChangedItemIcon.Finger,
|
||||
_ => ChangedItemIcon.Unknown,
|
||||
};
|
||||
|
||||
/// <summary> Return more detailed object information in text, if it exists. </summary>
|
||||
private static bool GetChangedItemObject(object? obj, out string text)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case EquipItem it:
|
||||
text = it.ModelString;
|
||||
return true;
|
||||
case ModelChara m:
|
||||
text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})";
|
||||
return true;
|
||||
default:
|
||||
text = string.Empty;
|
||||
return false;
|
||||
return localRet;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> We need to transform the internal EquipItem type to the Lumina Item type for API-events. </summary>
|
||||
private object? Convert(object? data)
|
||||
{
|
||||
if (data is EquipItem it)
|
||||
return (_items.GetRow(it.ItemId.Id), it.Type);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private static string ToDescription(ChangedItemIcon icon)
|
||||
=> icon switch
|
||||
{
|
||||
ChangedItemIcon.Head => EquipSlot.Head.ToName(),
|
||||
ChangedItemIcon.Body => EquipSlot.Body.ToName(),
|
||||
ChangedItemIcon.Hands => EquipSlot.Hands.ToName(),
|
||||
ChangedItemIcon.Legs => EquipSlot.Legs.ToName(),
|
||||
ChangedItemIcon.Feet => EquipSlot.Feet.ToName(),
|
||||
ChangedItemIcon.Ears => EquipSlot.Ears.ToName(),
|
||||
ChangedItemIcon.Neck => EquipSlot.Neck.ToName(),
|
||||
ChangedItemIcon.Wrists => EquipSlot.Wrists.ToName(),
|
||||
ChangedItemIcon.Finger => "Ring",
|
||||
ChangedItemIcon.Monster => "Monster",
|
||||
ChangedItemIcon.Demihuman => "Demi-Human",
|
||||
ChangedItemIcon.Customization => "Customization",
|
||||
ChangedItemIcon.Action => "Action",
|
||||
ChangedItemIcon.Emote => "Emote",
|
||||
ChangedItemIcon.Mainhand => "Weapon (Mainhand)",
|
||||
ChangedItemIcon.Offhand => "Weapon (Offhand)",
|
||||
_ => "Other",
|
||||
};
|
||||
|
||||
internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon)
|
||||
=> icon switch
|
||||
{
|
||||
ChangedItemIcon.Head => ApiChangedItemIcon.Head,
|
||||
ChangedItemIcon.Body => ApiChangedItemIcon.Body,
|
||||
ChangedItemIcon.Hands => ApiChangedItemIcon.Hands,
|
||||
ChangedItemIcon.Legs => ApiChangedItemIcon.Legs,
|
||||
ChangedItemIcon.Feet => ApiChangedItemIcon.Feet,
|
||||
ChangedItemIcon.Ears => ApiChangedItemIcon.Ears,
|
||||
ChangedItemIcon.Neck => ApiChangedItemIcon.Neck,
|
||||
ChangedItemIcon.Wrists => ApiChangedItemIcon.Wrists,
|
||||
ChangedItemIcon.Finger => ApiChangedItemIcon.Finger,
|
||||
ChangedItemIcon.Monster => ApiChangedItemIcon.Monster,
|
||||
ChangedItemIcon.Demihuman => ApiChangedItemIcon.Demihuman,
|
||||
ChangedItemIcon.Customization => ApiChangedItemIcon.Customization,
|
||||
ChangedItemIcon.Action => ApiChangedItemIcon.Action,
|
||||
ChangedItemIcon.Emote => ApiChangedItemIcon.Emote,
|
||||
ChangedItemIcon.Mainhand => ApiChangedItemIcon.Mainhand,
|
||||
ChangedItemIcon.Offhand => ApiChangedItemIcon.Offhand,
|
||||
ChangedItemIcon.Unknown => ApiChangedItemIcon.Unknown,
|
||||
_ => ApiChangedItemIcon.None,
|
||||
};
|
||||
|
||||
/// <summary> Apply Changed Item Counters to the Name if necessary. </summary>
|
||||
private static string ChangedItemName(string name, object? data)
|
||||
=> data is int counter ? $"{counter} Files Manipulating {name}s" : name;
|
||||
|
||||
/// <summary> Add filterable information to the string. </summary>
|
||||
private static string ChangedItemFilterName(string name, object? data)
|
||||
=> data switch
|
||||
{
|
||||
int counter => $"{counter} Files Manipulating {name}s",
|
||||
EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}",
|
||||
ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}",
|
||||
_ => name,
|
||||
};
|
||||
|
||||
/// <summary> Initialize the icons. </summary>
|
||||
private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider)
|
||||
{
|
||||
|
|
@ -425,30 +246,30 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
if (!equipTypeIcons.Valid)
|
||||
return false;
|
||||
|
||||
void Add(ChangedItemIcon icon, IDalamudTextureWrap? tex)
|
||||
void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex)
|
||||
{
|
||||
if (tex != null)
|
||||
_icons.Add(icon, tex);
|
||||
}
|
||||
|
||||
Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0));
|
||||
Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1));
|
||||
Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2));
|
||||
Add(ChangedItemIcon.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3));
|
||||
Add(ChangedItemIcon.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5));
|
||||
Add(ChangedItemIcon.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6));
|
||||
Add(ChangedItemIcon.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7));
|
||||
Add(ChangedItemIcon.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8));
|
||||
Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9));
|
||||
Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10));
|
||||
Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11));
|
||||
Add(ChangedItemIcon.Monster, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062042_hr1.tex")!));
|
||||
Add(ChangedItemIcon.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062041_hr1.tex")!));
|
||||
Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
|
||||
Add(ChangedItemIcon.Action, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062001_hr1.tex")!));
|
||||
Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, textureProvider));
|
||||
Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, textureProvider));
|
||||
Add(AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/114000/114052_hr1.tex")!));
|
||||
Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0));
|
||||
Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1));
|
||||
Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2));
|
||||
Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3));
|
||||
Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5));
|
||||
Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6));
|
||||
Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7));
|
||||
Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8));
|
||||
Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9));
|
||||
Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10));
|
||||
Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11));
|
||||
Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062044_hr1.tex")!));
|
||||
Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
|
||||
Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062045_hr1.tex")!));
|
||||
Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062001_hr1.tex")!));
|
||||
Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider));
|
||||
Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider));
|
||||
Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/114000/114052_hr1.tex")!));
|
||||
|
||||
_smallestIconWidth = _icons.Values.Min(i => i.Width);
|
||||
|
||||
|
|
@ -487,6 +308,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
|
|||
}
|
||||
}
|
||||
|
||||
return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon");
|
||||
return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2,
|
||||
"Penumbra.EmoteItemIcon");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
Penumbra/UI/ChangedItemIconFlag.cs
Normal file
122
Penumbra/UI/ChangedItemIconFlag.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.UI;
|
||||
|
||||
[Flags]
|
||||
public enum ChangedItemIconFlag : uint
|
||||
{
|
||||
Head = 0x00_00_01,
|
||||
Body = 0x00_00_02,
|
||||
Hands = 0x00_00_04,
|
||||
Legs = 0x00_00_08,
|
||||
Feet = 0x00_00_10,
|
||||
Ears = 0x00_00_20,
|
||||
Neck = 0x00_00_40,
|
||||
Wrists = 0x00_00_80,
|
||||
Finger = 0x00_01_00,
|
||||
Monster = 0x00_02_00,
|
||||
Demihuman = 0x00_04_00,
|
||||
Customization = 0x00_08_00,
|
||||
Action = 0x00_10_00,
|
||||
Mainhand = 0x00_20_00,
|
||||
Offhand = 0x00_40_00,
|
||||
Unknown = 0x00_80_00,
|
||||
Emote = 0x01_00_00,
|
||||
}
|
||||
|
||||
public static class ChangedItemFlagExtensions
|
||||
{
|
||||
public static readonly IReadOnlyList<ChangedItemIconFlag> Order =
|
||||
[
|
||||
ChangedItemIconFlag.Head,
|
||||
ChangedItemIconFlag.Body,
|
||||
ChangedItemIconFlag.Hands,
|
||||
ChangedItemIconFlag.Legs,
|
||||
ChangedItemIconFlag.Feet,
|
||||
ChangedItemIconFlag.Ears,
|
||||
ChangedItemIconFlag.Neck,
|
||||
ChangedItemIconFlag.Wrists,
|
||||
ChangedItemIconFlag.Finger,
|
||||
ChangedItemIconFlag.Mainhand,
|
||||
ChangedItemIconFlag.Offhand,
|
||||
ChangedItemIconFlag.Customization,
|
||||
ChangedItemIconFlag.Action,
|
||||
ChangedItemIconFlag.Emote,
|
||||
ChangedItemIconFlag.Monster,
|
||||
ChangedItemIconFlag.Demihuman,
|
||||
ChangedItemIconFlag.Unknown,
|
||||
];
|
||||
|
||||
public const ChangedItemIconFlag AllFlags = (ChangedItemIconFlag)0x01FFFF;
|
||||
public static readonly int NumCategories = Order.Count;
|
||||
public const ChangedItemIconFlag DefaultFlags = AllFlags & ~ChangedItemIconFlag.Offhand;
|
||||
|
||||
public static string ToDescription(this ChangedItemIconFlag iconFlag)
|
||||
=> iconFlag switch
|
||||
{
|
||||
ChangedItemIconFlag.Head => EquipSlot.Head.ToName(),
|
||||
ChangedItemIconFlag.Body => EquipSlot.Body.ToName(),
|
||||
ChangedItemIconFlag.Hands => EquipSlot.Hands.ToName(),
|
||||
ChangedItemIconFlag.Legs => EquipSlot.Legs.ToName(),
|
||||
ChangedItemIconFlag.Feet => EquipSlot.Feet.ToName(),
|
||||
ChangedItemIconFlag.Ears => EquipSlot.Ears.ToName(),
|
||||
ChangedItemIconFlag.Neck => EquipSlot.Neck.ToName(),
|
||||
ChangedItemIconFlag.Wrists => EquipSlot.Wrists.ToName(),
|
||||
ChangedItemIconFlag.Finger => "Ring",
|
||||
ChangedItemIconFlag.Monster => "Monster",
|
||||
ChangedItemIconFlag.Demihuman => "Demi-Human",
|
||||
ChangedItemIconFlag.Customization => "Customization",
|
||||
ChangedItemIconFlag.Action => "Action",
|
||||
ChangedItemIconFlag.Emote => "Emote",
|
||||
ChangedItemIconFlag.Mainhand => "Weapon (Mainhand)",
|
||||
ChangedItemIconFlag.Offhand => "Weapon (Offhand)",
|
||||
_ => "Other",
|
||||
};
|
||||
|
||||
public static ChangedItemIcon ToApiIcon(this ChangedItemIconFlag iconFlag)
|
||||
=> iconFlag switch
|
||||
{
|
||||
ChangedItemIconFlag.Head => ChangedItemIcon.Head,
|
||||
ChangedItemIconFlag.Body => ChangedItemIcon.Body,
|
||||
ChangedItemIconFlag.Hands => ChangedItemIcon.Hands,
|
||||
ChangedItemIconFlag.Legs => ChangedItemIcon.Legs,
|
||||
ChangedItemIconFlag.Feet => ChangedItemIcon.Feet,
|
||||
ChangedItemIconFlag.Ears => ChangedItemIcon.Ears,
|
||||
ChangedItemIconFlag.Neck => ChangedItemIcon.Neck,
|
||||
ChangedItemIconFlag.Wrists => ChangedItemIcon.Wrists,
|
||||
ChangedItemIconFlag.Finger => ChangedItemIcon.Finger,
|
||||
ChangedItemIconFlag.Monster => ChangedItemIcon.Monster,
|
||||
ChangedItemIconFlag.Demihuman => ChangedItemIcon.Demihuman,
|
||||
ChangedItemIconFlag.Customization => ChangedItemIcon.Customization,
|
||||
ChangedItemIconFlag.Action => ChangedItemIcon.Action,
|
||||
ChangedItemIconFlag.Emote => ChangedItemIcon.Emote,
|
||||
ChangedItemIconFlag.Mainhand => ChangedItemIcon.Mainhand,
|
||||
ChangedItemIconFlag.Offhand => ChangedItemIcon.Offhand,
|
||||
ChangedItemIconFlag.Unknown => ChangedItemIcon.Unknown,
|
||||
_ => ChangedItemIcon.None,
|
||||
};
|
||||
|
||||
public static ChangedItemIconFlag ToFlag(this ChangedItemIcon icon)
|
||||
=> icon switch
|
||||
{
|
||||
ChangedItemIcon.Unknown => ChangedItemIconFlag.Unknown,
|
||||
ChangedItemIcon.Head => ChangedItemIconFlag.Head,
|
||||
ChangedItemIcon.Body => ChangedItemIconFlag.Body,
|
||||
ChangedItemIcon.Hands => ChangedItemIconFlag.Hands,
|
||||
ChangedItemIcon.Legs => ChangedItemIconFlag.Legs,
|
||||
ChangedItemIcon.Feet => ChangedItemIconFlag.Feet,
|
||||
ChangedItemIcon.Ears => ChangedItemIconFlag.Ears,
|
||||
ChangedItemIcon.Neck => ChangedItemIconFlag.Neck,
|
||||
ChangedItemIcon.Wrists => ChangedItemIconFlag.Wrists,
|
||||
ChangedItemIcon.Finger => ChangedItemIconFlag.Finger,
|
||||
ChangedItemIcon.Mainhand => ChangedItemIconFlag.Mainhand,
|
||||
ChangedItemIcon.Offhand => ChangedItemIconFlag.Offhand,
|
||||
ChangedItemIcon.Customization => ChangedItemIconFlag.Customization,
|
||||
ChangedItemIcon.Monster => ChangedItemIconFlag.Monster,
|
||||
ChangedItemIcon.Demihuman => ChangedItemIconFlag.Demihuman,
|
||||
ChangedItemIcon.Action => ChangedItemIconFlag.Action,
|
||||
ChangedItemIcon.Emote => ChangedItemIconFlag.Emote,
|
||||
_ => ChangedItemIconFlag.Unknown,
|
||||
};
|
||||
}
|
||||
|
|
@ -550,7 +550,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
+ "Enter t:[string] to filter for mods set to specific tags.\n"
|
||||
+ "Enter n:[string] to filter only for mod names and no paths.\n"
|
||||
+ "Enter a:[string] to filter for mods by specific authors.\n"
|
||||
+ $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemDrawer.NumCategories + 1} or partial category name).\n\n"
|
||||
+ $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemFlagExtensions.NumCategories + 1} or partial category name).\n\n"
|
||||
+ "Use None as a placeholder value that only matches empty lists or names.\n"
|
||||
+ "Regularly, a mod has to match all supplied criteria separately.\n"
|
||||
+ "Put a - in front of a search token to search only for mods not matching the criterion.\n"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using OtterGui.Classes;
|
|||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData.Data;
|
||||
|
||||
namespace Penumbra.UI.ModsTab;
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItem
|
|||
if (!table)
|
||||
return;
|
||||
|
||||
var zipList = ZipList.FromSortedList((SortedList<string, object?>)selector.Selected!.ChangedItems);
|
||||
var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems);
|
||||
var height = ImGui.GetFrameHeightWithSpacing();
|
||||
ImGui.TableNextColumn();
|
||||
var skips = ImGuiClip.GetNecessarySkips(height);
|
||||
|
|
@ -32,15 +33,15 @@ public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItem
|
|||
ImGuiClip.DrawEndDummy(remainder, height);
|
||||
}
|
||||
|
||||
private bool CheckFilter((string Name, object? Data) kvp)
|
||||
private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp)
|
||||
=> drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty);
|
||||
|
||||
private void DrawChangedItem((string Name, object? Data) kvp)
|
||||
private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
drawer.DrawCategoryIcon(kvp.Name, kvp.Data);
|
||||
drawer.DrawCategoryIcon(kvp.Data);
|
||||
ImGui.SameLine();
|
||||
drawer.DrawChangedItem(kvp.Name, kvp.Data);
|
||||
drawer.DrawModelData(kvp.Data);
|
||||
ChangedItemDrawer.DrawModelData(kvp.Data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,16 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType
|
|||
{
|
||||
public readonly struct Entry : ISplitterEntry<ModSearchType, Entry>
|
||||
{
|
||||
public string Needle { get; init; }
|
||||
public ModSearchType Type { get; init; }
|
||||
public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; }
|
||||
public string Needle { get; init; }
|
||||
public ModSearchType Type { get; init; }
|
||||
public ChangedItemIconFlag IconFlagFilter { get; init; }
|
||||
|
||||
public bool Contains(Entry other)
|
||||
{
|
||||
if (Type != other.Type)
|
||||
return false;
|
||||
if (Type is ModSearchType.Category)
|
||||
return IconFilter == other.IconFilter;
|
||||
return IconFlagFilter == other.IconFlagFilter;
|
||||
|
||||
return Needle.Contains(other.Needle);
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType
|
|||
if (ChangedItemDrawer.TryParsePartial(entry.Needle, out var icon))
|
||||
list[i] = entry with
|
||||
{
|
||||
IconFilter = icon,
|
||||
IconFlagFilter = icon,
|
||||
Needle = string.Empty,
|
||||
};
|
||||
else
|
||||
|
|
@ -110,7 +110,7 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType
|
|||
ModSearchType.Name => leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
|
||||
ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
|
||||
ModSearchType.Category => leaf.Value.ChangedItems.Any(p
|
||||
=> (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0),
|
||||
=> ((p.Value?.Icon.ToFlag() ?? ChangedItemIconFlag.Unknown) & entry.IconFlagFilter) != 0),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using OtterGui.Services;
|
|||
using OtterGui.Widgets;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Services;
|
||||
|
|
@ -66,22 +67,22 @@ public class ChangedItemsTab(
|
|||
}
|
||||
|
||||
/// <summary> Apply the current filters. </summary>
|
||||
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
|
||||
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item)
|
||||
=> drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
|
||||
&& (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
|
||||
|
||||
/// <summary> Draw a full column for a changed item. </summary>
|
||||
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
|
||||
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
|
||||
drawer.DrawCategoryIcon(item.Value.Item2);
|
||||
ImGui.SameLine();
|
||||
drawer.DrawChangedItem(item.Key, item.Value.Item2);
|
||||
ImGui.TableNextColumn();
|
||||
DrawModColumn(item.Value.Item1);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
drawer.DrawModelData(item.Value.Item2);
|
||||
ChangedItemDrawer.DrawModelData(item.Value.Item2);
|
||||
}
|
||||
|
||||
private void DrawModColumn(SingleArray<IMod> mods)
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ public class SettingsTab : ITab, IUiService
|
|||
_config.HideChangedItemFilters = v;
|
||||
if (v)
|
||||
{
|
||||
_config.Ephemeral.ChangedItemFilter = ChangedItemDrawer.AllFlags;
|
||||
_config.Ephemeral.ChangedItemFilter = ChangedItemFlagExtensions.AllFlags;
|
||||
_config.Ephemeral.Save();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Penumbra.Util;
|
|||
public static class IdentifierExtensions
|
||||
{
|
||||
public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container,
|
||||
IDictionary<string, object?> changedItems)
|
||||
IDictionary<string, IIdentifiedObjectData?> changedItems)
|
||||
{
|
||||
foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys))
|
||||
identifier.Identify(changedItems, gamePath.ToString());
|
||||
|
|
@ -19,25 +19,25 @@ public static class IdentifierExtensions
|
|||
manip.AddChangedItems(identifier, changedItems);
|
||||
}
|
||||
|
||||
public static void RemoveMachinistOffhands(this SortedList<string, object?> changedItems)
|
||||
public static void RemoveMachinistOffhands(this SortedList<string, IIdentifiedObjectData?> changedItems)
|
||||
{
|
||||
for (var i = 0; i < changedItems.Count; i++)
|
||||
{
|
||||
{
|
||||
var value = changedItems.Values[i];
|
||||
if (value is EquipItem { Type: FullEquipType.GunOff })
|
||||
if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff })
|
||||
changedItems.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, object?)> changedItems)
|
||||
public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> changedItems)
|
||||
{
|
||||
for (var i = 0; i < changedItems.Count; i++)
|
||||
{
|
||||
{
|
||||
var value = changedItems.Values[i].Item2;
|
||||
if (value is EquipItem { Type: FullEquipType.GunOff })
|
||||
if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff })
|
||||
changedItems.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue