Add handling for Monk fist weapon hack, remove LoadEquipment hooks, and add item movement and plate apply hooks instead.

This commit is contained in:
Ottermandias 2023-07-22 02:16:54 +02:00
parent 323924fba2
commit 5874688838
19 changed files with 403 additions and 128 deletions

View file

@ -1,31 +0,0 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a game object updates an equipment piece in its model data.
/// <list type="number">
/// <item>Parameter is the character updating. </item>
/// <item>Parameter is the equipment slot changed. </item>
/// <item>Parameter is the model values to change the equipment piece to. </item>
/// </list>
/// </summary>
public sealed class EquipmentLoading : EventWrapper<Action<Actor, EquipSlot, CharacterArmor>, EquipmentLoading.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnEquipmentLoading"/>
StateListener = 0,
}
public EquipmentLoading()
: base(nameof(EquipmentLoading))
{ }
public void Invoke(Actor actor, EquipSlot slot, CharacterArmor armor)
=> Invoke(this, actor, slot, armor);
}

View file

@ -0,0 +1,28 @@
using System;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a game object updates an equipment piece in its model data.
/// <list type="number">
/// <item>Parameter is an array of slots updated and corresponding item ids and stains. </item>
/// </list>
/// </summary>
public sealed class MovedEquipment : EventWrapper<Action<(EquipSlot, uint, StainId)[]>, MovedEquipment.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnMovedEquipment"/>
StateListener = 0,
}
public MovedEquipment()
: base(nameof(MovedEquipment))
{ }
public void Invoke((EquipSlot, uint, StainId)[] items)
=> Invoke(this, items);
}

View file

@ -20,8 +20,7 @@ public partial class CustomizationDrawer
using (var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0))
{
// Print 1-based index instead of 0.
if (ImGui.ColorButton($"{current + 1}##color", color, ImGuiColorEditFlags.None, _framedIconSize))
if (ImGui.ColorButton($"{current}##color", color, ImGuiColorEditFlags.None, _framedIconSize))
ImGui.OpenPopup(ColorPickerPopupName);
}
@ -65,7 +64,7 @@ public partial class CustomizationDrawer
for (var i = 0; i < _currentCount; ++i)
{
var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]);
if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
if (ImGui.ColorButton(i.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
{
UpdateValue(custom.Value);
ImGui.CloseCurrentPopup();

View file

@ -31,6 +31,9 @@ public class EquipmentDrawer
private readonly TextureService _textures;
private readonly Configuration _config;
private float _requiredComboWidthUnscaled;
private float _requiredComboWidth;
public EquipmentDrawer(DataManager gameData, ItemManager items, CodeService codes, TextureService textures, Configuration config)
{
_items = items;
@ -60,6 +63,13 @@ public class EquipmentDrawer
{
_iconSize = new Vector2(2 * ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y);
_comboLength = DefaultWidth * ImGuiHelpers.GlobalScale;
if (_requiredComboWidthUnscaled == 0)
{
_requiredComboWidthUnscaled = _items.ItemService.AwaitedService.AllItems(true).Concat(_items.ItemService.AwaitedService.AllItems(false))
.Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X) / ImGuiHelpers.GlobalScale;
}
_requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale;
}
private bool VerifyRestrictedGear(EquipSlot slot, EquipItem gear, Gender gender, Race race)
@ -171,7 +181,7 @@ public class EquipmentDrawer
label = combo.Label;
using var disabled = ImRaii.Disabled(locked);
if (!combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength))
if (!combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth))
return false;
weapon = combo.CurrentSelection;
@ -189,7 +199,7 @@ public class EquipmentDrawer
label = combo.Label;
using var disabled = ImRaii.Disabled(locked);
var change = combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength);
var change = combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth);
if (change)
weapon = combo.CurrentSelection;
@ -225,7 +235,7 @@ public class EquipmentDrawer
label = combo.Label;
armor = current;
using var disabled = ImRaii.Disabled(locked);
var change = combo.Draw(armor.Name, armor.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength);
var change = combo.Draw(armor.Name, armor.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth);
if (change)
armor = combo.CurrentSelection;

View file

