From 7463aafa13a346b5bee8cf7c08c043163a66b826 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 Jun 2023 15:32:35 +0200 Subject: [PATCH] Improve Debug Tab to contain all game data. --- Glamourer.GameData/Structs/Item2.cs | 70 --------- Glamourer/Gui/Tabs/DebugTab.cs | 203 +++++++++++++++++++++------ Glamourer/Interop/Structs/Actor.cs | 3 + Glamourer/Interop/Structs/Model.cs | 116 ++++++++++++++- Glamourer/Interop/WeaponService.cs | 12 +- Glamourer/Services/ItemManager.cs | 170 +++++++--------------- Glamourer/Services/ServiceManager.cs | 3 +- Glamourer/Services/ServiceWrapper.cs | 48 ++++++- GlamourerOld/Interop/Actor.cs | 3 + 9 files changed, 380 insertions(+), 248 deletions(-) delete mode 100644 Glamourer.GameData/Structs/Item2.cs diff --git a/Glamourer.GameData/Structs/Item2.cs b/Glamourer.GameData/Structs/Item2.cs deleted file mode 100644 index ad87191..0000000 --- a/Glamourer.GameData/Structs/Item2.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Structs; - -// An Item wrapper struct that contains the item table, a precomputed name and the associated equip slot. -public readonly struct Item2 -{ - public readonly Lumina.Excel.GeneratedSheets.Item Base; - public readonly string Name; - public readonly EquipSlot EquippableTo; - - // Obtain the main model info used by the item. - public (SetId id, WeaponType type, ushort variant) MainModel - => ParseModel(EquippableTo, Base.ModelMain); - - // Obtain the sub model info used by the item. Will be 0 if the item has no sub model. - public (SetId id, WeaponType type, ushort variant) SubModel - => ParseModel(EquippableTo, Base.ModelSub); - - public bool HasSubModel - => Base.ModelSub != 0; - - public bool IsBothHand - => (EquipSlot)Base.EquipSlotCategory.Row == EquipSlot.BothHand; - - public FullEquipType WeaponCategory - => ((WeaponCategory) (Base.ItemUICategory?.Row ?? 0)).ToEquipType(); - - // Create a new item from its sheet list with the given name and either the inferred equip slot or the given one. - public Item2(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown) - { - Base = item; - Name = name; - EquippableTo = slot == EquipSlot.Unknown ? ((EquipSlot)item.EquipSlotCategory.Row).ToSlot() : slot; - } - - // Create empty Nothing items. - public static Item2 Nothing(EquipSlot slot) - => new("Nothing", slot); - - // Produce the relevant model information for a given item and equip slot. - private static (SetId id, WeaponType type, ushort variant) ParseModel(EquipSlot slot, ulong data) - { - if (slot is EquipSlot.MainHand or EquipSlot.OffHand) - { - var id = (SetId)(data & 0xFFFF); - var type = (WeaponType)((data >> 16) & 0xFFFF); - var variant = (ushort)((data >> 32) & 0xFFFF); - return (id, type, variant); - } - else - { - var id = (SetId)(data & 0xFFFF); - var variant = (byte)((data >> 16) & 0xFF); - return (id, new WeaponType(), variant); - } - } - - // Used for 'Nothing' items. - private Item2(string name, EquipSlot slot) - { - Name = name; - Base = new Lumina.Excel.GeneratedSheets.Item(); - EquippableTo = slot; - } - - public override string ToString() - => Name; -} diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index b94ab8f..849ae54 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -2,10 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Customization; using Glamourer.Interop; @@ -31,16 +29,15 @@ public unsafe class DebugTab : ITab private readonly PenumbraService _penumbra; private readonly ObjectTable _objects; - private readonly IdentifierService _identifier; + private readonly ItemManager _items; private readonly ActorService _actors; - private readonly ItemService _items; private readonly CustomizationService _customization; private int _gameObjectIndex; public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects, UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier, - ActorService actors, ItemService items, CustomizationService customization) + ActorService actors, ItemManager items, CustomizationService customization) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -48,7 +45,6 @@ public unsafe class DebugTab : ITab _updateSlotService = updateSlotService; _weaponService = weaponService; _penumbra = penumbra; - _identifier = identifier; _actors = actors; _items = items; _customization = customization; @@ -84,23 +80,25 @@ public unsafe class DebugTab : ITab ImGuiUtil.DrawTableColumn("Address"); ImGui.TableNextColumn(); - if (ImGui.Selectable($"0x{model.Address:X}")) - ImGui.SetClipboardText($"0x{model.Address:X}"); + ImGuiUtil.CopyOnClickSelectable(actor.ToString()); ImGui.TableNextColumn(); - if (ImGui.Selectable($"0x{model.Address:X}")) - ImGui.SetClipboardText($"0x{model.Address:X}"); + ImGuiUtil.CopyOnClickSelectable(model.ToString()); ImGui.TableNextColumn(); ImGuiUtil.DrawTableColumn("Mainhand"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetMainhand().ToString() : "No Character"); + + var (mainhand, offhand, mainModel, offModel) = model.GetWeapons(actor); + ImGuiUtil.DrawTableColumn(mainModel.ToString()); ImGui.TableNextColumn(); - var weapon = model.AsDrawObject->Object.ChildObject; - if (ImGui.Selectable($"0x{(ulong)weapon:X}")) - ImGui.SetClipboardText($"0x{(ulong)weapon:X}"); + ImGuiUtil.CopyOnClickSelectable(mainhand.ToString()); + ImGuiUtil.DrawTableColumn("Offhand"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetOffhand().ToString() : "No Character"); - if (weapon != null && ImGui.Selectable($"0x{(ulong)weapon->NextSiblingObject:X}")) - ImGui.SetClipboardText($"0x{(ulong)weapon->NextSiblingObject:X}"); + ImGuiUtil.DrawTableColumn(offModel.ToString()); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(offhand.ToString()); + DrawVisor(actor, model); DrawHatState(actor, model); DrawWeaponState(actor, model); @@ -343,8 +341,10 @@ public unsafe class DebugTab : ITab return; DrawIdentifierService(); + DrawRestrictedGear(); DrawActorService(); DrawItemService(); + DrawStainService(); DrawCustomizationService(); } @@ -355,9 +355,9 @@ public unsafe class DebugTab : ITab private void DrawIdentifierService() { - using var disabled = ImRaii.Disabled(!_identifier.Valid); + using var disabled = ImRaii.Disabled(!_items.IdentifierService.Valid); using var tree = ImRaii.TreeNode("Identifier Service"); - if (!tree || !_identifier.Valid) + if (!tree || !_items.IdentifierService.Valid) return; disabled.Dispose(); @@ -373,33 +373,73 @@ public unsafe class DebugTab : ITab ImGui.SameLine(); ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); ImGui.InputTextWithHint("##gamePath", "Enter game path...", ref _gamePath, 256); - var fileInfo = _identifier.AwaitedService.GamePathParser.GetFileInfo(_gamePath); + var fileInfo = _items.IdentifierService.AwaitedService.GamePathParser.GetFileInfo(_gamePath); ImGui.TextUnformatted( $"{fileInfo.ObjectType} {fileInfo.EquipSlot} {fileInfo.PrimaryId} {fileInfo.SecondaryId} {fileInfo.Variant} {fileInfo.BodySlot} {fileInfo.CustomizationType}"); - Text(string.Join("\n", _identifier.AwaitedService.Identify(_gamePath).Keys)); + Text(string.Join("\n", _items.IdentifierService.AwaitedService.Identify(_gamePath).Keys)); ImGui.Separator(); + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Identify Model"); ImGui.SameLine(); - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##SetId", ref _setId, 0, 0); - ImGui.SameLine(); - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##TypeId", ref _secondaryId, 0, 0); - ImGui.SameLine(); - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##Variant", ref _variant, 0, 0); + DrawInputModelSet(true); foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var identified = _identifier.AwaitedService.Identify((SetId)_setId, (ushort)_variant, slot); - Text(string.Join("\n", identified.Select(i => i.Name.ToDalamudString().TextValue))); + var identified = _items.Identify(slot, (SetId)_setId, (byte)_variant); + Text(identified.Name); + ImGuiUtil.HoverTooltip(string.Join("\n", + _items.IdentifierService.AwaitedService.Identify((SetId)_setId, (ushort)_variant, slot).Select(i => i.Name))); } - var main = _identifier.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.MainHand); - Text(string.Join("\n", main.Select(i => i.Name.ToDalamudString().TextValue))); - var off = _identifier.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.OffHand); - Text(string.Join("\n", off.Select(i => i.Name.ToDalamudString().TextValue))); + var weapon = _items.Identify(EquipSlot.MainHand, (SetId)_setId, (WeaponType)_secondaryId, (byte)_variant); + Text(weapon.Name); + ImGuiUtil.HoverTooltip(string.Join("\n", + _items.IdentifierService.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.MainHand))); + } + + private void DrawRestrictedGear() + { + using var tree = ImRaii.TreeNode("Restricted Gear Service"); + if (!tree) + return; + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Resolve Model"); + DrawInputModelSet(false); + foreach (var race in Enum.GetValues().Skip(1)) + { + foreach (var gender in new[] + { + Gender.Male, + Gender.Female, + }) + { + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var (replaced, model) = + _items.RestrictedGear.ResolveRestricted(new CharacterArmor((SetId)_setId, (byte)_variant, 0), slot, race, gender); + if (replaced) + ImGui.TextUnformatted($"{race.ToName()} - {gender} - {slot.ToName()} resolves to {model}."); + } + } + } + } + + private void DrawInputModelSet(bool withWeapon) + { + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##SetId", ref _setId, 0, 0); + if (withWeapon) + { + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##TypeId", ref _secondaryId, 0, 0); + } + + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##Variant", ref _variant, 0, 0); } private string _bnpcFilter = string.Empty; @@ -458,33 +498,106 @@ public unsafe class DebugTab : ITab ImGuiClip.DrawEndDummy(remainder, height); } + private string _itemFilter = string.Empty; + private void DrawItemService() { - using var disabled = ImRaii.Disabled(!_items.Valid); + using var disabled = ImRaii.Disabled(!_items.ItemService.Valid); using var tree = ImRaii.TreeNode("Item Manager"); - if (!tree || !_items.Valid) + if (!tree || !_items.ItemService.Valid) return; disabled.Dispose(); + ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})", + ImGuiTreeNodeFlags.Leaf).Dispose(); + DrawNameTable("All Items (Main)", ref _itemFilter, + _items.ItemService.AwaitedService.AllItems(true).Select(p => (p.Item1, + $"{p.Item2.Name} ({(p.Item2.WeaponType == 0 ? p.Item2.Armor().ToString() : p.Item2.Weapon().ToString())})")) + .OrderBy(p => p.Item1)); + DrawNameTable("All Items (Off)", ref _itemFilter, + _items.ItemService.AwaitedService.AllItems(false).Select(p => (p.Item1, + $"{p.Item2.Name} ({(p.Item2.WeaponType == 0 ? p.Item2.Armor().ToString() : p.Item2.Weapon().ToString())})")) + .OrderBy(p => p.Item1)); + foreach (var type in Enum.GetValues().Skip(1)) + { + DrawNameTable(type.ToName(), ref _itemFilter, + _items.ItemService.AwaitedService[type] + .Select(p => (p.Id, $"{p.Name} ({(p.WeaponType == 0 ? p.Armor().ToString() : p.Weapon().ToString())})"))); + } + } + + private string _stainFilter = string.Empty; + + private void DrawStainService() + { + using var tree = ImRaii.TreeNode("Stain Service"); + if (!tree) + return; + + var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _stainFilter, 256); + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + using var table = ImRaii.Table("##table", 4, + ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit, + new Vector2(-1, 10 * height)); + if (!table) + return; + + if (resetScroll) + ImGui.SetScrollY(0); + + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(height); + ImGui.TableNextRow(); + var remainder = ImGuiClip.FilteredClippedDraw(_items.Stains, skips, + p => p.Key.Value.ToString().Contains(_stainFilter) || p.Value.Name.Contains(_stainFilter, StringComparison.OrdinalIgnoreCase), + p => + { + ImGuiUtil.DrawTableColumn(p.Key.Value.ToString("D3")); + ImGui.TableNextColumn(); + ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), + ImGui.GetCursorScreenPos() + new Vector2(ImGui.GetTextLineHeight()), + p.Value.RgbaColor, 5 * ImGuiHelpers.GlobalScale); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight())); + ImGuiUtil.DrawTableColumn(p.Value.Name); + ImGuiUtil.DrawTableColumn($"#{p.Value.R:X2}{p.Value.G:X2}{p.Value.B:X2}{(p.Value.Gloss ? ", Glossy" : string.Empty)}"); + }); + ImGuiClip.DrawEndDummy(remainder, height); } private void DrawCustomizationService() { - using var id = ImRaii.PushId("Customization"); - ImGuiUtil.DrawTableColumn("Customization Service"); - ImGui.TableNextColumn(); - if (!_customization.Valid) - { - ImGui.TextUnformatted("Unavailable"); - ImGui.TableNextColumn(); + using var disabled = ImRaii.Disabled(!_customization.Valid); + using var tree = ImRaii.TreeNode("Customization Service"); + if (!tree || !_customization.Valid) return; + + disabled.Dispose(); + + foreach (var clan in _customization.AwaitedService.Clans) + { + foreach (var gender in _customization.AwaitedService.Genders) + DrawCustomizationInfo(_customization.AwaitedService.GetList(clan, gender)); } + } - using var tree = ImRaii.TreeNode("Available###Customization", ImGuiTreeNodeFlags.NoTreePushOnOpen); - ImGui.TableNextColumn(); - + private void DrawCustomizationInfo(CustomizationSet set) + { + using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); if (!tree) return; + + using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + foreach (var index in Enum.GetValues()) + { + ImGuiUtil.DrawTableColumn(index.ToString()); + ImGuiUtil.DrawTableColumn(set.Option(index)); + ImGuiUtil.DrawTableColumn(set.IsAvailable(index) ? "Available" : "Unavailable"); + ImGuiUtil.DrawTableColumn(set.Type(index).ToString()); + ImGuiUtil.DrawTableColumn(set.Count(index).ToString()); + } } #endregion diff --git a/Glamourer/Interop/Structs/Actor.cs b/Glamourer/Interop/Structs/Actor.cs index e54d28b..5285e14 100644 --- a/Glamourer/Interop/Structs/Actor.cs +++ b/Glamourer/Interop/Structs/Actor.cs @@ -94,4 +94,7 @@ public readonly unsafe struct Actor : IEquatable public CharacterWeapon GetOffhand() => *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel; + + public override string ToString() + => $"0x{Address:X}"; } diff --git a/Glamourer/Interop/Structs/Model.cs b/Glamourer/Interop/Structs/Model.cs index a7f04d5..4c892ca 100644 --- a/Glamourer/Interop/Structs/Model.cs +++ b/Glamourer/Interop/Structs/Model.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; namespace Glamourer.Interop.Structs; @@ -21,12 +22,18 @@ public readonly unsafe struct Model : IEquatable public CharacterBase* AsCharacterBase => (CharacterBase*)Address; + public Weapon* AsWeapon + => (Weapon*)Address; + public Human* AsHuman => (Human*)Address; public static implicit operator Model(nint? pointer) => new(pointer ?? nint.Zero); + public static implicit operator Model(Object* pointer) + => new((nint)pointer); + public static implicit operator Model(DrawObject* pointer) => new((nint)pointer); @@ -48,6 +55,9 @@ public readonly unsafe struct Model : IEquatable public bool IsHuman => IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human; + public bool IsWeapon + => IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Weapon; + public static implicit operator bool(Model actor) => actor.Address != nint.Zero; @@ -79,14 +89,106 @@ public readonly unsafe struct Model : IEquatable public CharacterArmor GetArmor(EquipSlot slot) => ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()]; - public CharacterWeapon GetMainhand() + public (Model Address, CharacterWeapon Data) GetMainhand() { - var weapon = AsDrawObject->Object.ChildObject; - if (weapon == null) - return CharacterWeapon.Empty; - weapon + Model weapon = AsDrawObject->Object.ChildObject; + return !weapon.IsWeapon + ? (Null, CharacterWeapon.Empty) + : (weapon, new CharacterWeapon(weapon.AsWeapon->ModelSetId, weapon.AsWeapon->SecondaryId, weapon.AsWeapon->Variant, + (StainId)weapon.AsWeapon->ModelUnknown)); } - public CharacterWeapon GetOffhand() - => *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel; + public (Model Address, CharacterWeapon Data) GetOffhand() + { + var mainhand = AsDrawObject->Object.ChildObject; + if (mainhand == null) + return (Null, CharacterWeapon.Empty); + + Model offhand = mainhand->NextSiblingObject; + if (offhand == mainhand || !offhand.IsWeapon) + return (Null, CharacterWeapon.Empty); + + return (offhand, new CharacterWeapon(offhand.AsWeapon->ModelSetId, offhand.AsWeapon->SecondaryId, offhand.AsWeapon->Variant, + (StainId)offhand.AsWeapon->ModelUnknown)); + } + + /// Obtain the mainhand and offhand and their data by guesstimating which child object is which. + public (Model Mainhand, Model Offhand, CharacterWeapon MainData, CharacterWeapon OffData) GetWeapons() + { + var (first, second, count) = GetChildrenWeapons(); + switch (count) + { + case 0: return (Null, Null, CharacterWeapon.Empty, CharacterWeapon.Empty); + case 1: + return (first, Null, new CharacterWeapon(first.AsWeapon->ModelSetId, first.AsWeapon->SecondaryId, first.AsWeapon->Variant, + (StainId)first.AsWeapon->ModelUnknown), CharacterWeapon.Empty); + default: + var (main, off) = DetermineMainhand(first, second); + var mainData = new CharacterWeapon(main.AsWeapon->ModelSetId, main.AsWeapon->SecondaryId, main.AsWeapon->Variant, + (StainId)main.AsWeapon->ModelUnknown); + var offData = new CharacterWeapon(off.AsWeapon->ModelSetId, off.AsWeapon->SecondaryId, off.AsWeapon->Variant, + (StainId)off.AsWeapon->ModelUnknown); + return (main, off, mainData, offData); + } + } + + /// Obtain the mainhand and offhand and their data by using the drawdata container from the corresponding actor. + public (Model Mainhand, Model Offhand, CharacterWeapon MainData, CharacterWeapon OffData) GetWeapons(Actor actor) + { + if (!Valid || !actor.IsCharacter || actor.Model.Address != Address) + return (Null, Null, CharacterWeapon.Empty, CharacterWeapon.Empty); + + Model main = *((nint*)&actor.AsCharacter->DrawData.MainHand + 1); + var mainData = CharacterWeapon.Empty; + if (main.IsWeapon) + mainData = new CharacterWeapon(main.AsWeapon->ModelSetId, main.AsWeapon->SecondaryId, main.AsWeapon->Variant, + (StainId)main.AsWeapon->ModelUnknown); + else + main = Null; + Model off = *((nint*)&actor.AsCharacter->DrawData.OffHand + 1); + var offData = CharacterWeapon.Empty; + if (off.IsWeapon) + offData = new CharacterWeapon(off.AsWeapon->ModelSetId, off.AsWeapon->SecondaryId, off.AsWeapon->Variant, + (StainId)off.AsWeapon->ModelUnknown); + else + off = Null; + return (main, off, mainData, offData); + } + + private (Model, Model, int) GetChildrenWeapons() + { + Span weapons = stackalloc Model[2]; + weapons[0] = Null; + weapons[1] = Null; + var count = 0; + + if (!Valid || AsDrawObject->Object.ChildObject == null) + return (weapons[0], weapons[1], count); + + Model starter = AsDrawObject->Object.ChildObject; + var iterator = starter; + do + { + if (iterator.IsWeapon) + weapons[count++] = iterator; + if (count == 2) + return (weapons[0], weapons[1], count); + + iterator = iterator.AsDrawObject->Object.NextSiblingObject; + } while (iterator.Address != starter.Address); + + return (weapons[0], weapons[1], count); + } + + /// I don't know a safe way to do this but in experiments this worked. + /// The first uint at +0x8 was set to non-zero for the mainhand and zero for the offhand. + private static (Model Mainhand, Model Offhand) DetermineMainhand(Model first, Model second) + { + var discriminator1 = *(ulong*)(first.Address + 0x10); + var discriminator2 = *(ulong*)(second.Address + 0x10); + return discriminator1 == 0 && discriminator2 != 0 ? (second, first) : (first, second); + } + + public override string ToString() + => $"0x{Address:X}"; } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index 14004fd..cece053 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -13,7 +13,7 @@ public unsafe class WeaponService : IDisposable public WeaponService() { SignatureHelper.Initialise(this); - _loadWeaponHook = Hook.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); + _loadWeaponHook = Hook.FromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _loadWeaponHook.Enable(); } @@ -22,18 +22,20 @@ public unsafe class WeaponService : IDisposable _loadWeaponHook.Dispose(); } - private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, byte unk4); + private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, + byte skipGameObject, byte unk4); private readonly Hook _loadWeaponHook; private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, byte unk4) { - var actor = (Actor) (nint)drawData->Unk8; - + var actor = (Actor) (nint)(drawData + 1); + // First call the regular function. _loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4); - Item.Log.Information($"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); + Item.Log.Information( + $"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); } // Load a specific weapon for a character by its data and slot. diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index a815c7b..bc24ff9 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -1,12 +1,8 @@ using System; -using System.Diagnostics; using System.Linq; using Dalamud.Data; using Dalamud.Plugin; -using Dalamud.Utility; using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; -using Lumina.Text; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -20,22 +16,22 @@ public class ItemManager : IDisposable public const string SmallClothesNpc = "Smallclothes (NPC)"; public const ushort SmallClothesNpcModel = 9903; - private readonly Configuration _config; - public readonly IdentifierService IdentifierService; - public readonly ExcelSheet ItemSheet; - public readonly StainData Stains; - public readonly ItemService ItemService; - public readonly RestrictedGear RestrictedGear; + public readonly IdentifierService IdentifierService; + public readonly ExcelSheet ItemSheet; + public readonly StainData Stains; + public readonly ItemService ItemService; + public readonly RestrictedGear RestrictedGear; - public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config) + public readonly EquipItem DefaultSword; + + public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService) { - _config = config; - ItemSheet = gameData.GetExcelSheet()!; + ItemSheet = gameData.GetExcelSheet()!; IdentifierService = identifierService; Stains = new StainData(pi, gameData, gameData.Language); ItemService = itemService; RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData); - DefaultSword = ItemSheet.GetRow(1601)!; // Weathered Shortsword + DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)!); // Weathered Shortsword } public void Dispose() @@ -44,15 +40,12 @@ public class ItemManager : IDisposable RestrictedGear.Dispose(); } + public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) - { - if (_config.UseRestrictedGearProtection) - return RestrictedGear.ResolveRestricted(armor, slot, race, gender); - - return (false, armor); - } - - public readonly Lumina.Excel.GeneratedSheets.Item DefaultSword; + // TODO + //if (_config.UseRestrictedGearProtection) + => RestrictedGear.ResolveRestricted(armor, slot, race, gender); + //return (false, armor); public static uint NothingId(EquipSlot slot) => uint.MaxValue - 128 - (uint)slot.ToSlot(); @@ -63,127 +56,66 @@ public class ItemManager : IDisposable public static uint NothingId(FullEquipType type) => uint.MaxValue - 384 - (uint)type; - public static Designs.Item NothingItem(EquipSlot slot) - { - Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(NothingItem)} on {slot}."); - return new Designs.Item(Nothing, NothingId(slot), CharacterArmor.Empty); - } + public static EquipItem NothingItem(EquipSlot slot) + => new(Nothing, NothingId(slot), 0, 0, 0, 0, slot.ToEquipType()); - public static Designs.Weapon NothingItem(FullEquipType type) - { - Debug.Assert(type.ToSlot() == EquipSlot.OffHand, $"Called {nameof(NothingItem)} on {type}."); - return new Designs.Weapon(Nothing, NothingId(type), CharacterWeapon.Empty, type); - } + public static EquipItem NothingItem(FullEquipType type) + => new(Nothing, NothingId(type), 0, 0, 0, 0, type); - public static Designs.Item SmallClothesItem(EquipSlot slot) - { - Debug.Assert(slot.IsEquipment(), $"Called {nameof(SmallClothesItem)} on {slot}."); - return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0)); - } + public static EquipItem SmallClothesItem(EquipSlot slot) + => new(SmallClothesNpc, SmallclothesId(slot), 0, SmallClothesNpcModel, 0, 1, slot.ToEquipType()); - public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) + public EquipItem Resolve(EquipSlot slot, uint itemId) { slot = slot.ToSlot(); if (itemId == NothingId(slot)) - return (true, 0, 0, Nothing); + return NothingItem(slot); if (itemId == SmallclothesId(slot)) - return (true, SmallClothesNpcModel, 1, SmallClothesNpc); + return SmallClothesItem(slot); - if (item == null || item.RowId != itemId) - item = ItemSheet.GetRow(itemId); + if (!ItemService.AwaitedService.TryGetValue(itemId, slot is EquipSlot.MainHand, out var item)) + return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0); - if (item == null) - return (false, 0, 0, string.Intern($"Unknown #{itemId}")); - if (item.ToEquipType().ToSlot() != slot) - return (false, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})")); + if (item.Type.ToSlot() != slot) + return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.ModelId, item.WeaponType, item.Variant, 0); - return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue)); + return item; } - public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) - { - if (item == null || item.RowId != itemId) - item = ItemSheet.GetRow(itemId); - - if (item == null) - return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown); - - var type = item.ToEquipType(); - if (type.ToSlot() != EquipSlot.MainHand) - return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type); - - return (true, (SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32), - string.Intern(item.Name.ToDalamudString().TextValue), type); - } - - public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, - FullEquipType mainType, Lumina.Excel.GeneratedSheets.Item? item = null) - { - var offType = mainType.Offhand(); - if (itemId == NothingId(offType)) - return (true, 0, 0, 0, Nothing, offType); - - if (item == null || item.RowId != itemId) - item = ItemSheet.GetRow(itemId); - - if (item == null) - return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown); - - - var type = item.ToEquipType(); - if (offType != type) - return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type); - - var (m, w, v) = offType.ToSlot() == EquipSlot.MainHand - ? ((SetId)item.ModelSub, (WeaponType)(item.ModelSub >> 16), (byte)(item.ModelSub >> 32)) - : ((SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32)); - - return (true, m, w, v, string.Intern(item.Name.ToDalamudString().TextValue), type); - } - - public (bool Valid, uint ItemId, string ItemName) Identify(EquipSlot slot, SetId id, byte variant) + public EquipItem Identify(EquipSlot slot, SetId id, byte variant) { slot = slot.ToSlot(); if (!slot.IsEquipmentPiece()) - return (false, 0, string.Intern($"Unknown ({id.Value}-{variant})")); + return new EquipItem($"Invalid ({id.Value}-{variant})", 0, 0, id, 0, variant, 0); switch (id.Value) { - case 0: return (true, NothingId(slot), Nothing); - case SmallClothesNpcModel: return (true, SmallclothesId(slot), SmallClothesNpc); + case 0: return NothingItem(slot); + case SmallClothesNpcModel: return SmallClothesItem(slot); default: var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault(); - return item == null - ? (false, 0, string.Intern($"Unknown ({id.Value}-{variant})")) - : (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue)); + return item.Valid + ? item + : new EquipItem($"Unknown ({id.Value}-{variant})", 0, 0, id, 0, variant, 0); } } - public (bool Valid, uint ItemId, string ItemName, FullEquipType Type) Identify(EquipSlot slot, SetId id, WeaponType type, byte variant, - FullEquipType mainhandType = FullEquipType.Unknown) - { - switch (slot) - { - case EquipSlot.MainHand: - { - var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault(); - return item != null - ? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType()) - : (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), mainhandType); - } - case EquipSlot.OffHand: - { - var weaponType = mainhandType.Offhand(); - if (id.Value == 0) - return (true, NothingId(weaponType), Nothing, weaponType); - var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault(); - return item != null - ? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType()) - : (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), - weaponType); - } - default: return (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), FullEquipType.Unknown); + public EquipItem Identify(EquipSlot slot, SetId id, WeaponType type, byte variant, FullEquipType mainhandType = FullEquipType.Unknown) + { + if (slot is EquipSlot.OffHand) + { + var weaponType = mainhandType.Offhand(); + if (id.Value == 0) + return NothingItem(weaponType); } + + if (slot is not EquipSlot.MainHand and not EquipSlot.OffHand) + return new EquipItem($"Invalid ({id.Value}-{type.Value}-{variant})", 0, 0, id, type, variant, 0); + + var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault(); + return item.Valid + ? item + : new EquipItem($"Unknown ({id.Value}-{type.Value}-{variant})", 0, 0, id, type, variant, 0); } } diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 6ce8631..89c50f4 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -46,7 +46,8 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddInterop(this IServiceCollection services) => services.AddSingleton() diff --git a/Glamourer/Services/ServiceWrapper.cs b/Glamourer/Services/ServiceWrapper.cs index b9220cc..70610c0 100644 --- a/Glamourer/Services/ServiceWrapper.cs +++ b/Glamourer/Services/ServiceWrapper.cs @@ -11,10 +11,11 @@ using Glamourer.Customization; using Glamourer.Interop.Penumbra; using Penumbra.GameData.Data; using Penumbra.GameData; +using Penumbra.GameData.Enums; namespace Glamourer.Services; -public abstract class AsyncServiceWrapper +public abstract class AsyncServiceWrapper : IDisposable { public string Name { get; } public T? Service { get; private set; } @@ -102,4 +103,49 @@ public sealed class CustomizationService : AsyncServiceWrapper CustomizationManager.Create(pi, gameData)) { } + + /// In languages other than english the actual clan name may depend on gender. + public string ClanName(SubRace race, Gender gender) + { + if (gender == Gender.FemaleNpc) + gender = Gender.Female; + if (gender == Gender.MaleNpc) + gender = Gender.Male; + return (gender, race) switch + { + (Gender.Male, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderM), + (Gender.Male, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderM), + (Gender.Male, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodM), + (Gender.Male, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightM), + (Gender.Male, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkM), + (Gender.Male, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkM), + (Gender.Male, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunM), + (Gender.Male, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonM), + (Gender.Male, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfM), + (Gender.Male, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardM), + (Gender.Male, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenM), + (Gender.Male, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaM), + (Gender.Male, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), + (Gender.Male, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), + (Gender.Male, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaM), + (Gender.Male, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaM), + (Gender.Female, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderF), + (Gender.Female, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderF), + (Gender.Female, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodF), + (Gender.Female, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightF), + (Gender.Female, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkF), + (Gender.Female, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkF), + (Gender.Female, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunF), + (Gender.Female, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonF), + (Gender.Female, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfF), + (Gender.Female, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardF), + (Gender.Female, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenF), + (Gender.Female, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaF), + (Gender.Female, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), + (Gender.Female, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), + (Gender.Female, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaF), + (Gender.Female, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaF), + _ => "Unknown", + }; + } } diff --git a/GlamourerOld/Interop/Actor.cs b/GlamourerOld/Interop/Actor.cs index 5192637..3e1676a 100644 --- a/GlamourerOld/Interop/Actor.cs +++ b/GlamourerOld/Interop/Actor.cs @@ -162,4 +162,7 @@ public unsafe partial struct Actor : IEquatable, IDesignable public static bool operator !=(Actor lhs, Actor rhs) => lhs.Pointer != rhs.Pointer; + + public string AddressString() + => $"0x{Address:X}"; }