Use EquipItem in item management and add filters to changed item types.

This commit is contained in:
Ottermandias 2023-06-09 16:10:02 +02:00
parent 5fcb07487e
commit d9c5c053cf
19 changed files with 641 additions and 375 deletions

View file

@ -9,15 +9,15 @@ using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data; namespace Penumbra.GameData.Data;
internal sealed class EquipmentIdentificationList : KeyList<Item> internal sealed class EquipmentIdentificationList : KeyList<EquipItem>
{ {
private const string Tag = "EquipmentIdentification"; private const string Tag = "EquipmentIdentification";
public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData) public EquipmentIdentificationList(DalamudPluginInterface pi, ClientLanguage language, DataManager gameData)
: base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(gameData, language)) : base(pi, Tag, language, ObjectIdentification.IdentificationVersion, CreateEquipmentList(gameData, language))
{ } { }
public IEnumerable<Item> Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0) public IEnumerable<EquipItem> Between(SetId modelId, EquipSlot slot = EquipSlot.Unknown, byte variant = 0)
{ {
if (slot == EquipSlot.Unknown) if (slot == EquipSlot.Unknown)
return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF)); return Between(ToKey(modelId, 0, 0), ToKey(modelId, (EquipSlot)0xFF, 0xFF));
@ -33,15 +33,10 @@ internal sealed class EquipmentIdentificationList : KeyList<Item>
public static ulong ToKey(SetId modelId, EquipSlot slot, byte variant) public static ulong ToKey(SetId modelId, EquipSlot slot, byte variant)
=> ((ulong)modelId << 32) | ((ulong)slot << 16) | variant; => ((ulong)modelId << 32) | ((ulong)slot << 16) | variant;
public static ulong ToKey(Item i) public static ulong ToKey(EquipItem i)
{ => ToKey(i.ModelId, i.Slot, i.Variant);
var model = (SetId)((Lumina.Data.Parsing.Quad)i.ModelMain).A;
var slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot();
var variant = (byte)((Lumina.Data.Parsing.Quad)i.ModelMain).B;
return ToKey(model, slot, variant);
}
protected override IEnumerable<ulong> ToKeys(Item i) protected override IEnumerable<ulong> ToKeys(EquipItem i)
{ {
yield return ToKey(i); yield return ToKey(i);
} }
@ -49,12 +44,12 @@ internal sealed class EquipmentIdentificationList : KeyList<Item>
protected override bool ValidKey(ulong key) protected override bool ValidKey(ulong key)
=> key != 0; => key != 0;
protected override int ValueKeySelector(Item data) protected override int ValueKeySelector(EquipItem data)
=> (int)data.RowId; => (int)data.Id;
private static IEnumerable<Item> CreateEquipmentList(DataManager gameData, ClientLanguage language) private static IEnumerable<EquipItem> CreateEquipmentList(DataManager gameData, ClientLanguage language)
{ {
var items = gameData.GetExcelSheet<Item>(language)!; var items = gameData.GetExcelSheet<Item>(language)!;
return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()); return items.Where(i => ((EquipSlot)i.EquipSlotCategory.Row).IsEquipmentPiece()).Select(EquipItem.FromArmor);
} }
} }

View file

@ -5,32 +5,41 @@ using System.Linq;
using Dalamud; using Dalamud;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data; namespace Penumbra.GameData.Data;
public sealed class ItemData : DataSharer, IReadOnlyDictionary<FullEquipType, IReadOnlyList<Item>> public sealed class ItemData : DataSharer, IReadOnlyDictionary<FullEquipType, IReadOnlyList<EquipItem>>
{ {
private readonly IReadOnlyList<IReadOnlyList<Item>> _items; private readonly IReadOnlyList<IReadOnlyList<EquipItem>> _items;
private static IReadOnlyList<IReadOnlyList<Item>> CreateItems(DataManager dataManager, ClientLanguage language) private static IReadOnlyList<IReadOnlyList<EquipItem>> CreateItems(DataManager dataManager, ClientLanguage language)
{ {
var tmp = Enum.GetValues<FullEquipType>().Select(t => new List<Item>(1024)).ToArray(); var tmp = Enum.GetValues<FullEquipType>().Select(_ => new List<EquipItem>(1024)).ToArray();
var itemSheet = dataManager.GetExcelSheet<Item>(language)!; var itemSheet = dataManager.GetExcelSheet<Item>(language)!;
foreach (var item in itemSheet) foreach (var item in itemSheet.Where(i => i.Name.RawData.Length > 1))
{ {
var type = item.ToEquipType(); var type = item.ToEquipType();
if (type != FullEquipType.Unknown && item.Name.RawData.Length > 1) if (type.IsWeapon())
tmp[(int)type].Add(item); {
if (item.ModelMain != 0)
tmp[(int)type].Add(EquipItem.FromMainhand(item));
if (item.ModelSub != 0)
tmp[(int)type].Add(EquipItem.FromOffhand(item));
}
else if (type != FullEquipType.Unknown)
{
tmp[(int)type].Add(EquipItem.FromArmor(item));
}
} }
var ret = new IReadOnlyList<Item>[tmp.Length]; var ret = new IReadOnlyList<EquipItem>[tmp.Length];
ret[0] = Array.Empty<Item>(); ret[0] = Array.Empty<EquipItem>();
for (var i = 1; i < tmp.Length; ++i) for (var i = 1; i < tmp.Length; ++i)
ret[i] = tmp[i].OrderBy(item => item.Name.ToDalamudString().TextValue).ToArray(); ret[i] = tmp[i].OrderBy(item => item.Name).ToArray();
return ret; return ret;
} }
@ -44,10 +53,10 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary<FullEquipType, IR
protected override void DisposeInternal() protected override void DisposeInternal()
=> DisposeTag("ItemList"); => DisposeTag("ItemList");
public IEnumerator<KeyValuePair<FullEquipType, IReadOnlyList<Item>>> GetEnumerator() public IEnumerator<KeyValuePair<FullEquipType, IReadOnlyList<EquipItem>>> GetEnumerator()
{ {
for (var i = 1; i < _items.Count; ++i) for (var i = 1; i < _items.Count; ++i)
yield return new KeyValuePair<FullEquipType, IReadOnlyList<Item>>((FullEquipType)i, _items[i]); yield return new KeyValuePair<FullEquipType, IReadOnlyList<EquipItem>>((FullEquipType)i, _items[i]);
} }
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
@ -59,7 +68,7 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary<FullEquipType, IR
public bool ContainsKey(FullEquipType key) public bool ContainsKey(FullEquipType key)
=> (int)key < _items.Count && key != FullEquipType.Unknown; => (int)key < _items.Count && key != FullEquipType.Unknown;
public bool TryGetValue(FullEquipType key, out IReadOnlyList<Item> value) public bool TryGetValue(FullEquipType key, out IReadOnlyList<EquipItem> value)
{ {
if (ContainsKey(key)) if (ContainsKey(key))
{ {
@ -71,12 +80,12 @@ public sealed class ItemData : DataSharer, IReadOnlyDictionary<FullEquipType, IR
return false; return false;
} }
public IReadOnlyList<Item> this[FullEquipType key] public IReadOnlyList<EquipItem> this[FullEquipType key]
=> TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException(); => TryGetValue(key, out var ret) ? ret : throw new IndexOutOfRangeException();
public IEnumerable<FullEquipType> Keys public IEnumerable<FullEquipType> Keys
=> Enum.GetValues<FullEquipType>().Skip(1); => Enum.GetValues<FullEquipType>().Skip(1);
public IEnumerable<IReadOnlyList<Item>> Values public IEnumerable<IReadOnlyList<EquipItem>> Values
=> _items.Skip(1); => _items.Skip(1);
} }

View file

@ -65,7 +65,7 @@ internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier
return ret; return ret;
} }
public IEnumerable<Item> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) public IEnumerable<EquipItem> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot)
=> slot switch => slot switch
{ {
EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant), EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant),

View file

@ -9,7 +9,7 @@ using Penumbra.GameData.Structs;
namespace Penumbra.GameData.Data; namespace Penumbra.GameData.Data;
internal sealed class WeaponIdentificationList : KeyList<Item> internal sealed class WeaponIdentificationList : KeyList<EquipItem>
{ {
private const string Tag = "WeaponIdentification"; private const string Tag = "WeaponIdentification";
private const int Version = 1; private const int Version = 1;
@ -18,10 +18,10 @@ internal sealed class WeaponIdentificationList : KeyList<Item>
: base(pi, Tag, language, Version, CreateWeaponList(gameData, language)) : base(pi, Tag, language, Version, CreateWeaponList(gameData, language))
{ } { }
public IEnumerable<Item> Between(SetId modelId) public IEnumerable<EquipItem> Between(SetId modelId)
=> Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); => Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF));
public IEnumerable<Item> Between(SetId modelId, WeaponType type, byte variant = 0) public IEnumerable<EquipItem> Between(SetId modelId, WeaponType type, byte variant = 0)
{ {
if (type == 0) if (type == 0)
return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF)); return Between(ToKey(modelId, 0, 0), ToKey(modelId, 0xFFFF, 0xFF));
@ -37,38 +37,31 @@ internal sealed class WeaponIdentificationList : KeyList<Item>
public static ulong ToKey(SetId modelId, WeaponType type, byte variant) public static ulong ToKey(SetId modelId, WeaponType type, byte variant)
=> ((ulong)modelId << 32) | ((ulong)type << 16) | variant; => ((ulong)modelId << 32) | ((ulong)type << 16) | variant;
public static ulong ToKey(Item i, bool offhand) public static ulong ToKey(EquipItem i)
{ => ToKey(i.ModelId, i.WeaponType, i.Variant);
var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain;
return ToKey(quad.A, quad.B, (byte)quad.C);
}
protected override IEnumerable<ulong> ToKeys(Item i) protected override IEnumerable<ulong> ToKeys(EquipItem data)
{ {
var key1 = 0ul; yield return ToKey(data);
if (i.ModelMain != 0)
{
key1 = ToKey(i, false);
yield return key1;
}
if (i.ModelSub != 0)
{
var key2 = ToKey(i, true);
if (key1 != key2)
yield return key2;
}
} }
protected override bool ValidKey(ulong key) protected override bool ValidKey(ulong key)
=> key != 0; => key != 0;
protected override int ValueKeySelector(Item data) protected override int ValueKeySelector(EquipItem data)
=> (int)data.RowId; => (int)data.Id;
private static IEnumerable<Item> CreateWeaponList(DataManager gameData, ClientLanguage language) private static IEnumerable<EquipItem> CreateWeaponList(DataManager gameData, ClientLanguage language)
=> gameData.GetExcelSheet<Item>(language)!.SelectMany(ToEquipItems);
private static IEnumerable<EquipItem> ToEquipItems(Item item)
{ {
var items = gameData.GetExcelSheet<Item>(language)!; if ((EquipSlot)item.EquipSlotCategory.Row is not (EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand))
return items.Where(i => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand); yield break;
if (item.ModelMain != 0)
yield return EquipItem.FromMainhand(item);
if (item.ModelSub != 0)
yield return EquipItem.FromOffhand(item);
} }
} }

View file

@ -1,33 +1,19 @@
using System;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Structs;
using Action = Lumina.Excel.GeneratedSheets.Action; using Action = Lumina.Excel.GeneratedSheets.Action;
namespace Penumbra.GameData.Enums; namespace Penumbra.GameData.Enums;
public static class ChangedItemExtensions public static class ChangedItemExtensions
{ {
public static (ChangedItemType, uint) ChangedItemToTypeAndId( object? item ) public static (ChangedItemType, uint) ChangedItemToTypeAndId(object? item)
{ {
return item switch return item switch
{ {
null => ( ChangedItemType.None, 0 ), null => (ChangedItemType.None, 0),
Item i => ( ChangedItemType.Item, i.RowId ), EquipItem i => (ChangedItemType.Item, i.Id),
Action a => ( ChangedItemType.Action, a.RowId ), Action a => (ChangedItemType.Action, a.RowId),
_ => ( ChangedItemType.Customization, 0 ), _ => (ChangedItemType.Customization, 0),
}; };
} }
}
public static object? GetObject( this ChangedItemType type, DataManager manager, uint id )
{
return type switch
{
ChangedItemType.None => null,
ChangedItemType.Item => manager.GetExcelSheet< Item >()?.GetRow( id ),
ChangedItemType.Action => manager.GetExcelSheet< Action >()?.GetRow( id ),
ChangedItemType.Customization => null,
_ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ),
};
}
}

View file