@ -19,6 +19,7 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
public readonly string Label;
private uint _currentItem;
private float _innerWidth;
public ItemCombo(DataManager gameData, ItemManager items, EquipSlot slot, TextureService textures)
: base(() => GetItems(items, slot))
@ -46,12 +47,16 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
return base.UpdateCurrentSelected(CurrentSelectionIdx);
}
public bool Draw(string previewName, uint previewIdx, float width)
public bool Draw(string previewName, uint previewIdx, float width, float innerWidth)
{
_innerWidth = innerWidth;
_currentItem = previewIdx;
return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
}
protected override float GetFilterWidth()
=> _innerWidth - 2 * ImGui.GetStyle().FramePadding.X;
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var obj = Items[globalIdx];

View file

@ -16,6 +16,7 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
{
public readonly string Label;
private uint _currentItemId;
private float _innerWidth;
public WeaponCombo(ItemManager items, FullEquipType type)
: base(() => GetWeapons(items, type))
@ -41,9 +42,13 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
return base.UpdateCurrentSelected(CurrentSelectionIdx);
}
public bool Draw(string previewName, uint previewId, float width)
protected override float GetFilterWidth()
=> _innerWidth - 2 * ImGui.GetStyle().FramePadding.X;
public bool Draw(string previewName, uint previewId, float width, float innerWidth)
{
_currentItemId = previewId;
_innerWidth = innerWidth;
return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
}

View file

@ -162,7 +162,7 @@ public class PenumbraChangedItemTooltip : IDisposable
switch (type)
{
case ChangedItemType.Item:
if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item))
if (!_items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out var item))
return;
CreateTooltip(item, "[Glamourer] ", false);
@ -192,7 +192,7 @@ public class PenumbraChangedItemTooltip : IDisposable
if (!Player(out var state))
return;
if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item))
if (!_items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out var item))
return;
ApplyItem(state, item);

View file

@ -9,6 +9,7 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Api;
using Glamourer.Automation;
@ -42,6 +43,7 @@ public unsafe class DebugTab : ITab
private readonly UpdateSlotService _updateSlotService;
private readonly WeaponService _weaponService;
private readonly MetaService _metaService;
private readonly InventoryService _inventoryService;
private readonly PenumbraService _penumbra;
private readonly ObjectTable _objects;
private readonly ObjectManager _objectManager;
@ -76,7 +78,7 @@ public unsafe class DebugTab : ITab
DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config,
PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface,
AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks,
ItemUnlockManager itemUnlocks, DesignConverter designConverter, DatFileService datFileService)
ItemUnlockManager itemUnlocks, DesignConverter designConverter, DatFileService datFileService, InventoryService inventoryService)
{
_changeCustomizeService = changeCustomizeService;
_visorService = visorService;
@ -103,6 +105,7 @@ public unsafe class DebugTab : ITab
_itemUnlocks = itemUnlocks;
_designConverter = designConverter;
_datFileService = datFileService;
_inventoryService = inventoryService;
}
public ReadOnlySpan<byte> Label
@ -120,6 +123,7 @@ public unsafe class DebugTab : ITab
DrawDesigns();
DrawState();
DrawAutoDesigns();
DrawInventory();
DrawUnlocks();
DrawIpc();
}
@ -860,7 +864,7 @@ public unsafe class DebugTab : ITab
if (!table)
return;
foreach(var (index, value) in set.NpcOptions)
foreach (var (index, value) in set.NpcOptions)
{
ImGuiUtil.DrawTableColumn(index.ToString());
ImGuiUtil.DrawTableColumn(value.Value.ToString());
@ -980,6 +984,7 @@ public unsafe class DebugTab : ITab
ImGui.SameLine();
}
ImGui.NewLine();
}
@ -996,6 +1001,7 @@ public unsafe class DebugTab : ITab
ImGui.SameLine();
}
ImGui.NewLine();
}
}
@ -1442,7 +1448,7 @@ public unsafe class DebugTab : ITab
var remainder = ImGuiClip.ClippedDraw(_itemUnlocks.Unlocked, skips, t =>
{
ImGuiUtil.DrawTableColumn(t.Key.ToString());
if (_items.ItemService.AwaitedService.TryGetValue(t.Key, out var equip))
if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip))
{
ImGuiUtil.DrawTableColumn(equip.Name);
ImGuiUtil.DrawTableColumn(equip.Type.ToName());
@ -1489,7 +1495,7 @@ public unsafe class DebugTab : ITab
var remainder = ImGuiClip.ClippedDraw(_itemUnlocks.Unlockable, skips, t =>
{
ImGuiUtil.DrawTableColumn(t.Key.ToString());
if (_items.ItemService.AwaitedService.TryGetValue(t.Key, out var equip))
if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip))
{
ImGuiUtil.DrawTableColumn(equip.Name);
ImGuiUtil.DrawTableColumn(equip.Type.ToName());
@ -1514,6 +1520,50 @@ public unsafe class DebugTab : ITab
#endregion
#region Inventory
private void DrawInventory()
{
if (!ImGui.CollapsingHeader("Inventory"))
return;
var inventory = InventoryManager.Instance();
if (inventory == null)
return;
ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)inventory:X}");
var equip = inventory->GetInventoryContainer(InventoryType.EquippedItems);
if (equip == null || equip->Loaded == 0)
return;
ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)equip:X}");
using var table = ImRaii.Table("items", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
for (var i = 0; i < equip->Size; ++i)
{
ImGuiUtil.DrawTableColumn(i.ToString());
var item = equip->GetInventorySlot(i);
if (item == null)
{
ImGuiUtil.DrawTableColumn("NULL");
ImGui.TableNextRow();
}
else
{
ImGuiUtil.DrawTableColumn(item->ItemID.ToString());
ImGuiUtil.DrawTableColumn(item->GlamourID.ToString());
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)item:X}");
}
}
}
#endregion
#region IPC
private string _gameObjectName = string.Empty;

