|
| 1 | +// This file is part of ClassicAPI. |
| 2 | +// |
| 3 | +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms |
| 4 | +// of the GNU General Public License as published by the Free Software Foundation, either |
| 5 | +// version 3 of the License, or (at your option) any later version. |
| 6 | +// |
| 7 | +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY |
| 8 | +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR |
| 9 | +// PURPOSE. See the GNU General Public License for more details. |
| 10 | +// |
| 11 | +// You should have received a copy of the GNU General Public License along with |
| 12 | +// ClassicAPI. If not, see <https://www.gnu.org/licenses/>. |
| 13 | + |
| 14 | +// `PLAYER_EQUIPMENT_CHANGED(equipmentSlot, hasCurrent)` — the WotLK-era |
| 15 | +// per-slot paperdoll event. Vanilla only fires UNIT_INVENTORY_CHANGED for |
| 16 | +// the player, which doesn't say WHICH slot changed; gear trackers and stat |
| 17 | +// sheets built against 3.3+ gate their refresh on this event. |
| 18 | +// |
| 19 | +// Fully event-driven via the engine's own descriptor-field observer system |
| 20 | +// (see the FUN_DESC_OBSERVER_REGISTER block in Offsets.h). The engine's |
| 21 | +// inventory setup (FUN_INV_OBSERVER_SETUP, once per enter-world) registers |
| 22 | +// change observers for the bag/backpack/bank/keyring GUID fields of the |
| 23 | +// player descriptor — but never for the 19 equipment slots (0x4A8..0x538). |
| 24 | +// We co-hook that setup and register our own observer per equipment field, |
| 25 | +// mirroring the engine's registration shape exactly (bank 4, size 8). The |
| 26 | +// dispatcher invokes us only when a slot's item GUID actually changed |
| 27 | +// (per-node mirror memcmp), so there's no polling and no diff bookkeeping |
| 28 | +// on our side, and the nodes share the engine observers' lifetime (they |
| 29 | +// die with the player object; the setup re-runs each enter-world). |
| 30 | +// |
| 31 | +// `equipmentSlot` is the 1-based Lua inventory slot (1 = head … 19 = |
| 32 | +// tabard), matching modern. `hasCurrent` is 1 when the slot now holds an |
| 33 | +// item and nil when it's empty (the engine dispatcher has no `%b`; 1/nil |
| 34 | +// keeps the idiomatic `if hasCurrent then` working — see FireIdSuccess). |
| 35 | + |
| 36 | +#include "Game.h" |
| 37 | +#include "Offsets.h" |
| 38 | +#include "event/Custom.h" |
| 39 | +#include "unit/Identity.h" |
| 40 | + |
| 41 | +#include <cstdint> |
| 42 | + |
| 43 | +namespace Player::Equipment { |
| 44 | + |
| 45 | +namespace { |
| 46 | + |
| 47 | +constexpr const char *kEvtEquipmentChanged = "PLAYER_EQUIPMENT_CHANGED"; |
| 48 | +const Event::Custom::AutoReserve _r{kEvtEquipmentChanged}; |
| 49 | + |
| 50 | +using ResolveUnitToken_t = void *(__fastcall *)(const char *token); |
| 51 | + |
| 52 | +// Whether the slot currently holds an item — read from the player's |
| 53 | +// inventory-manager GUID array, the same source the engine's own bag |
| 54 | +// observer (FUN_004F8DB0) treats as the post-change truth when its field |
| 55 | +// observer fires. |
| 56 | +bool SlotHasItem(int slot0Based) { |
| 57 | + auto resolve = |
| 58 | + reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN); |
| 59 | + auto *player = static_cast<const uint8_t *>(resolve("player")); |
| 60 | + if (player == nullptr) |
| 61 | + return false; |
| 62 | + const uint8_t *invMgr = player + Offsets::OFF_PLAYER_INVENTORY_MANAGER; |
| 63 | + const uint32_t count = *reinterpret_cast<const uint32_t *>( |
| 64 | + invMgr + Offsets::OFF_INVMGR_SLOT_COUNT); |
| 65 | + if (static_cast<uint32_t>(slot0Based) >= count) |
| 66 | + return false; |
| 67 | + const uint64_t *guids = *reinterpret_cast<const uint64_t *const *>( |
| 68 | + invMgr + Offsets::OFF_INVMGR_GUID_ARRAY); |
| 69 | + return guids != nullptr && guids[slot0Based] != 0; |
| 70 | +} |
| 71 | + |
| 72 | +// Descriptor-field observer callback — ABI per the Offsets.h note: |
| 73 | +// __fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi, oldValue*, |
| 74 | +// userArg), return 1, callee cleans 0x10. Invoked by the dispatcher only |
| 75 | +// when the watched equipment GUID field actually changed. |
| 76 | +int __fastcall EquipSlotChanged_cb(uint32_t fieldOffset, uint32_t /*size*/, |
| 77 | + uint32_t /*guidLo*/, uint32_t /*guidHi*/, |
| 78 | + const uint32_t * /*oldValue*/, |
| 79 | + void * /*userArg*/) { |
| 80 | + const int slot0 = |
| 81 | + static_cast<int>(fieldOffset - Offsets::OFF_DESC_PLAYER_EQUIP_FIRST) >> 3; |
| 82 | + if (slot0 >= 0 && slot0 < Offsets::DESC_PLAYER_EQUIP_SLOTS) { |
| 83 | + Event::Custom::FireIdSuccess(Event::Custom::Lookup(kEvtEquipmentChanged), |
| 84 | + slot0 + 1, SlotHasItem(slot0)); |
| 85 | + } |
| 86 | + return 1; |
| 87 | +} |
| 88 | + |
| 89 | +// Co-hook on the engine's inventory observer setup: after it registers the |
| 90 | +// bag-range observers, register ours for the equipment range it skips. |
| 91 | +// Same registrar, same shape — only the field offsets differ. |
| 92 | +using ObserverSetup_t = void(__fastcall *)(); |
| 93 | +ObserverSetup_t g_origSetup = nullptr; |
| 94 | + |
| 95 | +using RegisterObserver_t = void(__fastcall *)( |
| 96 | + int bank, uint32_t fieldOffset, uint32_t guidLo, uint32_t guidHi, int size, |
| 97 | + const void *callback, void *userArg1, void *userArg2); |
| 98 | + |
| 99 | +void __fastcall ObserverSetup_h() { |
| 100 | + g_origSetup(); |
| 101 | + const uint64_t guid = Unit::Identity::PlayerGuid(); |
| 102 | + if (guid == 0) |
| 103 | + return; |
| 104 | + auto reg = reinterpret_cast<RegisterObserver_t>( |
| 105 | + Offsets::FUN_DESC_OBSERVER_REGISTER); |
| 106 | + for (int slot = 0; slot < Offsets::DESC_PLAYER_EQUIP_SLOTS; ++slot) { |
| 107 | + reg(Offsets::DESC_OBSERVER_BANK_PLAYER, |
| 108 | + Offsets::OFF_DESC_PLAYER_EQUIP_FIRST + slot * 8, |
| 109 | + static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), |
| 110 | + /*size*/ 8, reinterpret_cast<const void *>(&EquipSlotChanged_cb), |
| 111 | + nullptr, nullptr); |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +static const Game::HookAutoRegister _setupHook{ |
| 116 | + Offsets::FUN_INV_OBSERVER_SETUP, |
| 117 | + reinterpret_cast<void *>(&ObserverSetup_h), |
| 118 | + reinterpret_cast<void **>(&g_origSetup)}; |
| 119 | + |
| 120 | +} // namespace |
| 121 | + |
| 122 | +} // namespace Player::Equipment |
0 commit comments