@ -20,26 +20,34 @@ public enum FullEquipType : byte
Wrists, Wrists,
Finger, Finger,
Fists, // PGL, MNK Fists, // PGL, MNK
Sword, // GLA, PLD Main FistsOff,
Axe, // MRD, WAR Sword, // GLA, PLD Main
Bow, // ARC, BRD Axe, // MRD, WAR
Lance, // LNC, DRG, Bow, // ARC, BRD
Staff, // THM, BLM, CNJ, WHM BowOff,
Wand, // THM, BLM, CNJ, WHM Main Lance, // LNC, DRG,
Book, // ACN, SMN, SCH Staff, // THM, BLM, CNJ, WHM
Daggers, // ROG, NIN Wand, // THM, BLM, CNJ, WHM Main
Book, // ACN, SMN, SCH
Daggers, // ROG, NIN
DaggersOff,
Broadsword, // DRK, Broadsword, // DRK,
Gun, // MCH, Gun, // MCH,
Orrery, // AST, GunOff,
Katana, // SAM Orrery, // AST,
Rapier, // RDM OrreryOff,
Cane, // BLU Katana, // SAM
Gunblade, // GNB, KatanaOff,
Glaives, // DNC, Rapier, // RDM
Scythe, // RPR, RapierOff,
Nouliths, // SGE Cane, // BLU
Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off Gunblade, // GNB,
Glaives, // DNC,
GlaivesOff,
Scythe, // RPR,
Nouliths, // SGE
Shield, // GLA, PLD, THM, BLM, CNJ, WHM Off
Saw, // CRP Saw, // CRP
CrossPeinHammer, // BSM CrossPeinHammer, // BSM
@ -68,7 +76,7 @@ public enum FullEquipType : byte
public static class FullEquipTypeExtensions public static class FullEquipTypeExtensions
{ {
public static FullEquipType ToEquipType(this Item item) internal static FullEquipType ToEquipType(this Item item)
{ {
var slot = (EquipSlot)item.EquipSlotCategory.Row; var slot = (EquipSlot)item.EquipSlotCategory.Row;
var weapon = (WeaponCategory)item.ItemUICategory.Row; var weapon = (WeaponCategory)item.ItemUICategory.Row;
@ -152,22 +160,30 @@ public static class FullEquipTypeExtensions
FullEquipType.Wrists => EquipSlot.Wrists.ToName(), FullEquipType.Wrists => EquipSlot.Wrists.ToName(),
FullEquipType.Finger => "Ring", FullEquipType.Finger => "Ring",
FullEquipType.Fists => "Fist Weapon", FullEquipType.Fists => "Fist Weapon",
FullEquipType.FistsOff => "Fist Weapon (Offhand)",
FullEquipType.Sword => "Sword", FullEquipType.Sword => "Sword",
FullEquipType.Axe => "Axe", FullEquipType.Axe => "Axe",
FullEquipType.Bow => "Bow", FullEquipType.Bow => "Bow",
FullEquipType.BowOff => "Quiver",
FullEquipType.Lance => "Lance", FullEquipType.Lance => "Lance",
FullEquipType.Staff => "Staff", FullEquipType.Staff => "Staff",
FullEquipType.Wand => "Mace", FullEquipType.Wand => "Mace",
FullEquipType.Book => "Book", FullEquipType.Book => "Book",
FullEquipType.Daggers => "Dagger", FullEquipType.Daggers => "Dagger",
FullEquipType.DaggersOff => "Dagger (Offhand)",
FullEquipType.Broadsword => "Broadsword", FullEquipType.Broadsword => "Broadsword",
FullEquipType.Gun => "Gun", FullEquipType.Gun => "Gun",
FullEquipType.GunOff => "Aetherotransformer",
FullEquipType.Orrery => "Orrery", FullEquipType.Orrery => "Orrery",
FullEquipType.OrreryOff => "Card Holder",
FullEquipType.Katana => "Katana", FullEquipType.Katana => "Katana",
FullEquipType.KatanaOff => "Sheathe",
FullEquipType.Rapier => "Rapier", FullEquipType.Rapier => "Rapier",
FullEquipType.RapierOff => "Focus",
FullEquipType.Cane => "Cane", FullEquipType.Cane => "Cane",
FullEquipType.Gunblade => "Gunblade", FullEquipType.Gunblade => "Gunblade",
FullEquipType.Glaives => "Glaive", FullEquipType.Glaives => "Glaive",
FullEquipType.GlaivesOff => "Glaive (Offhand)",
FullEquipType.Scythe => "Scythe", FullEquipType.Scythe => "Scythe",
FullEquipType.Nouliths => "Nouliths", FullEquipType.Nouliths => "Nouliths",
FullEquipType.Shield => "Shield", FullEquipType.Shield => "Shield",
@ -209,22 +225,30 @@ public static class FullEquipTypeExtensions
FullEquipType.Wrists => EquipSlot.Wrists, FullEquipType.Wrists => EquipSlot.Wrists,
FullEquipType.Finger => EquipSlot.RFinger, FullEquipType.Finger => EquipSlot.RFinger,
FullEquipType.Fists => EquipSlot.MainHand, FullEquipType.Fists => EquipSlot.MainHand,
FullEquipType.FistsOff => EquipSlot.OffHand,
FullEquipType.Sword => EquipSlot.MainHand, FullEquipType.Sword => EquipSlot.MainHand,
FullEquipType.Axe => EquipSlot.MainHand, FullEquipType.Axe => EquipSlot.MainHand,
FullEquipType.Bow => EquipSlot.MainHand, FullEquipType.Bow => EquipSlot.MainHand,
FullEquipType.BowOff => EquipSlot.OffHand,
FullEquipType.Lance => EquipSlot.MainHand, FullEquipType.Lance => EquipSlot.MainHand,
FullEquipType.Staff => EquipSlot.MainHand, FullEquipType.Staff => EquipSlot.MainHand,
FullEquipType.Wand => EquipSlot.MainHand, FullEquipType.Wand => EquipSlot.MainHand,
FullEquipType.Book => EquipSlot.MainHand, FullEquipType.Book => EquipSlot.MainHand,
FullEquipType.Daggers => EquipSlot.MainHand, FullEquipType.Daggers => EquipSlot.MainHand,
FullEquipType.DaggersOff => EquipSlot.OffHand,
FullEquipType.Broadsword => EquipSlot.MainHand, FullEquipType.Broadsword => EquipSlot.MainHand,
FullEquipType.Gun => EquipSlot.MainHand, FullEquipType.Gun => EquipSlot.MainHand,
FullEquipType.GunOff => EquipSlot.OffHand,
FullEquipType.Orrery => EquipSlot.MainHand, FullEquipType.Orrery => EquipSlot.MainHand,
FullEquipType.OrreryOff => EquipSlot.OffHand,
FullEquipType.Katana => EquipSlot.MainHand, FullEquipType.Katana => EquipSlot.MainHand,
FullEquipType.KatanaOff => EquipSlot.OffHand,
FullEquipType.Rapier => EquipSlot.MainHand, FullEquipType.Rapier => EquipSlot.MainHand,
FullEquipType.RapierOff => EquipSlot.OffHand,
FullEquipType.Cane => EquipSlot.MainHand, FullEquipType.Cane => EquipSlot.MainHand,
FullEquipType.Gunblade => EquipSlot.MainHand, FullEquipType.Gunblade => EquipSlot.MainHand,
FullEquipType.Glaives => EquipSlot.MainHand, FullEquipType.Glaives => EquipSlot.MainHand,
FullEquipType.GlaivesOff => EquipSlot.OffHand,
FullEquipType.Scythe => EquipSlot.MainHand, FullEquipType.Scythe => EquipSlot.MainHand,
FullEquipType.Nouliths => EquipSlot.MainHand, FullEquipType.Nouliths => EquipSlot.MainHand,
FullEquipType.Shield => EquipSlot.OffHand, FullEquipType.Shield => EquipSlot.OffHand,
@ -253,7 +277,7 @@ public static class FullEquipTypeExtensions
_ => EquipSlot.Unknown, _ => EquipSlot.Unknown,
}; };
public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown) public static FullEquipType ToEquipType(this EquipSlot slot, WeaponCategory category = WeaponCategory.Unknown, bool mainhand = true)
=> slot switch => slot switch
{ {
EquipSlot.Head => FullEquipType.Head, EquipSlot.Head => FullEquipType.Head,
@ -273,77 +297,101 @@ public static class FullEquipTypeExtensions
EquipSlot.BodyHands => FullEquipType.Body, EquipSlot.BodyHands => FullEquipType.Body,
EquipSlot.BodyLegsFeet => FullEquipType.Body, EquipSlot.BodyLegsFeet => FullEquipType.Body,
EquipSlot.ChestHands => FullEquipType.Body, EquipSlot.ChestHands => FullEquipType.Body,
EquipSlot.MainHand => category.ToEquipType(), EquipSlot.MainHand => category.ToEquipType(mainhand),
EquipSlot.OffHand => category.ToEquipType(), EquipSlot.OffHand => category.ToEquipType(mainhand),
EquipSlot.BothHand => category.ToEquipType(), EquipSlot.BothHand => category.ToEquipType(mainhand),
_ => FullEquipType.Unknown, _ => FullEquipType.Unknown,
}; };
public static FullEquipType ToEquipType(this WeaponCategory category) public static FullEquipType ToEquipType(this WeaponCategory category, bool mainhand = true)
=> category switch => category switch
{ {
WeaponCategory.Pugilist => FullEquipType.Fists, WeaponCategory.Pugilist when mainhand => FullEquipType.Fists,
WeaponCategory.Gladiator => FullEquipType.Sword, WeaponCategory.Pugilist => FullEquipType.FistsOff,
WeaponCategory.Marauder => FullEquipType.Axe, WeaponCategory.Gladiator => FullEquipType.Sword,
WeaponCategory.Archer => FullEquipType.Bow, WeaponCategory.Marauder => FullEquipType.Axe,
WeaponCategory.Lancer => FullEquipType.Lance, WeaponCategory.Archer when mainhand => FullEquipType.Bow,
WeaponCategory.Thaumaturge1 => FullEquipType.Wand, WeaponCategory.Archer => FullEquipType.BowOff,
WeaponCategory.Thaumaturge2 => FullEquipType.Staff, WeaponCategory.Lancer => FullEquipType.Lance,
WeaponCategory.Conjurer1 => FullEquipType.Wand, WeaponCategory.Thaumaturge1 => FullEquipType.Wand,
WeaponCategory.Conjurer2 => FullEquipType.Staff, WeaponCategory.Thaumaturge2 => FullEquipType.Staff,
WeaponCategory.Arcanist => FullEquipType.Book, WeaponCategory.Conjurer1 => FullEquipType.Wand,
WeaponCategory.Shield => FullEquipType.Shield, WeaponCategory.Conjurer2 => FullEquipType.Staff,
WeaponCategory.CarpenterMain => FullEquipType.Saw, WeaponCategory.Arcanist => FullEquipType.Book,
WeaponCategory.CarpenterOff => FullEquipType.ClawHammer, WeaponCategory.Shield => FullEquipType.Shield,
WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer, WeaponCategory.CarpenterMain => FullEquipType.Saw,
WeaponCategory.BlacksmithOff => FullEquipType.File, WeaponCategory.CarpenterOff => FullEquipType.ClawHammer,
WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer, WeaponCategory.BlacksmithMain => FullEquipType.CrossPeinHammer,
WeaponCategory.ArmorerOff => FullEquipType.Pliers, WeaponCategory.BlacksmithOff => FullEquipType.File,
WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer, WeaponCategory.ArmorerMain => FullEquipType.RaisingHammer,
WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel, WeaponCategory.ArmorerOff => FullEquipType.Pliers,
WeaponCategory.LeatherworkerMain => FullEquipType.Knife, WeaponCategory.GoldsmithMain => FullEquipType.LapidaryHammer,
WeaponCategory.LeatherworkerOff => FullEquipType.Awl, WeaponCategory.GoldsmithOff => FullEquipType.GrindingWheel,
WeaponCategory.WeaverMain => FullEquipType.Needle, WeaponCategory.LeatherworkerMain => FullEquipType.Knife,
WeaponCategory.WeaverOff => FullEquipType.SpinningWheel, WeaponCategory.LeatherworkerOff => FullEquipType.Awl,
WeaponCategory.AlchemistMain => FullEquipType.Alembic, WeaponCategory.WeaverMain => FullEquipType.Needle,
WeaponCategory.AlchemistOff => FullEquipType.Mortar, WeaponCategory.WeaverOff => FullEquipType.SpinningWheel,
WeaponCategory.CulinarianMain => FullEquipType.Frypan, WeaponCategory.AlchemistMain => FullEquipType.Alembic,
WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife, WeaponCategory.AlchemistOff => FullEquipType.Mortar,
WeaponCategory.MinerMain => FullEquipType.Pickaxe, WeaponCategory.CulinarianMain => FullEquipType.Frypan,
WeaponCategory.MinerOff => FullEquipType.Sledgehammer, WeaponCategory.CulinarianOff => FullEquipType.CulinaryKnife,
WeaponCategory.BotanistMain => FullEquipType.Hatchet, WeaponCategory.MinerMain => FullEquipType.Pickaxe,
WeaponCategory.BotanistOff => FullEquipType.GardenScythe, WeaponCategory.MinerOff => FullEquipType.Sledgehammer,
WeaponCategory.FisherMain => FullEquipType.FishingRod, WeaponCategory.BotanistMain => FullEquipType.Hatchet,
WeaponCategory.Rogue => FullEquipType.Gig, WeaponCategory.BotanistOff => FullEquipType.GardenScythe,
WeaponCategory.DarkKnight => FullEquipType.Broadsword, WeaponCategory.FisherMain => FullEquipType.FishingRod,
WeaponCategory.Machinist => FullEquipType.Gun, WeaponCategory.FisherOff => FullEquipType.Gig,
WeaponCategory.Astrologian => FullEquipType.Orrery, WeaponCategory.Rogue when mainhand => FullEquipType.DaggersOff,
WeaponCategory.Samurai => FullEquipType.Katana, WeaponCategory.Rogue => FullEquipType.Daggers,
WeaponCategory.RedMage => FullEquipType.Rapier, WeaponCategory.DarkKnight => FullEquipType.Broadsword,
WeaponCategory.Scholar => FullEquipType.Book, WeaponCategory.Machinist when mainhand => FullEquipType.Gun,
WeaponCategory.FisherOff => FullEquipType.Gig, WeaponCategory.Machinist => FullEquipType.GunOff,
WeaponCategory.BlueMage => FullEquipType.Cane, WeaponCategory.Astrologian when mainhand => FullEquipType.Orrery,
WeaponCategory.Gunbreaker => FullEquipType.Gunblade, WeaponCategory.Astrologian => FullEquipType.OrreryOff,
WeaponCategory.Dancer => FullEquipType.Glaives, WeaponCategory.Samurai when mainhand => FullEquipType.Katana,
WeaponCategory.Reaper => FullEquipType.Scythe, WeaponCategory.Samurai => FullEquipType.KatanaOff,
WeaponCategory.Sage => FullEquipType.Nouliths, WeaponCategory.RedMage when mainhand => FullEquipType.Rapier,
_ => FullEquipType.Unknown, WeaponCategory.RedMage => FullEquipType.RapierOff,
WeaponCategory.Scholar => FullEquipType.Book,
WeaponCategory.BlueMage => FullEquipType.Cane,
WeaponCategory.Gunbreaker => FullEquipType.Gunblade,
WeaponCategory.Dancer when mainhand => FullEquipType.Glaives,
WeaponCategory.Dancer => FullEquipType.GlaivesOff,
WeaponCategory.Reaper => FullEquipType.Scythe,
WeaponCategory.Sage => FullEquipType.Nouliths,
_ => FullEquipType.Unknown,
}; };
public static FullEquipType Offhand(this FullEquipType type) public static FullEquipType Offhand(this FullEquipType type)
=> type switch => type switch
{ {
FullEquipType.Fists => FullEquipType.Fists, FullEquipType.Fists => FullEquipType.FistsOff,
FullEquipType.Sword => FullEquipType.Shield, FullEquipType.Sword => FullEquipType.Shield,
FullEquipType.Wand => FullEquipType.Shield, FullEquipType.Wand => FullEquipType.Shield,
FullEquipType.Daggers => FullEquipType.Daggers, FullEquipType.Daggers => FullEquipType.DaggersOff,
FullEquipType.Gun => FullEquipType.Gun, FullEquipType.Gun => FullEquipType.GunOff,
FullEquipType.Orrery => FullEquipType.Orrery, FullEquipType.Orrery => FullEquipType.OrreryOff,
FullEquipType.Rapier => FullEquipType.Rapier, FullEquipType.Rapier => FullEquipType.RapierOff,
FullEquipType.Glaives => FullEquipType.Glaives, FullEquipType.Glaives => FullEquipType.GlaivesOff,
FullEquipType.Bow => FullEquipType.BowOff,
FullEquipType.Katana => FullEquipType.KatanaOff,
_ => FullEquipType.Unknown, _ => FullEquipType.Unknown,
}; };
internal static string OffhandTypeSuffix(this FullEquipType type)
=> type switch
{
FullEquipType.FistsOff => " (Offhand)",
FullEquipType.DaggersOff => " (Offhand)",
FullEquipType.GunOff => " (Aetherotransformer)",
FullEquipType.OrreryOff => " (Card Holder)",
FullEquipType.RapierOff => " (Focus)",
FullEquipType.GlaivesOff => " (Offhand)",
FullEquipType.BowOff => " (Quiver)",
FullEquipType.KatanaOff => " (Sheathe)",
_ => string.Empty,
};
public static readonly IReadOnlyList<FullEquipType> WeaponTypes public static readonly IReadOnlyList<FullEquipType> WeaponTypes
= Enum.GetValues<FullEquipType>().Where(v => v.IsWeapon()).ToArray(); = Enum.GetValues<FullEquipType>().Where(v => v.IsWeapon()).ToArray();

View file

@ -58,10 +58,10 @@ public interface IObjectIdentifier : IDisposable
/// <param name="weaponType">The secondary model ID for weapons, WeaponType.Zero for equipment and accessories.</param> /// <param name="weaponType">The secondary model ID for weapons, WeaponType.Zero for equipment and accessories.</param>
/// <param name="variant">The variant ID of the model.</param> /// <param name="variant">The variant ID of the model.</param>
/// <param name="slot">The equipment slot the piece of equipment uses.</param> /// <param name="slot">The equipment slot the piece of equipment uses.</param>
public IEnumerable<Item> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); public IEnumerable<EquipItem> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot);
/// <inheritdoc cref="Identify(SetId, WeaponType, ushort, EquipSlot)"/> /// <inheritdoc cref="Identify(SetId, WeaponType, ushort, EquipSlot)"/>
public IEnumerable<Item> Identify(SetId setId, ushort variant, EquipSlot slot) public IEnumerable<EquipItem> Identify(SetId setId, ushort variant, EquipSlot slot)
=> Identify(setId, 0, variant, slot); => Identify(setId, 0, variant, slot);
} }

