-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathEquipment.cpp
More file actions
261 lines (232 loc) · 10.6 KB
/
Equipment.cpp
File metadata and controls
261 lines (232 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include "Offsets.h"
#include "item/Arg.h"
#include "item/ID.h"
#include "item/Location.h"
#include "item/Swap.h"
#include <cstdint>
#include <string.h>
namespace Item::Equipment {
namespace {
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid, void *callback,
void *userData, int unused);
// Same item-cache peek pattern as `Item::Bag::PeekItemRecord` — we
// don't share because each caller is one-line-different and the
// helper isn't worth a header.
const uint8_t *PeekItemRecord(uint32_t itemID) {
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
const uint64_t zeroGuid = 0;
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
}
// `OffhandHasWeapon()` — true iff the player has a one-handed
// weapon (or off-hand-only weapon) equipped in the off-hand slot.
// Returns false for empty off-hand, shields, and held items
// (tomes, orbs, librams) — even though the latter two share the
// off-hand slot.
//
// Implementation: resolves slot 17 → CGItem → itemID → cached
// ItemStats record → m_inventoryType. Falls back to false on any
// missing link in that chain (no item, item data not cached yet,
// etc.). Modern API behavior is the same — no async load fired,
// no event emitted; if the off-hand item isn't cached the function
// just returns false until something else warms the cache.
int __fastcall Script_OffhandHasWeapon(void *L) {
auto *item = Item::Location::ResolveEquipmentSlot(Offsets::INVSLOT_OFFHAND);
if (item == nullptr) {
Game::Lua::PushBool(L, 0);
return 1;
}
const int itemID = Item::ID::FromCGItem(item);
if (itemID == 0) {
Game::Lua::PushBoolean(L, 0);
return 1;
}
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record == nullptr) {
Game::Lua::PushBoolean(L, 0);
return 1;
}
const uint32_t invType = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
const bool isWeapon = (invType == Offsets::INVTYPE_WEAPON ||
invType == Offsets::INVTYPE_WEAPONOFFHAND);
Game::Lua::PushBoolean(L, static_cast<int>(isWeapon));
return 1;
}
// `C_Item.IsEquippableItem(item)` — true iff `item` can be equipped
// in *some* character-pane slot. Reads `m_inventoryType` off the
// cached ItemStats record and treats any non-zero value as
// equippable (INVTYPE_NON_EQUIP = 0, everything else 1..23 fits some
// slot). Modern signature accepts itemID number, `"item:N..."` link,
// or item name; we delegate input resolution to `Item::Arg::Resolve`.
//
// Cache-miss handling: returns false without firing a load (sync
// call, matches modern semantics). Callers that want auto-warm
// behavior should run `C_Item.RequestLoadItemDataByID` first and
// re-check on `ITEM_DATA_LOAD_RESULT`.
//
// Name-only inputs aren't supported because vanilla has no item-name
// → itemID resolver (`Item::Arg::Resolve` returns just a name string,
// no itemID, and we'd need to scan the entire ItemStats cache to
// match a name back — slow, and the engine doesn't ship that map).
// Modern WoW only resolves names for items currently in inventory;
// `C_Item.IsEquippedItem` covers that exact niche already.
int __fastcall Script_C_Item_IsEquippableItem(void *L) {
const int itemID = Item::Arg::ResolveItemID(L, 1);
if (itemID <= 0) {
Game::Lua::PushBoolean(L, 0);
return 1;
}
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record == nullptr) {
Game::Lua::PushBoolean(L, 0);
return 1;
}
const uint32_t invType = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
Game::Lua::PushBoolean(L, invType > 0);
return 1;
}
// `C_Item.IsEquippedItem(item)` — true iff any character-pane equipment
// slot (1..19) currently holds an item matching `item`. Modern API
// accepts:
// - itemID number → exact match against the equipped slot's itemID
// - "item:N..." string → parse the link's first numeric field as the
// itemID and match the same way
// - plain string → case-insensitive name match against the cached
// `m_name[0]` of each equipped item
//
// Returns false (never errors) for invalid input, an empty inventory,
// or an uncached match candidate. Walks slots in order and short-
// circuits on the first hit.
int __fastcall Script_C_Item_IsEquippedItem(void *L) {
const auto arg = Item::Arg::Resolve(L, 1);
if (arg.itemID <= 0 && arg.name == nullptr) {
Game::Lua::PushBoolean(L, 0);
return 1;
}
for (int slot = Offsets::EQUIPMENT_SLOT_FIRST; slot <= Offsets::EQUIPMENT_SLOT_LAST; ++slot) {
auto *item = Item::Location::ResolveEquipmentSlot(slot);
if (item == nullptr) continue;
const int id = Item::ID::FromCGItem(item);
if (id == 0) continue;
if (arg.itemID > 0) {
if (id == arg.itemID) {
Game::Lua::PushBoolean(L, 1);
return 1;
}
continue;
}
// Name-match path — peek the item-cache record and compare
// `m_name[0]`. Uncached items short-circuit to "no match"
// rather than firing a load; matches modern API semantics
// (sync call, possibly stale, never blocks).
auto *record = PeekItemRecord(static_cast<uint32_t>(id));
if (record == nullptr) continue;
const char *name = *reinterpret_cast<const char *const *>(
record + Offsets::OFF_ITEMSTATS_NAME);
if (name == nullptr) continue;
if (_stricmp(name, arg.name) == 0) {
Game::Lua::PushBoolean(L, 1);
return 1;
}
}
Game::Lua::PushBoolean(L, 0);
return 1;
}
// `C_Item.EquipItemByName(itemInfo [, dstSlot])` — finds the first
// item in the player's bags matching `itemInfo` (itemID, link, or
// name) and equips it.
//
// Two paths after a shared cursor-clear:
//
// - **Explicit `dstSlot`** (1..19): cursor-free direct swap via
// `Item::Swap::FromBag`, which calls the engine's
// `FUN_INVENTORY_SWAP` primitive. Atomic server-side, no pickup.
//
// - **No `dstSlot`** (engine auto-picks slot from INVTYPE): cursor-
// pickup + `AutoEquipCursorItem`, because 1.12's auto-pick logic
// reads its slot decision off cursor state.
//
// Both paths start with `ClearCursor()` to clear any preexisting
// cursor state. If something was on the cursor, it gets returned
// to its source slot (visual lock cleared, source globals reset)
// before our work runs. Without this, the engine's swap primitive
// ends each call with a cursor-state cleanup that skips the
// item-flag clear (`FUN_00495190(0, 1)`); a held item would stay
// visually locked until a relog refreshed inventory state.
//
// Returns nothing. Silently no-ops on bad/missing input, item not
// found in bags, or engine refusing the swap (combat lockdown,
// item-locked flag, type mismatch).
int __fastcall Script_C_Item_EquipItemByName(void *L) {
const auto arg = Item::Arg::Resolve(L, 1);
if (arg.itemID <= 0 && arg.name == nullptr) {
return 0;
}
const bool hasDstSlot = Game::Lua::IsNumber(L, 2);
const int dstSlot = hasDstSlot ? static_cast<int>(Game::Lua::ToNumber(L, 2)) : 0;
using ScriptFn_t = int(__fastcall *)(void *L);
// Return any held cursor item to its slot BEFORE searching for
// the target. FUN_INVENTORY_SWAP's end-of-call cleanup at
// FUN_00495190(0, 1) clears LOCAL cursor globals but skips the
// visual-lock-flag clear (`item+0x314 & ~1`) — so a held item
// would stay visually locked until a relog refreshed state.
// ClearCursor's FUN_00495190(1, 1) does include that clear.
Game::Lua::SetTop(L, 0);
reinterpret_cast<int(__fastcall *)(void *)>(
Offsets::FUN_SCRIPT_CLEAR_CURSOR)(L);
Item::Location::ByGUIDResult found;
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
return 0;
}
if (hasDstSlot) {
Item::Swap::FromBag(found.item, found.bagID, found.slotIndex, dstSlot);
return 0;
}
// Auto-slot path: pickup the item via Script_PickupContainerItem
// (its state-machine handles spell-cast targeting / repair /
// enchant-scroll / etc., none of which we want to replicate), then
// call the engine's `CGPlayer::AutoEquipCursorItem` helper directly
// — `Script_AutoEquipCursorItem` is a thin wrapper that just
// resolves the local player and calls this with flag=0.
Game::Lua::SetTop(L, 0);
Game::Lua::PushNumber(L, static_cast<double>(found.bagID));
Game::Lua::PushNumber(L, static_cast<double>(found.slotIndex));
auto pickup = reinterpret_cast<ScriptFn_t>(Offsets::FUN_SCRIPT_PICKUP_CONTAINER_ITEM);
pickup(L);
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using AutoEquipCursor_t = void (__thiscall *)(void *player, int flag);
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
Offsets::FUN_RESOLVE_UNIT_TOKEN);
if (auto *player = resolve("player")) {
auto equip = reinterpret_cast<AutoEquipCursor_t>(
Offsets::FUN_AUTO_EQUIP_CURSOR_ITEM);
equip(player, 0);
}
return 0;
}
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("OffhandHasWeapon", &Script_OffhandHasWeapon);
Game::Lua::RegisterTableFunction("C_Item", "IsEquippableItem",
&Script_C_Item_IsEquippableItem);
Game::Lua::RegisterTableFunction("C_Item", "IsEquippedItem", &Script_C_Item_IsEquippedItem);
Game::Lua::RegisterTableFunction("C_Item", "EquipItemByName", &Script_C_Item_EquipItemByName);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Item::Equipment