View file

@ -179,7 +179,7 @@ public class UnlockOverview
ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})");
if (item.Type.ValidOffhand().IsOffhandType())
ImGui.TextUnformatted(
$"{item.Weapon()}{(_items.ItemService.AwaitedService.TryGetValue(item.ItemId, false, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}");
$"{item.Weapon()}{(_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}");
else
ImGui.TextUnformatted(slot is EquipSlot.MainHand ? $"{item.Weapon()}" : $"{item.Armor()}");
ImGui.TextUnformatted(

View file

@ -242,7 +242,7 @@ public class UnlockTable : Table<EquipItem>, IDisposable
ImGuiUtil.RightAlign(item.ModelString);
if (ImGui.IsItemHovered()
&& item.Type.ValidOffhand().IsOffhandType()
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, false, out var offhand))
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
{
using var tt = ImRaii.Tooltip();
ImGui.TextUnformatted("Offhand: " + offhand.ModelString);
@ -260,7 +260,7 @@ public class UnlockTable : Table<EquipItem>, IDisposable
if (FilterRegex?.IsMatch(item.ModelString) ?? item.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase))
return true;
if (item.Type.ValidOffhand().IsOffhandType() && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, false, out var offhand))
if (item.Type.ValidOffhand().IsOffhandType() && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
return FilterRegex?.IsMatch(offhand.ModelString)
?? offhand.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase);