View file

@ -0,0 +1,88 @@
using System.Runtime.InteropServices;
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
namespace Penumbra.GameData.Structs;
[StructLayout(LayoutKind.Sequential)]
public readonly struct EquipItem
{
public readonly string Name;
public readonly uint Id;
public readonly ushort IconId;
public readonly SetId ModelId;
public readonly WeaponType WeaponType;
public readonly byte Variant;
public readonly FullEquipType Type;
public readonly EquipSlot Slot;
public bool Valid
=> Type != FullEquipType.Unknown;
public CharacterArmor Armor()
=> new(ModelId, Variant, 0);
public CharacterArmor Armor(StainId stain)
=> new(ModelId, Variant, stain);
public CharacterWeapon Weapon()
=> new(ModelId, WeaponType, Variant, 0);
public CharacterWeapon Weapon(StainId stain)
=> new(ModelId, WeaponType, Variant, stain);
public EquipItem()
=> Name = string.Empty;
public EquipItem(string name, uint id, ushort iconId, SetId modelId, WeaponType weaponType, byte variant, FullEquipType type,
EquipSlot slot)
{
Name = name;
Id = id;
IconId = iconId;
ModelId = modelId;
WeaponType = weaponType;
Variant = variant;
Type = type;
Slot = slot;
}
public static EquipItem FromArmor(Item item)
{
var type = item.ToEquipType();
var slot = type.ToSlot();
var name = string.Intern(item.Name.ToDalamudString().TextValue);
var id = item.RowId;
var icon = item.Icon;
var model = (SetId)item.ModelMain;
var weapon = (WeaponType)0;
var variant = (byte)(item.ModelMain >> 16);
return new EquipItem(name, id, icon, model, weapon, variant, type, slot);
}
public static EquipItem FromMainhand(Item item)
{
var type = item.ToEquipType();
var name = string.Intern(item.Name.ToDalamudString().TextValue);
var id = item.RowId;
var icon = item.Icon;
var model = (SetId)item.ModelMain;
var weapon = (WeaponType)(item.ModelMain >> 16);
var variant = (byte)(item.ModelMain >> 32);
return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.MainHand);
}
public static EquipItem FromOffhand(Item item)
{
var type = item.ToEquipType().Offhand();
var name = string.Intern(item.Name.ToDalamudString().TextValue + type.OffhandTypeSuffix());
var id = item.RowId;
var icon = item.Icon;
var model = (SetId)item.ModelSub;
var weapon = (WeaponType)(item.ModelSub >> 16);
var variant = (byte)(item.ModelSub >> 32);
return new EquipItem(name, id, icon, model, weapon, variant, type, EquipSlot.OffHand);
}
}

