Improve Debug Tab to contain all game data.

This commit is contained in:
Ottermandias 2023-06-15 15:32:35 +02:00
parent f2f16c664c
commit 7463aafa13
9 changed files with 380 additions and 248 deletions

View file

@ -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;
}

View file

@ -2,10 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Interop; using Glamourer.Interop;
@ -31,16 +29,15 @@ public unsafe class DebugTab : ITab
private readonly PenumbraService _penumbra; private readonly PenumbraService _penumbra;
private readonly ObjectTable _objects; private readonly ObjectTable _objects;
private readonly IdentifierService _identifier; private readonly ItemManager _items;
private readonly ActorService _actors; private readonly ActorService _actors;
private readonly ItemService _items;
private readonly CustomizationService _customization; private readonly CustomizationService _customization;
private int _gameObjectIndex; private int _gameObjectIndex;
public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects, public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects,
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier, UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier,
ActorService actors, ItemService items, CustomizationService customization) ActorService actors, ItemManager items, CustomizationService customization)
{ {
_changeCustomizeService = changeCustomizeService; _changeCustomizeService = changeCustomizeService;
_visorService = visorService; _visorService = visorService;
@ -48,7 +45,6 @@ public unsafe class DebugTab : ITab
_updateSlotService = updateSlotService; _updateSlotService = updateSlotService;
_weaponService = weaponService; _weaponService = weaponService;
_penumbra = penumbra; _penumbra = penumbra;
_identifier = identifier;
_actors = actors; _actors = actors;
_items = items; _items = items;
_customization = customization; _customization = customization;
@ -84,23 +80,25 @@ public unsafe class DebugTab : ITab
ImGuiUtil.DrawTableColumn("Address"); ImGuiUtil.DrawTableColumn("Address");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (ImGui.Selectable($"0x{model.Address:X}")) ImGuiUtil.CopyOnClickSelectable(actor.ToString());
ImGui.SetClipboardText($"0x{model.Address:X}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (ImGui.Selectable($"0x{model.Address:X}")) ImGuiUtil.CopyOnClickSelectable(model.ToString());
ImGui.SetClipboardText($"0x{model.Address:X}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Mainhand"); ImGuiUtil.DrawTableColumn("Mainhand");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetMainhand().ToString() : "No Character"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetMainhand().ToString() : "No Character");
var (mainhand, offhand, mainModel, offModel) = model.GetWeapons(actor);
ImGuiUtil.DrawTableColumn(mainModel.ToString());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var weapon = model.AsDrawObject->Object.ChildObject; ImGuiUtil.CopyOnClickSelectable(mainhand.ToString());
if (ImGui.Selectable($"0x{(ulong)weapon:X}"))
ImGui.SetClipboardText($"0x{(ulong)weapon:X}");
ImGuiUtil.DrawTableColumn("Offhand"); ImGuiUtil.DrawTableColumn("Offhand");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetOffhand().ToString() : "No Character"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetOffhand().ToString() : "No Character");
if (weapon != null && ImGui.Selectable($"0x{(ulong)weapon->NextSiblingObject:X}")) ImGuiUtil.DrawTableColumn(offModel.ToString());
ImGui.SetClipboardText($"0x{(ulong)weapon->NextSiblingObject:X}"); ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable(offhand.ToString());
DrawVisor(actor, model); DrawVisor(actor, model);
DrawHatState(actor, model); DrawHatState(actor, model);
DrawWeaponState(actor, model); DrawWeaponState(actor, model);
@ -343,8 +341,10 @@ public unsafe class DebugTab : ITab
return; return;
DrawIdentifierService(); DrawIdentifierService();
DrawRestrictedGear();
DrawActorService(); DrawActorService();
DrawItemService(); DrawItemService();
DrawStainService();
DrawCustomizationService(); DrawCustomizationService();
} }
@ -355,9 +355,9 @@ public unsafe class DebugTab : ITab
private void DrawIdentifierService() 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"); using var tree = ImRaii.TreeNode("Identifier Service");
if (!tree || !_identifier.Valid) if (!tree || !_items.IdentifierService.Valid)
return; return;
disabled.Dispose(); disabled.Dispose();
@ -373,33 +373,73 @@ public unsafe class DebugTab : ITab
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
ImGui.InputTextWithHint("##gamePath", "Enter game path...", ref _gamePath, 256); 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( ImGui.TextUnformatted(
$"{fileInfo.ObjectType} {fileInfo.EquipSlot} {fileInfo.PrimaryId} {fileInfo.SecondaryId} {fileInfo.Variant} {fileInfo.BodySlot} {fileInfo.CustomizationType}"); $"{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.Separator();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Identify Model"); ImGui.TextUnformatted("Identify Model");
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); DrawInputModelSet(true);
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);
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)
{ {
var identified = _identifier.AwaitedService.Identify((SetId)_setId, (ushort)_variant, slot); var identified = _items.Identify(slot, (SetId)_setId, (byte)_variant);
Text(string.Join("\n", identified.Select(i => i.Name.ToDalamudString().TextValue))); 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); var weapon = _items.Identify(EquipSlot.MainHand, (SetId)_setId, (WeaponType)_secondaryId, (byte)_variant);
Text(string.Join("\n", main.Select(i => i.Name.ToDalamudString().TextValue))); Text(weapon.Name);
var off = _identifier.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.OffHand); ImGuiUtil.HoverTooltip(string.Join("\n",
Text(string.Join("\n", off.Select(i => i.Name.ToDalamudString().TextValue))); _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<Race>().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; private string _bnpcFilter = string.Empty;
@ -458,33 +498,106 @@ public unsafe class DebugTab : ITab
ImGuiClip.DrawEndDummy(remainder, height); ImGuiClip.DrawEndDummy(remainder, height);
} }
private string _itemFilter = string.Empty;
private void DrawItemService() 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"); using var tree = ImRaii.TreeNode("Item Manager");
if (!tree || !_items.Valid) if (!tree || !_items.ItemService.Valid)
return; return;
disabled.Dispose(); 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<FullEquipType>().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() private void DrawCustomizationService()
{ {
using var id = ImRaii.PushId("Customization"); using var disabled = ImRaii.Disabled(!_customization.Valid);
ImGuiUtil.DrawTableColumn("Customization Service"); using var tree = ImRaii.TreeNode("Customization Service");
ImGui.TableNextColumn(); if (!tree || !_customization.Valid)
if (!_customization.Valid)
{
ImGui.TextUnformatted("Unavailable");
ImGui.TableNextColumn();
return; 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); private void DrawCustomizationInfo(CustomizationSet set)
ImGui.TableNextColumn(); {
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}");
if (!tree) if (!tree)
return; return;
using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
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 #endregion

View file

@ -94,4 +94,7 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public CharacterWeapon GetOffhand() public CharacterWeapon GetOffhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel; => *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel;
public override string ToString()
=> $"0x{Address:X}";
} }