View file

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Glamourer.Events;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe class InventoryService : IDisposable
{
private readonly MovedEquipment _event;
private readonly List<(EquipSlot, uint, StainId)> _itemList = new(12);
public InventoryService(MovedEquipment @event)
{
_event = @event;
_moveItemHook = Hook<MoveItemDelegate>.FromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour);
_equipGearsetHook =
Hook<EquipGearsetDelegate>.FromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour);
_moveItemHook.Enable();
_equipGearsetHook.Enable();
}
public void Dispose()
{
_moveItemHook.Dispose();
_equipGearsetHook.Dispose();
}
private delegate int EquipGearsetDelegate(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId);
private readonly Hook<EquipGearsetDelegate> _equipGearsetHook;
private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId)
{
var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId);
Glamourer.Log.Excessive($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})");
if (ret == 0)
{
var entry = module->GetGearset(gearsetId);
if (entry == null)
return ret;
if (glamourPlateId == 0)
glamourPlateId = entry->GlamourSetLink;
_itemList.Clear();
if (glamourPlateId != 0)
{
void Add(EquipSlot slot, uint glamourId, StainId glamourStain, ref RaptureGearsetModule.GearsetItem item)
{
if (item.ItemID == 0)
_itemList.Add((slot, 0, 0));
else if (glamourId != 0)
_itemList.Add((slot, glamourId, glamourStain));
else if (item.GlamourId != 0)
_itemList.Add((slot, item.GlamourId, item.Stain));
else
_itemList.Add((slot, item.ItemID, item.Stain));
}
var plate = MirageManager.Instance()->GlamourPlatesSpan[glamourPlateId - 1];
Add(EquipSlot.MainHand, plate.ItemIds[0], plate.StainIds[0], ref entry->MainHand);
Add(EquipSlot.OffHand, plate.ItemIds[1], plate.StainIds[10], ref entry->OffHand);
Add(EquipSlot.Head, plate.ItemIds[2], plate.StainIds[2], ref entry->Head);
Add(EquipSlot.Body, plate.ItemIds[3], plate.StainIds[3], ref entry->Body);
Add(EquipSlot.Hands, plate.ItemIds[4], plate.StainIds[4], ref entry->Hands);
Add(EquipSlot.Legs, plate.ItemIds[5], plate.StainIds[5], ref entry->Legs);
Add(EquipSlot.Feet, plate.ItemIds[6], plate.StainIds[6], ref entry->Feet);
Add(EquipSlot.Ears, plate.ItemIds[7], plate.StainIds[7], ref entry->Ears);
Add(EquipSlot.Neck, plate.ItemIds[8], plate.StainIds[8], ref entry->Neck);
Add(EquipSlot.Wrists, plate.ItemIds[9], plate.StainIds[9], ref entry->Wrists);
Add(EquipSlot.RFinger, plate.ItemIds[10], plate.StainIds[10], ref entry->RingRight);
Add(EquipSlot.LFinger, plate.ItemIds[11], plate.StainIds[11], ref entry->RightLeft);
}
else
{
void Add(EquipSlot slot, ref RaptureGearsetModule.GearsetItem item)
{
if (item.ItemID == 0)
_itemList.Add((slot, 0, 0));
else if (item.GlamourId != 0)
_itemList.Add((slot, item.GlamourId, item.Stain));
else
_itemList.Add((slot, item.ItemID, item.Stain));
}
Add(EquipSlot.MainHand, ref entry->MainHand);
Add(EquipSlot.OffHand, ref entry->OffHand);
Add(EquipSlot.Head, ref entry->Head);
Add(EquipSlot.Body, ref entry->Body);
Add(EquipSlot.Hands, ref entry->Hands);
Add(EquipSlot.Legs, ref entry->Legs);
Add(EquipSlot.Feet, ref entry->Feet);
Add(EquipSlot.Ears, ref entry->Ears);
Add(EquipSlot.Neck, ref entry->Neck);
Add(EquipSlot.Wrists, ref entry->Wrists);
Add(EquipSlot.RFinger, ref entry->RingRight);
Add(EquipSlot.LFinger, ref entry->RightLeft);
}
_event.Invoke(_itemList.ToArray());
}
return ret;
}
private delegate int MoveItemDelegate(InventoryManager* manager, InventoryType sourceContainer, ushort sourceSlot,
InventoryType targetContainer, ushort targetSlot, byte unk);
private readonly Hook<MoveItemDelegate> _moveItemHook;
private int MoveItemDetour(InventoryManager* manager, InventoryType sourceContainer, ushort sourceSlot,
InventoryType targetContainer, ushort targetSlot, byte unk)
{
var ret = _moveItemHook.Original(manager, sourceContainer, sourceSlot, targetContainer, targetSlot, unk);
Glamourer.Log.Excessive($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})");
if (ret == 0)
{
if (InvokeSource(sourceContainer, sourceSlot, out var source))
if (InvokeTarget(manager, targetContainer, targetSlot, out var target))
_event.Invoke(new[]
{
source,
target,
});
else
_event.Invoke(new[]
{
source,
});
else if (InvokeTarget(manager, targetContainer, targetSlot, out var target))
_event.Invoke(new[]
{
target,
});
}
return ret;
}
private static bool InvokeSource(InventoryType sourceContainer, uint sourceSlot, out (EquipSlot, uint, StainId) tuple)
{
tuple = default;
if (sourceContainer is not InventoryType.EquippedItems)
return false;
var slot = GetSlot(sourceSlot);
if (slot is EquipSlot.Unknown)
return false;
tuple = (slot, 0u, 0);
return true;
}
private static bool InvokeTarget(InventoryManager* manager, InventoryType targetContainer, uint targetSlot,
out (EquipSlot, uint, StainId) tuple)
{
tuple = default;
if (targetContainer is not InventoryType.EquippedItems)
return false;
var slot = GetSlot(targetSlot);
if (slot is EquipSlot.Unknown)
return false;
// Invoked after calling Original, so the item is already moved.
var inventory = manager->GetInventoryContainer(targetContainer);
if (inventory == null || inventory->Loaded == 0 || inventory->Size <= targetSlot)
return false;
var item = inventory->GetInventorySlot((int)targetSlot);
if (item == null)
return false;
tuple = (slot, item->GlamourID != 0 ? item->GlamourID : item->ItemID, item->Stain);
return true;
}
private static EquipSlot GetSlot(uint slot)
=> slot switch
{
0 => EquipSlot.MainHand,
1 => EquipSlot.OffHand,
2 => EquipSlot.Head,
3 => EquipSlot.Body,
4 => EquipSlot.Hands,
6 => EquipSlot.Legs,
7 => EquipSlot.Feet,
8 => EquipSlot.Ears,
9 => EquipSlot.Neck,
10 => EquipSlot.Wrists,
11 => EquipSlot.RFinger,
12 => EquipSlot.LFinger,
_ => EquipSlot.Unknown,
};
}