View file

@ -17,6 +17,9 @@ public sealed class ChangedItemClick : EventWrapper<Action<MouseButton, object?>
{ {
/// <seealso cref="Api.PenumbraApi.ChangedItemClicked"/> /// <seealso cref="Api.PenumbraApi.ChangedItemClicked"/>
Default = 0, Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>
Link = 1,
} }
public ChangedItemClick() public ChangedItemClick()

View file

@ -15,6 +15,9 @@ public sealed class ChangedItemHover : EventWrapper<Action<object?>, ChangedItem
{ {
/// <seealso cref="Api.PenumbraApi.ChangedItemTooltip"/> /// <seealso cref="Api.PenumbraApi.ChangedItemTooltip"/>
Default = 0, Default = 0,
/// <seealso cref="Penumbra.SetupApi"/>
Link = 1,
} }
public ChangedItemHover() public ChangedItemHover()

View file

@ -91,6 +91,8 @@ public class Configuration : IPluginConfiguration, ISavable
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings; public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags;
public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool PrintSuccessfulCommandsToChat { get; set; } = true;
public bool FixMainWindow { get; set; } = false; public bool FixMainWindow { get; set; } = false;
public bool AutoDeduplicateOnImport { get; set; } = true; public bool AutoDeduplicateOnImport { get; set; } = true;

View file

@ -53,7 +53,7 @@ public class ResourceTree
if (imcNode != null) if (imcNode != null)
Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode); Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode);
var mdl = (RenderModel*)model->ModelArray[i]; var mdl = (RenderModel*)model->Models[i];
var mdlNode = context.CreateNodeFromRenderModel(mdl); var mdlNode = context.CreateNodeFromRenderModel(mdl);
if (mdlNode != null) if (mdlNode != null)
Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode);

View file