View file

@ -2,6 +2,7 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
namespace Glamourer.Interop.Structs; namespace Glamourer.Interop.Structs;
@ -21,12 +22,18 @@ public readonly unsafe struct Model : IEquatable<Model>
public CharacterBase* AsCharacterBase public CharacterBase* AsCharacterBase
=> (CharacterBase*)Address; => (CharacterBase*)Address;
public Weapon* AsWeapon
=> (Weapon*)Address;
public Human* AsHuman public Human* AsHuman
=> (Human*)Address; => (Human*)Address;
public static implicit operator Model(nint? pointer) public static implicit operator Model(nint? pointer)
=> new(pointer ?? nint.Zero); => new(pointer ?? nint.Zero);
public static implicit operator Model(Object* pointer)
=> new((nint)pointer);
public static implicit operator Model(DrawObject* pointer) public static implicit operator Model(DrawObject* pointer)
=> new((nint)pointer); => new((nint)pointer);
@ -48,6 +55,9 @@ public readonly unsafe struct Model : IEquatable<Model>
public bool IsHuman public bool IsHuman
=> IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human; => IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human;
public bool IsWeapon
=> IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Weapon;
public static implicit operator bool(Model actor) public static implicit operator bool(Model actor)
=> actor.Address != nint.Zero; => actor.Address != nint.Zero;
@ -79,14 +89,106 @@ public readonly unsafe struct Model : IEquatable<Model>
public CharacterArmor GetArmor(EquipSlot slot) public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()]; => ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()];
public CharacterWeapon GetMainhand() public (Model Address, CharacterWeapon Data) GetMainhand()
{ {
var weapon = AsDrawObject->Object.ChildObject; Model weapon = AsDrawObject->Object.ChildObject;
if (weapon == null) return !weapon.IsWeapon
return CharacterWeapon.Empty; ? (Null, CharacterWeapon.Empty)
weapon : (weapon, new CharacterWeapon(weapon.AsWeapon->ModelSetId, weapon.AsWeapon->SecondaryId, weapon.AsWeapon->Variant,
(StainId)weapon.AsWeapon->ModelUnknown));
} }
public CharacterWeapon GetOffhand() public (Model Address, CharacterWeapon Data) GetOffhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel; {
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));
}
/// <summary> Obtain the mainhand and offhand and their data by guesstimating which child object is which. </summary>
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);
}
}
/// <summary> Obtain the mainhand and offhand and their data by using the drawdata container from the corresponding actor. </summary>
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<Model> 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);
}
/// <summary> 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. </summary>
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}";
} }