View file

@ -1,7 +1,6 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
@ -11,24 +10,18 @@ namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable
{
public readonly SlotUpdating SlotUpdatingEvent;
public readonly EquipmentLoading EquipmentLoadingEvent;
public readonly SlotUpdating SlotUpdatingEvent;
public UpdateSlotService(SlotUpdating slotUpdating, EquipmentLoading equipmentLoadingEvent)
public UpdateSlotService(SlotUpdating slotUpdating)
{
SlotUpdatingEvent = slotUpdating;
EquipmentLoadingEvent = equipmentLoadingEvent;
SlotUpdatingEvent = slotUpdating;
SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable();
_loadEquipmentHook =
Hook<LoadEquipmentDelegateIntern>.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadEquipment, LoadEquipmentDetour);
_loadEquipmentHook.Enable();
}
public void Dispose()
{
_flagSlotForUpdateHook.Dispose();
_loadEquipmentHook.Dispose();
}
public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
@ -53,10 +46,6 @@ public unsafe class UpdateSlotService : IDisposable
[Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagSlotForUpdateHook = null!;
private delegate void LoadEquipmentDelegateIntern(DrawDataContainer* drawDataContainer, uint slotIdx, CharacterArmor data, bool force);
private readonly Hook<LoadEquipmentDelegateIntern> _loadEquipmentHook = null!;
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
@ -66,14 +55,6 @@ public unsafe class UpdateSlotService : IDisposable
return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue;
}
private void LoadEquipmentDetour(DrawDataContainer* drawDataContainer, uint slotIdx, CharacterArmor data, bool force)
{
var slot = slotIdx.ToEquipSlot();
EquipmentLoadingEvent.Invoke(drawDataContainer->Parent, slot, data);
Glamourer.Log.Excessive($"[LoadEquipment] Called with 0x{(ulong)drawDataContainer:X} for slot {slot} with {data} ({force}).");
_loadEquipmentHook.Original(drawDataContainer, slotIdx, data, force);
}
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
}

View file

@ -4,10 +4,8 @@ using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using ImGuiNET;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using static FFXIVClientStructs.FFXIV.Client.UI.UIModule;
namespace Glamourer.Interop;
@ -75,14 +73,14 @@ public unsafe class WeaponService : IDisposable
switch (slot)
{
case EquipSlot.MainHand:
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0);
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0);
return;
case EquipSlot.OffHand:
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, weapon.Value, 0, 0, 1, 0);
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0);
return;
case EquipSlot.BothHand:
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0);
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0);
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0);
_loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0);
return;
// function can also be called with '2', but does not seem to ever be.
}