@ -2,8 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Lumina.Data.Parsing;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -46,9 +44,9 @@ public static class EquipmentSwap
: Array.Empty<EquipSlot>(); : Array.Empty<EquipSlot>();
} }
public static Item[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps, public static EquipItem[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps,
Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips,
EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo) EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo)
{ {
LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom);
LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo);
@ -104,9 +102,9 @@ public static class EquipmentSwap
return affectedItems; return affectedItems;
} }
public static Item[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps, public static EquipItem[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps,
Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, Item itemFrom, Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, EquipItem itemFrom,
Item itemTo, bool rFinger = true, bool lFinger = true) EquipItem itemTo, bool rFinger = true, bool lFinger = true)
{ {
// Check actual ids, variants and slots. We only support using the same slot. // Check actual ids, variants and slots. We only support using the same slot.
LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom);
@ -122,7 +120,7 @@ public static class EquipmentSwap
if (gmp != null) if (gmp != null)
swaps.Add(gmp); swaps.Add(gmp);
var affectedItems = Array.Empty<Item>(); var affectedItems = Array.Empty<EquipItem>();
foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger))
{ {
(var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom);
@ -242,22 +240,22 @@ public static class EquipmentSwap
return mdl; return mdl;
} }
private static void LookupItem(Item i, out EquipSlot slot, out SetId modelId, out byte variant) private static void LookupItem(EquipItem i, out EquipSlot slot, out SetId modelId, out byte variant)
{ {
slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot(); if (!i.Slot.IsEquipmentPiece())
if (!slot.IsEquipmentPiece())
throw new ItemSwap.InvalidItemTypeException(); throw new ItemSwap.InvalidItemTypeException();
modelId = ((Quad)i.ModelMain).A; slot = i.Slot;
variant = (byte)((Quad)i.ModelMain).B; modelId = i.ModelId;
variant = i.Variant;
} }
private static (ImcFile, byte[], Item[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom, private static (ImcFile, byte[], EquipItem[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom,
SetId idFrom, SetId idTo, byte variantFrom) SetId idFrom, SetId idTo, byte variantFrom)
{ {
var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default); var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default);
var imc = new ImcFile(manager, entry); var imc = new ImcFile(manager, entry);
Item[] items; EquipItem[] items;
byte[] variants; byte[] variants;
if (idFrom.Value == idTo.Value) if (idFrom.Value == idTo.Value)
{ {
@ -271,7 +269,7 @@ public static class EquipmentSwap
{ {
items = identifier.Identify(slotFrom.IsEquipment() items = identifier.Identify(slotFrom.IsEquipment()
? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)
: GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType<Item>().ToArray(); : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType<EquipItem>().ToArray();
variants = Enumerable.Range(0, imc.Count + 1).Select(i => (byte)i).ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (byte)i).ToArray();
} }

View file

@ -132,7 +132,7 @@ public class ItemSwapContainer
return m => set.TryGetValue( m, out var a ) ? a : m; return m => set.TryGetValue( m, out var a ) ? a : m;
} }
public Item[] LoadEquipment( Item from, Item to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true ) public EquipItem[] LoadEquipment( EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true )
{ {
Swaps.Clear(); Swaps.Clear();
Loaded = false; Loaded = false;
@ -141,7 +141,7 @@ public class ItemSwapContainer
return ret; return ret;
} }
public Item[] LoadTypeSwap( EquipSlot slotFrom, Item from, EquipSlot slotTo, Item to, ModCollection? collection = null ) public EquipItem[] LoadTypeSwap( EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null )
{ {
Swaps.Clear(); Swaps.Clear();
Loaded = false; Loaded = false;

View file

@ -2,7 +2,6 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
@ -21,6 +20,8 @@ using Penumbra.Interop.Services;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.UI.Tabs; using Penumbra.UI.Tabs;
using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
namespace Penumbra; namespace Penumbra;
@ -50,16 +51,17 @@ public class Penumbra : IDalamudPlugin
public Penumbra(DalamudPluginInterface pluginInterface) public Penumbra(DalamudPluginInterface pluginInterface)
{ {
try try
{ {
var startTimer = new StartTracker(); var startTimer = new StartTracker();
using var timer = startTimer.Measure(StartTimeType.Total); using var timer = startTimer.Measure(StartTimeType.Total);
_services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer); _services = ServiceManager.CreateProvider(this, pluginInterface, Log, startTimer);
ChatService = _services.GetRequiredService<ChatService>(); ChatService = _services.GetRequiredService<ChatService>();
_validityChecker = _services.GetRequiredService<ValidityChecker>(); _validityChecker = _services.GetRequiredService<ValidityChecker>();
var startup = _services.GetRequiredService<DalamudServices>().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s) var startup = _services.GetRequiredService<DalamudServices>().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool s)
? s.ToString() ? s.ToString()
: "Unknown"; : "Unknown";
Log.Information($"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}..."); Log.Information(
$"Loading Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} with Waiting For Plugins: {startup}...");
_services.GetRequiredService<BackupService>(); // Initialize because not required anywhere else. _services.GetRequiredService<BackupService>(); // Initialize because not required anywhere else.
_config = _services.GetRequiredService<Configuration>(); _config = _services.GetRequiredService<Configuration>();
_characterUtility = _services.GetRequiredService<CharacterUtility>(); _characterUtility = _services.GetRequiredService<CharacterUtility>();
@ -72,7 +74,7 @@ public class Penumbra : IDalamudPlugin
_redrawService = _services.GetRequiredService<RedrawService>(); _redrawService = _services.GetRequiredService<RedrawService>();
_communicatorService = _services.GetRequiredService<CommunicatorService>(); _communicatorService = _services.GetRequiredService<CommunicatorService>();
_services.GetRequiredService<ResourceService>(); // Initialize because not required anywhere else. _services.GetRequiredService<ResourceService>(); // Initialize because not required anywhere else.
_services.GetRequiredService<ModCacheManager>(); // Initialize because not required anywhere else. _services.GetRequiredService<ModCacheManager>(); // Initialize because not required anywhere else.
_collectionManager.Caches.CreateNecessaryCaches(); _collectionManager.Caches.CreateNecessaryCaches();
using (var t = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver)) using (var t = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
{ {
@ -91,8 +93,8 @@ public class Penumbra : IDalamudPlugin
if (_characterUtility.Ready) if (_characterUtility.Ready)
_residentResources.Reload(); _residentResources.Reload();
} }
catch(Exception ex) catch (Exception ex)
{ {
Log.Error($"Error constructing Penumbra, Disposing again:\n{ex}"); Log.Error($"Error constructing Penumbra, Disposing again:\n{ex}");
Dispose(); Dispose();
throw; throw;
@ -104,16 +106,17 @@ public class Penumbra : IDalamudPlugin
using var timer = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api); using var timer = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api);
var api = _services.GetRequiredService<IPenumbraApi>(); var api = _services.GetRequiredService<IPenumbraApi>();
_services.GetRequiredService<PenumbraIpcProviders>(); _services.GetRequiredService<PenumbraIpcProviders>();
api.ChangedItemTooltip += it => _communicatorService.ChangedItemHover.Subscribe(it =>
{ {
if (it is Item) if (it is Item)
ImGui.TextUnformatted("Left Click to create an item link in chat."); ImGui.TextUnformatted("Left Click to create an item link in chat.");
}; }, ChangedItemHover.Priority.Link);
api.ChangedItemClicked += (button, it) =>
_communicatorService.ChangedItemClick.Subscribe((button, it) =>
{ {
if (button == MouseButton.Left && it is Item item) if (button == MouseButton.Left && it is Item item)
ChatService.LinkItem(item); ChatService.LinkItem(item);
}; }, ChangedItemClick.Priority.Link);
} }
private void SetupInterface() private void SetupInterface()

View file

@ -129,14 +129,14 @@ public class ItemSwapTab : IDisposable, ITab
Weapon, Weapon,
} }
private class ItemSelector : FilterComboCache<(string, Item)> private class ItemSelector : FilterComboCache<EquipItem>
{ {
public ItemSelector(ItemService data, FullEquipType type) public ItemSelector(ItemService data, FullEquipType type)
: base(() => data.AwaitedService[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray()) : base(() => data.AwaitedService[type])
{ } { }
protected override string ToString((string, Item) obj) protected override string ToString(EquipItem obj)
=> obj.Item1; => obj.Name;
} }
private class WeaponSelector : FilterComboCache<FullEquipType> private class WeaponSelector : FilterComboCache<FullEquipType>
@ -179,7 +179,7 @@ public class ItemSwapTab : IDisposable, ITab
private bool _useLeftRing = true; private bool _useLeftRing = true;
private bool _useRightRing = true; private bool _useRightRing = true;
private Item[]? _affectedItems; private EquipItem[]? _affectedItems;
private void UpdateState() private void UpdateState()
{ {
@ -203,17 +203,16 @@ public class ItemSwapTab : IDisposable, ITab
case SwapType.Bracelet: case SwapType.Bracelet:
case SwapType.Ring: case SwapType.Ring:
var values = _selectors[_lastTab]; var values = _selectors[_lastTab];
if (values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null) if (values.Source.CurrentSelection.Type != FullEquipType.Unknown && values.Target.CurrentSelection.Type != FullEquipType.Unknown)
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection,
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
break; break;
case SwapType.BetweenSlots: case SwapType.BetweenSlots:
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null) if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid)
_affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection, _slotFrom, selectorFrom.CurrentSelection,
selectorFrom.CurrentSelection.Item2,
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0: case SwapType.Hair when _targetId > 0 && _sourceId > 0:
@ -468,7 +467,7 @@ public class ItemSwapTab : IDisposable, ITab
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Name ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()); ImGui.GetTextLineHeightWithSpacing());
(article1, _, selector) = GetAccessorySelector(_slotTo, false); (article1, _, selector) = GetAccessorySelector(_slotTo, false);
@ -493,7 +492,7 @@ public class ItemSwapTab : IDisposable, ITab
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Name, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()); ImGui.GetTextLineHeightWithSpacing());
if (_affectedItems is not { Length: > 1 }) if (_affectedItems is not { Length: > 1 })
return; return;
@ -502,8 +501,8 @@ public class ItemSwapTab : IDisposable, ITab
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
Colors.PressEnterWarningBg); Colors.PressEnterWarningBg);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, selector.CurrentSelection.Item2)) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name))
.Select(i => i.Name.ToDalamudString().TextValue))); .Select(i => i.Name)));
} }
private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source) private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source)
@ -534,7 +533,7 @@ public class ItemSwapTab : IDisposable, ITab
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(text1); ImGui.TextUnformatted(text1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Name, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()); ImGui.GetTextLineHeightWithSpacing());
if (type == SwapType.Ring) if (type == SwapType.Ring)
@ -547,7 +546,7 @@ public class ItemSwapTab : IDisposable, ITab
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(text2); ImGui.TextUnformatted(text2);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Name, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()); ImGui.GetTextLineHeightWithSpacing());
if (type == SwapType.Ring) if (type == SwapType.Ring)
{ {
@ -562,8 +561,8 @@ public class ItemSwapTab : IDisposable, ITab
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
Colors.PressEnterWarningBg); Colors.PressEnterWarningBg);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name))
.Select(i => i.Name.ToDalamudString().TextValue))); .Select(i => i.Name)));
} }
private void DrawHairSwap() private void DrawHairSwap()
@ -647,14 +646,14 @@ public class ItemSwapTab : IDisposable, ITab
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("and put this variant of it"); ImGui.TextUnformatted("and put this variant of it");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Name, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()); ImGui.GetTextLineHeightWithSpacing());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("onto this one"); ImGui.TextUnformatted("onto this one");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Name, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()); ImGui.GetTextLineHeightWithSpacing());
} }

View file