View file

@ -13,7 +13,7 @@ public unsafe class WeaponService : IDisposable
public WeaponService() public WeaponService()
{ {
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_loadWeaponHook.Enable(); _loadWeaponHook.Enable();
} }
@ -22,18 +22,20 @@ public unsafe class WeaponService : IDisposable
_loadWeaponHook.Dispose(); _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<LoadWeaponDelegate> _loadWeaponHook; private readonly Hook<LoadWeaponDelegate> _loadWeaponHook;
private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
byte unk4) byte unk4)
{ {
var actor = (Actor) (nint)drawData->Unk8; var actor = (Actor) (nint)(drawData + 1);
// First call the regular function. // First call the regular function.
_loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4); _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. // Load a specific weapon for a character by its data and slot.

View file

@ -1,12 +1,8 @@
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Lumina.Text;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -20,22 +16,22 @@ public class ItemManager : IDisposable
public const string SmallClothesNpc = "Smallclothes (NPC)"; public const string SmallClothesNpc = "Smallclothes (NPC)";
public const ushort SmallClothesNpcModel = 9903; public const ushort SmallClothesNpcModel = 9903;
private readonly Configuration _config; public readonly IdentifierService IdentifierService;
public readonly IdentifierService IdentifierService; public readonly ExcelSheet<Lumina.Excel.GeneratedSheets.Item> ItemSheet;
public readonly ExcelSheet<Lumina.Excel.GeneratedSheets.Item> ItemSheet; public readonly StainData Stains;
public readonly StainData Stains; public readonly ItemService ItemService;
public readonly ItemService ItemService; public readonly RestrictedGear RestrictedGear;
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<Lumina.Excel.GeneratedSheets.Item>()!;
ItemSheet = gameData.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>()!;
IdentifierService = identifierService; IdentifierService = identifierService;
Stains = new StainData(pi, gameData, gameData.Language); Stains = new StainData(pi, gameData, gameData.Language);
ItemService = itemService; ItemService = itemService;
RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData); RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData);
DefaultSword = ItemSheet.GetRow(1601)!; // Weathered Shortsword DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)!); // Weathered Shortsword
} }
public void Dispose() public void Dispose()
@ -44,15 +40,12 @@ public class ItemManager : IDisposable
RestrictedGear.Dispose(); RestrictedGear.Dispose();
} }
public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
{ // TODO
if (_config.UseRestrictedGearProtection) //if (_config.UseRestrictedGearProtection)
return RestrictedGear.ResolveRestricted(armor, slot, race, gender); => RestrictedGear.ResolveRestricted(armor, slot, race, gender);
//return (false, armor);
return (false, armor);
}
public readonly Lumina.Excel.GeneratedSheets.Item DefaultSword;
public static uint NothingId(EquipSlot slot) public static uint NothingId(EquipSlot slot)
=> uint.MaxValue - 128 - (uint)slot.ToSlot(); => uint.MaxValue - 128 - (uint)slot.ToSlot();
@ -63,127 +56,66 @@ public class ItemManager : IDisposable
public static uint NothingId(FullEquipType type) public static uint NothingId(FullEquipType type)
=> uint.MaxValue - 384 - (uint)type; => uint.MaxValue - 384 - (uint)type;
public static Designs.Item NothingItem(EquipSlot slot) public static EquipItem NothingItem(EquipSlot slot)
{ => new(Nothing, NothingId(slot), 0, 0, 0, 0, slot.ToEquipType());
Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(NothingItem)} on {slot}.");
return new Designs.Item(Nothing, NothingId(slot), CharacterArmor.Empty);
}
public static Designs.Weapon NothingItem(FullEquipType type) public static EquipItem NothingItem(FullEquipType type)
{ => new(Nothing, NothingId(type), 0, 0, 0, 0, type);
Debug.Assert(type.ToSlot() == EquipSlot.OffHand, $"Called {nameof(NothingItem)} on {type}.");
return new Designs.Weapon(Nothing, NothingId(type), CharacterWeapon.Empty, type);
}
public static Designs.Item SmallClothesItem(EquipSlot slot) public static EquipItem SmallClothesItem(EquipSlot slot)
{ => new(SmallClothesNpc, SmallclothesId(slot), 0, SmallClothesNpcModel, 0, 1, slot.ToEquipType());
Debug.Assert(slot.IsEquipment(), $"Called {nameof(SmallClothesItem)} on {slot}.");
return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0));
}
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(); slot = slot.ToSlot();
if (itemId == NothingId(slot)) if (itemId == NothingId(slot))
return (true, 0, 0, Nothing); return NothingItem(slot);
if (itemId == SmallclothesId(slot)) if (itemId == SmallclothesId(slot))
return (true, SmallClothesNpcModel, 1, SmallClothesNpc); return SmallClothesItem(slot);
if (item == null || item.RowId != itemId) if (!ItemService.AwaitedService.TryGetValue(itemId, slot is EquipSlot.MainHand, out var item))
item = ItemSheet.GetRow(itemId); return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0);
if (item == null) if (item.Type.ToSlot() != slot)
return (false, 0, 0, string.Intern($"Unknown #{itemId}")); return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.ModelId, item.WeaponType, item.Variant, 0);
if (item.ToEquipType().ToSlot() != slot)
return (false, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"));
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) public EquipItem Identify(EquipSlot slot, SetId id, byte variant)
{
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)
{ {
slot = slot.ToSlot(); slot = slot.ToSlot();
if (!slot.IsEquipmentPiece()) 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) switch (id.Value)
{ {
case 0: return (true, NothingId(slot), Nothing); case 0: return NothingItem(slot);
case SmallClothesNpcModel: return (true, SmallclothesId(slot), SmallClothesNpc); case SmallClothesNpcModel: return SmallClothesItem(slot);
default: default:
var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault(); var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault();
return item == null return item.Valid
? (false, 0, string.Intern($"Unknown ({id.Value}-{variant})")) ? item
: (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue)); : 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(); public EquipItem Identify(EquipSlot slot, SetId id, WeaponType type, byte variant, FullEquipType mainhandType = FullEquipType.Unknown)
return item != null {
? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType()) if (slot is EquipSlot.OffHand)
: (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), {
weaponType); var weaponType = mainhandType.Offhand();
} if (id.Value == 0)
default: return (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), FullEquipType.Unknown); 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);
} }
} }