View file

@ -74,7 +74,7 @@ public class ItemManager : IDisposable
if (itemId == SmallclothesId(slot))
return SmallClothesItem(slot);
if (!ItemService.AwaitedService.TryGetValue(itemId, slot is not EquipSlot.OffHand, out var item))
if (!ItemService.AwaitedService.TryGetValue(itemId, slot, out var item))
return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0);
if (item.Type.ToSlot() != slot)
@ -88,7 +88,7 @@ public class ItemManager : IDisposable
if (itemId == NothingId(type))
return NothingItem(type);
if (!ItemService.AwaitedService.TryGetValue(itemId, type is FullEquipType.Shield, out var item))
if (!ItemService.AwaitedService.TryGetValue(itemId, type is FullEquipType.Shield ? EquipSlot.MainHand : EquipSlot.OffHand, out var item))
return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0);
if (item.Type != type)

View file

@ -61,7 +61,6 @@ public static class ServiceManager
private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton<VisorStateChanged>()
.AddSingleton<SlotUpdating>()
.AddSingleton<EquipmentLoading>()
.AddSingleton<DesignChanged>()
.AddSingleton<AutomationChanged>()
.AddSingleton<StateChanged>()
@ -69,7 +68,8 @@ public static class ServiceManager
.AddSingleton<HeadGearVisibilityChanged>()
.AddSingleton<WeaponVisibilityChanged>()
.AddSingleton<ObjectUnlocked>()
.AddSingleton<TabSelected>();
.AddSingleton<TabSelected>()
.AddSingleton<MovedEquipment>();
private static IServiceCollection AddData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>()
@ -91,7 +91,8 @@ public static class ServiceManager
.AddSingleton<JobService>()
.AddSingleton<CustomizeUnlockManager>()
.AddSingleton<ItemUnlockManager>()
.AddSingleton<DatFileService>();
.AddSingleton<DatFileService>()
.AddSingleton<InventoryService>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()

View file

@ -75,8 +75,8 @@ public abstract class AsyncServiceWrapper<T> : IDisposable
public sealed class IdentifierService : AsyncServiceWrapper<IObjectIdentifier>
{
public IdentifierService(DalamudPluginInterface pi, DataManager data)
: base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data))
public IdentifierService(DalamudPluginInterface pi, DataManager data, ItemService itemService)
: base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data, itemService.AwaitedService))
{ }
}

View file

@ -2,6 +2,7 @@
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
@ -22,12 +23,12 @@ public class StateListener : IDisposable
{
private readonly Configuration _config;
private readonly ActorService _actors;
private readonly ObjectManager _objects;
private readonly StateManager _manager;
private readonly StateApplier _applier;
private readonly ItemManager _items;
private readonly PenumbraService _penumbra;
private readonly SlotUpdating _slotUpdating;
private readonly EquipmentLoading _equipmentLoading;
private readonly WeaponLoading _weaponLoading;
private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState;
@ -35,9 +36,11 @@ public class StateListener : IDisposable
private readonly AutoDesignApplier _autoDesignApplier;
private readonly FunModule _funModule;
private readonly HumanModelList _humans;
private readonly MovedEquipment _movedEquipment;
private ActorIdentifier _creatingIdentifier = ActorIdentifier.Invalid;
private ActorState? _creatingState = null;
private ActorState? _creatingState;
private CharacterWeapon _lastFistOffhand = CharacterWeapon.Empty;
public bool Enabled
{
@ -48,7 +51,7 @@ public class StateListener : IDisposable
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
EquipmentLoading equipmentLoading, StateApplier applier)
StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects)
{
_manager = manager;
_items = items;
@ -63,8 +66,9 @@ public class StateListener : IDisposable
_autoDesignApplier = autoDesignApplier;
_funModule = funModule;
_humans = humans;
_equipmentLoading = equipmentLoading;
_applier = applier;
_movedEquipment = movedEquipment;
_objects = objects;
if (Enabled)
Subscribe();
@ -168,40 +172,36 @@ public class StateListener : IDisposable
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
}
/// <summary>
/// The game object does not actually invoke changes when the model id is identical,
/// so we need to handle that case too.
/// </summary>
private void OnEquipmentLoading(Actor actor, EquipSlot slot, CharacterArmor armor)
private void OnMovedEquipment((EquipSlot, uint, StainId)[] items)
{
if (!actor.Model.Valid || armor != actor.GetArmor(slot))
_objects.Update();
var (identifier, objects) = _objects.PlayerData;
if (!identifier.IsValid || !_manager.TryGetValue(identifier, out var state))
return;
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|| !_manager.TryGetValue(identifier, out var state)
|| !state.BaseData.IsHuman)
return;
if (state.ModelData.Armor(slot) == armor)
return;
var setItem = state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc;
var setStain = state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc;
switch (setItem, setStain)
foreach (var (slot, item, stain) in items)
{
case (true, true):
_manager.ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Manual);
state[slot, false] = StateChanged.Source.Game;
state[slot, true] = StateChanged.Source.Game;
break;
case (true, false):
_manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Manual);
state[slot, false] = StateChanged.Source.Game;
break;
case (false, true):
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Manual);
state[slot, true] = StateChanged.Source.Game;
break;
var currentItem = state.BaseData.Item(slot);
var model = state.ModelData.Weapon(slot);
var current = currentItem.Weapon(state.BaseData.Stain(slot));
if (model.Value == current.Value || !_items.ItemService.AwaitedService.TryGetValue(item, EquipSlot.MainHand, out var changedItem))
continue;
var changed = changedItem.Weapon(stain);
if (current.Value == changed.Value && state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
{
_manager.ChangeItem(state, slot, currentItem, StateChanged.Source.Game);
switch (slot)
{
case EquipSlot.MainHand:
case EquipSlot.OffHand:
_applier.ChangeWeapon(objects, slot, currentItem, stain);
break;
default:
_applier.ChangeArmor(objects, slot, current.ToArmor(), state.ModelData.IsHatVisible());
break;
}
}
}
}
@ -212,6 +212,13 @@ public class StateListener : IDisposable
/// </summary>
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
{
// Fist weapon gauntlet hack.
if (slot is EquipSlot.OffHand && weapon.Value.Variant == 0 && weapon.Value.Set.Value != 0 && _lastFistOffhand.Set.Value != 0)
{
weapon.Value = _lastFistOffhand;
_lastFistOffhand = CharacterWeapon.Empty;
}
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|| !_manager.TryGetValue(identifier, out var state))
return;
@ -229,7 +236,7 @@ public class StateListener : IDisposable
else
apply = true;
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
else
apply = true;
@ -249,6 +256,11 @@ public class StateListener : IDisposable
else if (actorWeapon.Set.Value != 0)
actorWeapon = actorWeapon.With(newWeapon.Stain);
}
// Fist Weapon Offhand hack.
if (slot is EquipSlot.MainHand && weapon.Value.Set.Value is > 1600 and < 1651)
_lastFistOffhand = new CharacterWeapon((SetId)(weapon.Value.Set.Value + 50), weapon.Value.Type, weapon.Value.Variant,
weapon.Value.Stain);
}
/// <summary> Update base data for a single changed equipment slot. </summary>
@ -257,7 +269,7 @@ public class StateListener : IDisposable
var actorArmor = actor.GetArmor(slot);
// The actor armor does not correspond to the model armor, thus the actor is transformed.
// This also prevents it from changing values due to hat state.
if (actorArmor.Value != armor.Value)
if (actorArmor.Value != armor.Value && armor.Set.Value != actor.GetOffhand().Set.Value)
return UpdateState.Transformed;
var baseData = state.BaseData.Armor(slot);
@ -491,7 +503,7 @@ public class StateListener : IDisposable
_penumbra.CreatingCharacterBase += OnCreatingCharacterBase;
_penumbra.CreatedCharacterBase += OnCreatedCharacterBase;
_slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener);
_equipmentLoading.Subscribe(OnEquipmentLoading, EquipmentLoading.Priority.StateListener);
_movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
_headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener);
@ -503,7 +515,7 @@ public class StateListener : IDisposable
_penumbra.CreatingCharacterBase -= OnCreatingCharacterBase;
_penumbra.CreatedCharacterBase -= OnCreatedCharacterBase;
_slotUpdating.Unsubscribe(OnSlotUpdating);
_equipmentLoading.Unsubscribe(OnEquipmentLoading);
_movedEquipment.Unsubscribe(OnMovedEquipment);
_weaponLoading.Unsubscribe(OnWeaponLoading);
_visorState.Unsubscribe(OnVisorChange);
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);
@ -515,7 +527,7 @@ public class StateListener : IDisposable
if (_creatingState == null)
return;
_applier.ChangeHatState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsHatVisible());
_applier.ChangeHatState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsHatVisible());
_applier.ChangeWeaponState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWeaponVisible());
}
}