@ -4,15 +4,19 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET; using ImGuiNET;
using ImGuiScene; using ImGuiScene;
using Lumina.Data.Parsing; using Lumina.Data.Files;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using OtterGui; using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -20,18 +24,42 @@ namespace Penumbra.UI;
public class ChangedItemDrawer : IDisposable public class ChangedItemDrawer : IDisposable
{ {
private const EquipSlot MonsterSlot = (EquipSlot)100; [Flags]
private const EquipSlot DemihumanSlot = (EquipSlot)101; public enum ChangedItemIcon : uint
private const EquipSlot CustomizationSlot = (EquipSlot)102;
private const EquipSlot ActionSlot = (EquipSlot)103;
private readonly CommunicatorService _communicator;
private readonly Dictionary<EquipSlot, TextureWrap> _icons = new(16);
public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator)
{ {
Head = 0x0001,
Body = 0x0002,
Hands = 0x0004,
Legs = 0x0008,
Feet = 0x0010,
Ears = 0x0020,
Neck = 0x0040,
Wrists = 0x0080,
Finger = 0x0100,
Monster = 0x0200,
Demihuman = 0x0400,
Customization = 0x0800,
Action = 0x1000,
Mainhand = 0x2000,
Offhand = 0x4000,
Unknown = 0x8000,
}
public const ChangedItemIcon AllFlags = (ChangedItemIcon)0xFFFF;
public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand;
private readonly Configuration _config;
private readonly ExcelSheet<Item> _items;
private readonly CommunicatorService _communicator;
private readonly Dictionary<ChangedItemIcon, TextureWrap> _icons = new(16);
private float _smallestIconWidth = 0;
public ChangedItemDrawer(UiBuilder uiBuilder, DataManager gameData, CommunicatorService communicator, Configuration config)
{
_items = gameData.GetExcelSheet<Item>()!;
uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData), true); uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData), true);
_communicator = communicator; _communicator = communicator;
_config = config;
} }
public void Dispose() public void Dispose()
@ -41,37 +69,49 @@ public class ChangedItemDrawer : IDisposable
_icons.Clear(); _icons.Clear();
} }
/// <summary> Apply Changed Item Counters to the Name if necessary. </summary> /// <summary> Check if a changed item should be drawn based on its category. </summary>
public static string ChangedItemName(string name, object? data) public bool FilterChangedItem(string name, object? data, LowerString filter)
=> data is int counter ? $"{counter} Files Manipulating {name}s" : name; => (_config.ChangedItemFilter == AllFlags || _config.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data)))
&& (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data)));
/// <summary> Add filterable information to the string. </summary> /// <summary> Draw the icon corresponding to the category of a changed item. </summary>
public static string ChangedItemFilterName(string name, object? data) public void DrawCategoryIcon(string name, object? data)
=> data switch {
var height = ImGui.GetFrameHeight();
var iconType = GetCategoryIcon(name, data);
if (!_icons.TryGetValue(iconType, out var icon))
{ {
int counter => $"{counter} Files Manipulating {name}s", ImGui.Dummy(new Vector2(height));
Item it => $"{name}\0{((EquipSlot)it.EquipSlotCategory.Row).ToName()}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", return;
ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", }
_ => name,
}; ImGui.Image(icon.ImGuiHandle, new Vector2(height));
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
ImGui.SameLine();
ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0);
}
}
/// <summary> /// <summary>
/// Draw a changed item, invoking the Api-Events for clicks and tooltips. /// 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> /// </summary>
public void DrawChangedItem(string name, object? data, bool drawId) public void DrawChangedItem(string name, object? data)
{ {
name = ChangedItemName(name, data); name = ChangedItemName(name, data);
DrawCategoryIcon(name, data);
ImGui.SameLine();
using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f))
.Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) .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())) ? MouseButton.Left : MouseButton.None; var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
? MouseButton.Left
: MouseButton.None;
ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret;
ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret;
if (ret != MouseButton.None) if (ret != MouseButton.None)
_communicator.ChangedItemClick.Invoke(ret, data); _communicator.ChangedItemClick.Invoke(ret, Convert(data));
} }
if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered())
@ -80,13 +120,17 @@ public class ChangedItemDrawer : IDisposable
// Circumvent ugly blank tooltip with less-ugly useless tooltip. // Circumvent ugly blank tooltip with less-ugly useless tooltip.
using var tt = ImRaii.Tooltip(); using var tt = ImRaii.Tooltip();
using var group = ImRaii.Group(); using var group = ImRaii.Group();
_communicator.ChangedItemHover.Invoke(data); _communicator.ChangedItemHover.Invoke(Convert(data));
group.Dispose(); group.Dispose();
if (ImGui.GetItemRectSize() == Vector2.Zero) if (ImGui.GetItemRectSize() == Vector2.Zero)
ImGui.TextUnformatted("No actions available."); ImGui.TextUnformatted("No actions available.");
} }
}
if (!drawId || !GetChangedItemObject(data, out var text)) /// <summary> Draw the model information, right-justified. </summary>
public void DrawModelData(object? data)
{
if (!GetChangedItemObject(data, out var text))
return; return;
ImGui.SameLine(ImGui.GetContentRegionAvail().X); ImGui.SameLine(ImGui.GetContentRegionAvail().X);
@ -94,58 +138,147 @@ public class ChangedItemDrawer : IDisposable
ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); ImGuiUtil.RightJustify(text, ColorId.ItemId.Value());
} }
private void DrawCategoryIcon(string name, object? obj) /// <summary> Draw a header line with the different icon types to filter them. </summary>
public void DrawTypeFilter()
{ {
var height = ImGui.GetFrameHeight(); using var _ = ImRaii.PushId("ChangedItemIconFilter");
var slot = EquipSlot.Unknown; var available = ImGui.GetContentRegionAvail().X;
var desc = string.Empty; var (numLines, size) = available / _icons.Count > ImGui.GetTextLineHeight() * 2
if (obj is Item it) ? (1, new Vector2(Math.Min(_smallestIconWidth, available / _icons.Count)))
{ : (2, new Vector2(Math.Min(_smallestIconWidth, 2 * available / _icons.Count)));
slot = (EquipSlot)it.EquipSlotCategory.Row; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
desc = slot.ToName(); var lines = numLines == 2
} ? new[]
else if (obj is ModelChara m)
{
(slot, desc) = (CharacterBase.ModelType)m.Type switch
{ {
CharacterBase.ModelType.DemiHuman => (DemihumanSlot, "Demi-Human"), new[]
CharacterBase.ModelType.Monster => (MonsterSlot, "Monster"), {
_ => (EquipSlot.Unknown, string.Empty), ChangedItemIcon.Head,
ChangedItemIcon.Body,
ChangedItemIcon.Hands,
ChangedItemIcon.Legs,
ChangedItemIcon.Feet,
ChangedItemIcon.Mainhand,
ChangedItemIcon.Offhand,
ChangedItemIcon.Unknown,
},
new[]
{
ChangedItemIcon.Ears,
ChangedItemIcon.Neck,
ChangedItemIcon.Wrists,
ChangedItemIcon.Finger,
ChangedItemIcon.Customization,
ChangedItemIcon.Action,
ChangedItemIcon.Monster,
ChangedItemIcon.Demihuman,
},
}
: new[]
{
new[]
{
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.Monster,
ChangedItemIcon.Demihuman,
ChangedItemIcon.Unknown,
},
}; };
}
else if (name.StartsWith("Action: ")) void DrawIcon(ChangedItemIcon type)
{ {
(slot, desc) = (ActionSlot, "Action"); var icon = _icons[type];
} var flag = _config.ChangedItemFilter.HasFlag(type);
else if (name.StartsWith("Customization: ")) ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f));
{ if (ImGui.IsItemClicked())
(slot, desc) = (CustomizationSlot, "Customization"); {
_config.ChangedItemFilter = flag ? _config.ChangedItemFilter & ~type : _config.ChangedItemFilter | type;
_config.Save();
}
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
ImGui.SameLine();
ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0);
}
} }
if (!_icons.TryGetValue(slot, out var icon)) foreach (var line in lines)
{ {
ImGui.Dummy(new Vector2(height)); foreach (var iconType in line.SkipLast(1))
return; {
} DrawIcon(iconType);
ImGui.SameLine();
}
ImGui.Image(icon.ImGuiHandle, new Vector2(height)); DrawIcon(line.Last());
if (ImGui.IsItemHovered() && icon.Height > height)
{
using var tt = ImRaii.Tooltip();
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
ImGui.SameLine();
ImGuiUtil.DrawTextButton(desc, new Vector2(0, icon.Height), 0);
} }
} }
/// <summary> Obtain the icon category corresponding to a changed item. </summary>
private static ChangedItemIcon GetCategoryIcon(string name, object? obj)
{
var iconType = ChangedItemIcon.Unknown;
switch (obj)
{
case EquipItem it:
iconType = it.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,
};
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("Customization: "))
iconType = ChangedItemIcon.Customization;
break;
}
}
return iconType;
}
/// <summary> Return more detailed object information in text, if it exists. </summary> /// <summary> Return more detailed object information in text, if it exists. </summary>
public static bool GetChangedItemObject(object? obj, out string text) private static bool GetChangedItemObject(object? obj, out string text)
{ {
switch (obj) switch (obj)
{ {
case Item it: case EquipItem it:
var quad = (Quad)it.ModelMain; text = it.WeaponType == 0 ? $"({it.ModelId.Value}-{it.Variant})" : $"({it.ModelId.Value}-{it.WeaponType.Value}-{it.Variant})";
text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})";
return true; return true;
case ModelChara m: case ModelChara m:
text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})";
@ -156,6 +289,51 @@ public class ChangedItemDrawer : IDisposable
} }
} }
/// <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.Id);
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.Mainhand => "Weapon (Mainhand)",
ChangedItemIcon.Offhand => "Weapon (Offhand)",
_ => "Other",
};
/// <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(UiBuilder uiBuilder, DataManager gameData) private bool CreateEquipSlotIcons(UiBuilder uiBuilder, DataManager gameData)
{ {
using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld");
@ -163,94 +341,40 @@ public class ChangedItemDrawer : IDisposable
if (!equipTypeIcons.Valid) if (!equipTypeIcons.Valid)
return false; return false;
// Weapon void Add(ChangedItemIcon icon, TextureWrap? tex)
var tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0);
if (tex != null)
{ {
_icons.Add(EquipSlot.MainHand, tex); if (tex != null)
_icons.Add(EquipSlot.BothHand, tex); _icons.Add(icon, tex);
} }
// Hat Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0));
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1); Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1));
if (tex != null) Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2));
_icons.Add(EquipSlot.Head, tex); 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, gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex"));
Add(ChangedItemIcon.Demihuman, gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex"));
Add(ChangedItemIcon.Customization, gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex"));
Add(ChangedItemIcon.Action, gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex"));
// Body var unk = gameData.GetFile<TexFile>("ui/uld/levelup2_hr1.tex");
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2); if (unk == null)
if (tex != null) return true;
{
_icons.Add(EquipSlot.Body, tex);
_icons.Add(EquipSlot.BodyHands, tex);
_icons.Add(EquipSlot.BodyHandsLegsFeet, tex);
_icons.Add(EquipSlot.BodyLegsFeet, tex);
_icons.Add(EquipSlot.ChestHands, tex);
_icons.Add(EquipSlot.FullBody, tex);
_icons.Add(EquipSlot.HeadBody, tex);
}
// Hands var image = unk.GetRgbaImageData();
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3); var bytes = new byte[unk.Header.Height * unk.Header.Height * 4];
if (tex != null) var diff = 2 * (unk.Header.Height - unk.Header.Width);
_icons.Add(EquipSlot.Hands, tex); for (var y = 0; y < unk.Header.Height; ++y)
image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff));
Add(ChangedItemIcon.Unknown, uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4));
// Pants _smallestIconWidth = _icons.Values.Min(i => i.Width);
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5);
if (tex != null)
{
_icons.Add(EquipSlot.Legs, tex);
_icons.Add(EquipSlot.LegsFeet, tex);
}
// Shoes
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6);
if (tex != null)
_icons.Add(EquipSlot.Feet, tex);
// Offhand
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7);
if (tex != null)
_icons.Add(EquipSlot.OffHand, tex);
// Earrings
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8);
if (tex != null)
_icons.Add(EquipSlot.Ears, tex);
// Necklace
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9);
if (tex != null)
_icons.Add(EquipSlot.Neck, tex);
// Bracelet
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10);
if (tex != null)
_icons.Add(EquipSlot.Wrists, tex);
// Ring
tex = equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11);
if (tex != null)
_icons.Add(EquipSlot.RFinger, tex);
// Monster
tex = gameData.GetImGuiTexture("ui/icon/062000/062042_hr1.tex");
if (tex != null)
_icons.Add(MonsterSlot, tex);
// Demihuman
tex = gameData.GetImGuiTexture("ui/icon/062000/062041_hr1.tex");
if (tex != null)
_icons.Add(DemihumanSlot, tex);
// Customization
tex = gameData.GetImGuiTexture("ui/icon/062000/062043_hr1.tex");
if (tex != null)
_icons.Add(CustomizationSlot, tex);
// Action
tex = gameData.GetImGuiTexture("ui/icon/062000/062001_hr1.tex");
if (tex != null)
_icons.Add(ActionSlot, tex);
return true; return true;
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
@ -14,6 +15,8 @@ public class ModPanelChangedItemsTab : ITab
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ChangedItemDrawer _drawer; private readonly ChangedItemDrawer _drawer;
private ChangedItemDrawer.ChangedItemIcon _filter = Enum.GetValues<ChangedItemDrawer.ChangedItemIcon>().Aggregate((a, b) => a | b);
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Changed Items"u8; => "Changed Items"u8;
@ -28,12 +31,30 @@ public class ModPanelChangedItemsTab : ITab
public void DrawContent() public void DrawContent()
{ {
using var list = ImRaii.ListBox("##changedItems", -Vector2.One); _drawer.DrawTypeFilter();
if (!list) ImGui.Separator();
using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY,
new Vector2(ImGui.GetContentRegionAvail().X, -1));
if (!table)
return; return;
var zipList = ZipList.FromSortedList((SortedList<string, object?>)_selector.Selected!.ChangedItems); var zipList = ZipList.FromSortedList((SortedList<string, object?>)_selector.Selected!.ChangedItems);
var height = ImGui.GetFrameHeight(); var height = ImGui.GetFrameHeightWithSpacing();
ImGuiClip.ClippedDraw(zipList, kvp => _drawer.DrawChangedItem(kvp.Item1, kvp.Item2, true), height); ImGui.TableNextColumn();
var skips = ImGuiClip.GetNecessarySkips(height);
var remainder = ImGuiClip.FilteredClippedDraw(zipList, skips, CheckFilter, DrawChangedItem);
ImGuiClip.DrawEndDummy(remainder, height);
}
private bool CheckFilter((string Name, object? Data) kvp)
=> _drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty);
private void DrawChangedItem((string Name, object? Data) kvp)
{
ImGui.TableNextColumn();
_drawer.DrawCategoryIcon(kvp.Name, kvp.Data);
ImGui.SameLine();
_drawer.DrawChangedItem(kvp.Name, kvp.Data);
_drawer.DrawModelData(kvp.Data);
} }
} }