View file

@ -46,7 +46,8 @@ public static class ServiceManager
=> services.AddSingleton<IdentifierService>() => services.AddSingleton<IdentifierService>()
.AddSingleton<ItemService>() .AddSingleton<ItemService>()
.AddSingleton<ActorService>() .AddSingleton<ActorService>()
.AddSingleton<CustomizationService>(); .AddSingleton<CustomizationService>()
.AddSingleton<ItemManager>();
private static IServiceCollection AddInterop(this IServiceCollection services) private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton<VisorService>() => services.AddSingleton<VisorService>()

View file

@ -11,10 +11,11 @@ using Glamourer.Customization;
using Glamourer.Interop.Penumbra; using Glamourer.Interop.Penumbra;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums;
namespace Glamourer.Services; namespace Glamourer.Services;
public abstract class AsyncServiceWrapper<T> public abstract class AsyncServiceWrapper<T> : IDisposable
{ {
public string Name { get; } public string Name { get; }
public T? Service { get; private set; } public T? Service { get; private set; }
@ -102,4 +103,49 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
public CustomizationService(DalamudPluginInterface pi, DataManager gameData) public CustomizationService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData)) : base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
{ } { }
/// <summary> In languages other than english the actual clan name may depend on gender. </summary>
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",
};
}
} }

View file

@ -162,4 +162,7 @@ public unsafe partial struct Actor : IEquatable<Actor>, IDesignable
public static bool operator !=(Actor lhs, Actor rhs) public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Pointer != rhs.Pointer; => lhs.Pointer != rhs.Pointer;
public string AddressString()
=> $"0x{Address:X}";
} }