View file

@ -169,6 +169,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
main = actor.GetMainhand();
off = actor.GetOffhand();
FistWeaponHack(ref ret, ref main, ref off);
ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
}
@ -190,6 +191,19 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
return ret;
}
/// <summary> This is hardcoded in the game. </summary>
private void FistWeaponHack(ref DesignData ret, ref CharacterWeapon mainhand, ref CharacterWeapon offhand)
{
if (mainhand.Set.Value is < 1601 or >= 1651)
return;
var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Set, 0, (byte)offhand.Variant);
offhand.Set = (SetId)(mainhand.Set.Value + 50);
offhand.Variant = mainhand.Variant;
offhand.Type = mainhand.Type;
ret.SetItem(EquipSlot.Hands, gauntlets);
}
#region Change Values
/// <summary> Turn an actor human. </summary>
@ -433,7 +447,8 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
if (!GetOrCreate(actor, out var state))
return;
ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false);
ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(),
false);
}
public void DeleteState(ActorIdentifier identifier)

View file

@ -107,7 +107,7 @@ public class ItemUnlockManager : ISavable, IDisposable
private bool AddItem(uint itemId, long time)
{
itemId = HandleHq(itemId);
if (!_items.ItemService.AwaitedService.TryGetValue(itemId, out var equip) || !_unlocked.TryAdd(equip.ItemId, time))
if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var equip) || !_unlocked.TryAdd(equip.ItemId, time))
return false;
_event.Invoke(ObjectUnlocked.Type.Item, equip.ItemId, DateTimeOffset.FromUnixTimeMilliseconds(time));
@ -278,7 +278,7 @@ public class ItemUnlockManager : ISavable, IDisposable
private void Load()
{
var version = UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked,
id => _items.ItemService.AwaitedService.TryGetValue(id, out _), "item");
id => _items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out _), "item");
UpdateModels(version);
}
@ -291,7 +291,7 @@ public class ItemUnlockManager : ISavable, IDisposable
var cabinet = gameData.GetExcelSheet<Cabinet>()!;
foreach (var row in cabinet)
{
if (items.ItemService.AwaitedService.TryGetValue(row.Item.Row, out var item))
if (items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item))
ret.TryAdd(item.ItemId, new UnlockRequirements(row.RowId, 0, 0, 0, UnlockType.Cabinet));
}
@ -299,7 +299,7 @@ public class ItemUnlockManager : ISavable, IDisposable
var gilShop = gameData.GetExcelSheet<GilShop>()!;
foreach (var row in gilShopItem)
{
if (!items.ItemService.AwaitedService.TryGetValue(row.Item.Row, out var item))
if (!items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item))
continue;
var quest1 = row.QuestRequired[0].Row;
@ -332,7 +332,7 @@ public class ItemUnlockManager : ISavable, IDisposable
foreach (var (item, time) in _unlocked.ToArray())
{
if (!_items.ItemService.AwaitedService.TryGetValue(item, out var equip))
if (!_items.ItemService.AwaitedService.TryGetValue(item, EquipSlot.MainHand, out var equip))
continue;
var ident = _identifier.AwaitedService.Identify(equip.ModelId, equip.WeaponType, equip.Variant, equip.Type.ToSlot());