View file

@ -39,8 +39,9 @@ public class ChangedItemsTab : ITab
public void DrawContent() public void DrawContent()
{ {
_collectionHeader.Draw(true); _collectionHeader.Draw(true);
var varWidth = DrawFilters(); _drawer.DrawTypeFilter();
var varWidth = DrawFilters();
using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); using var child = ImRaii.Child("##changedItemsChild", -Vector2.One);
if (!child) if (!child)
return; return;
@ -57,9 +58,7 @@ public class ChangedItemsTab : ITab
ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale);
var items = _collectionManager.Active.Current.ChangedItems; var items = _collectionManager.Active.Current.ChangedItems;
var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);
? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count)
: ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);
ImGuiClip.DrawEndDummy(rest, height); ImGuiClip.DrawEndDummy(rest, height);
} }
@ -79,26 +78,21 @@ public class ChangedItemsTab : ITab
/// <summary> Apply the current filters. </summary> /// <summary> Apply the current filters. </summary>
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, object?)> item) private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
=> (_changedItemFilter.IsEmpty => _drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
|| ChangedItemDrawer.ChangedItemFilterName(item.Key, item.Value.Item2)
.Contains(_changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase))
&& (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
/// <summary> Draw a full column for a changed item. </summary> /// <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>, object?)> item)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_drawer.DrawChangedItem(item.Key, item.Value.Item2, false); _drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
ImGui.SameLine();
_drawer.DrawChangedItem(item.Key, item.Value.Item2);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
DrawModColumn(item.Value.Item1); DrawModColumn(item.Value.Item1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (!ChangedItemDrawer.GetChangedItemObject(item.Value.Item2, out var text)) _drawer.DrawModelData(item.Value.Item2);
return;
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value());
ImGui.AlignTextToFramePadding();
ImGuiUtil.RightAlign(text);
} }
private void DrawModColumn(SingleArray<IMod> mods) private void DrawModColumn(SingleArray<IMod> mods)
@ -110,7 +104,7 @@ public class ChangedItemsTab : ITab
using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f));
if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
&& ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyCtrl
&& first is Mod mod) && first is Mod mod)
_communicator.SelectTab.Invoke(TabType.Mods, mod); _communicator.SelectTab.Invoke(TabType.Mods, mod);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())