mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
Improve Debug Tab to contain all game data.
This commit is contained in:
parent
f2f16c664c
commit
7463aafa13
9 changed files with 380 additions and 248 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<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;
|
||||
|
|
@ -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<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()
|
||||
{
|
||||
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<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
|
||||
|
|
|
|||
|
|
@ -94,4 +94,7 @@ public readonly unsafe struct Actor : IEquatable<Actor>
|
|||
|
||||
public CharacterWeapon GetOffhand()
|
||||
=> *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel;
|
||||
|
||||
public override string ToString()
|
||||
=> $"0x{Address:X}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Model>
|
|||
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<Model>
|
|||
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<Model>
|
|||
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));
|
||||
}
|
||||
|
||||
/// <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}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public unsafe class WeaponService : IDisposable
|
|||
public WeaponService()
|
||||
{
|
||||
SignatureHelper.Initialise(this);
|
||||
_loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
|
||||
_loadWeaponHook = Hook<LoadWeaponDelegate>.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<LoadWeaponDelegate> _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.
|
||||
|
|
|
|||
|
|
@ -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<Lumina.Excel.GeneratedSheets.Item> 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<Lumina.Excel.GeneratedSheets.Item>()!;
|
||||
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)
|
||||
|
||||
public EquipItem 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:
|
||||
if (slot is EquipSlot.OffHand)
|
||||
{
|
||||
var weaponType = mainhandType.Offhand();
|
||||
if (id.Value == 0)
|
||||
return (true, NothingId(weaponType), Nothing, weaponType);
|
||||
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 != 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);
|
||||
}
|
||||
return item.Valid
|
||||
? item
|
||||
: new EquipItem($"Unknown ({id.Value}-{type.Value}-{variant})", 0, 0, id, type, variant, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ public static class ServiceManager
|
|||
=> services.AddSingleton<IdentifierService>()
|
||||
.AddSingleton<ItemService>()
|
||||
.AddSingleton<ActorService>()
|
||||
.AddSingleton<CustomizationService>();
|
||||
.AddSingleton<CustomizationService>()
|
||||
.AddSingleton<ItemManager>();
|
||||
|
||||
private static IServiceCollection AddInterop(this IServiceCollection services)
|
||||
=> services.AddSingleton<VisorService>()
|
||||
|
|
|
|||
|
|
@ -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<T>
|
||||
public abstract class AsyncServiceWrapper<T> : IDisposable
|
||||
{
|
||||
public string Name { get; }
|
||||
public T? Service { get; private set; }
|
||||
|
|
@ -102,4 +103,49 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
|
|||
public CustomizationService(DalamudPluginInterface pi, DataManager 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,4 +162,7 @@ public unsafe partial struct Actor : IEquatable<Actor>, IDesignable
|
|||
|
||||
public static bool operator !=(Actor lhs, Actor rhs)
|
||||
=> lhs.Pointer != rhs.Pointer;
|
||||
|
||||
public string AddressString()
|
||||
=> $"0x{Address:X}";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue