-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathTooltip.cpp
More file actions
384 lines (351 loc) · 16.7 KB
/
Tooltip.cpp
File metadata and controls
384 lines (351 loc) · 16.7 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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// 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/Data.h"
#include "item/ID.h"
#include "item/Link.h"
#include "item/Location.h"
#include "item/QualityColor.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
namespace Item::Tooltip {
// `GameTooltip:SetItemByID(itemID)` — modern method that renders an
// item tooltip from just an itemID. The 1.12 workaround was to
// construct an item hyperlink and call `SetHyperlink` —
// `tooltip:SetHyperlink("item:" .. id .. ":0:0:0:0:0:0:0")` — which
// works but forces every caller to know the hyperlink format.
//
// Implementation: format the hyperlink string in C and dispatch to the
// existing `Script_GameTooltip_SetHyperlink` (registry slot 12 at
// `0x00531FD0`). Same registration pattern as `SetSpellByID` in
// [src/spell/Tooltip.cpp](src/spell/Tooltip.cpp).
static int __fastcall Script_GameTooltipSetItemByID(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GameTooltip:SetItemByID(itemID)");
return 0;
}
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: GameTooltip:SetItemByID(itemID)");
return 0;
}
const int itemID = static_cast<int>(Game::Lua::ToNumber(L, 2));
if (itemID <= 0)
return 0;
// Warm the cache if uncached. The cache-load callback fires
// `GET_ITEM_INFO_RECEIVED` when data arrives. Addons that want
// the tooltip to auto-refresh in place should listen for that
// event and re-call SetItemByID — modern WoW (5.x+) does the
// same internally; we don't replicate that engine-level hook
// because the only viable C-side path here is invoking Lua
// from a network callback, which has its own pitfalls.
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
char hyperlink[64];
std::snprintf(hyperlink, sizeof(hyperlink), "item:%d:0:0:0:0:0:0:0", itemID);
// Replace stack[2] (the itemID) with the formatted hyperlink, so
// the existing SetHyperlink sees `(self, "item:...")`.
Game::Lua::SetTop(L, 1); // keep self at stack[1]
Game::Lua::PushString(L, hyperlink); // stack[2] = hyperlink
using Script_t = int(__fastcall *)(void *L);
auto fn = reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_HYPERLINK);
return fn(L);
}
// `GameTooltip:SetInventoryItemByID(itemID)` — modern method that
// renders the tooltip for the **equipped instance** of `itemID`,
// including enchants, random suffix stats, and broken/locked state.
// Distinct from `SetItemByID`, which renders the base ItemSparse
// data (clean, no enchants).
//
// Verified empirically: with run-speed-enchanted boots equipped,
// `SetInventoryItemByID(<bootsID>)` shows the enchant line in
// addition to the base stats; `SetItemByID(<bootsID>)` shows the
// boots without enchants.
//
// Implementation: walk character-pane slots 1..19 looking for an
// equipped item matching `itemID`; on hit, dispatch to the
// engine's existing `Script_GameTooltip_SetInventoryItem`
// (registry slot 19, `0x00532EE0`) with `("player", slot)`. The
// engine's function reads the actual CGItem instance (with
// descriptor flags + applied enchants) for the tooltip.
//
// Silent no-op if the item isn't equipped — caller should fall
// back to `SetItemByID` for unworn items.
static int __fastcall Script_GameTooltipSetInventoryItemByID(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GameTooltip:SetInventoryItemByID(itemID)");
return 0;
}
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: GameTooltip:SetInventoryItemByID(itemID)");
return 0;
}
const int itemID = static_cast<int>(Game::Lua::ToNumber(L, 2));
if (itemID <= 0)
return 0;
// Walk in ascending slot order (1..19). When the player has
// duplicates of the same itemID equipped (matched MH/OH weapons,
// identical rings, identical trinkets), modern client behavior
// is to render the lower-numbered slot — MAINHAND (16) before
// OFFHAND (17), FINGER1 (11) before FINGER2 (12), TRINKET1 (13)
// before TRINKET2 (14). Verified empirically against the modern
// client; our ascending walk + first-match-break naturally
// matches.
int foundSlot = 0;
for (int slot = Offsets::EQUIPMENT_SLOT_FIRST;
slot <= Offsets::EQUIPMENT_SLOT_LAST; ++slot) {
const uint8_t *item = Item::Location::ResolveEquipmentSlot(slot);
if (item == nullptr)
continue;
if (Item::ID::FromCGItem(item) == itemID) {
foundSlot = slot;
break;
}
}
if (foundSlot == 0)
return 0;
// Replace the itemID arg with ("player", slot) so the engine's
// SetInventoryItem dispatcher reads its expected (unit, slot).
Game::Lua::SetTop(L, 1); // keep self at stack[1]
Game::Lua::PushString(L, "player");
Game::Lua::PushNumber(L, static_cast<double>(foundSlot));
using Script_t = int(__fastcall *)(void *L);
auto fn = reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_INVENTORY_ITEM);
return fn(L);
}
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid, void *callback,
void *userData, int unused);
static 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);
}
using ResolveObjectByGuid_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
// Resolves a stored item GUID into a CGItem via the engine's own
// resolver. Same path Item::Count uses for direct bank reads — no
// gating, no inventory walk, works for any object the engine has
// loaded (player bags, equipment, merchant items, loot, trade,
// mailbox, etc.).
static void *ResolveItemByGuid(uint32_t guidLo, uint32_t guidHi) {
if (guidLo == 0 && guidHi == 0)
return nullptr;
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
return fn(Offsets::OBJ_TYPE_ITEM, "GameTooltip:GetItem", guidLo, guidHi, 0x172);
}
// `GameTooltip:GetItem()` → (name, link, itemID) for whichever item
// the tooltip is currently displaying. The third return is a
// non-modern addition — modern WoW only returns (name, link) and
// expects addons to parse the itemID out of the link. Returning it
// directly saves callers a gsub round.
//
// BuildItemTooltip stores two fields per Set* call:
// - tooltip+0x398 → itemID (always populated for any item path)
// - tooltip+0x380/+0x384 → item GUID (populated only when there's a
// real CGItem — SetBagItem,
// SetInventoryItem, SetLootItem,
// SetMerchantItem, etc. Zero for
// SetItemByID / SetHyperlink
// which have no instance.)
// Both are zeroed by the per-tooltip Clear at FUN_00530050.
//
// Two paths for the link:
// - GUID non-zero → resolve to CGItem and dispatch to the engine's
// own link builder at FUN_0052AE00. This produces the full dressed
// link with enchant ID, random suffix factor, unique ID, and
// random-suffix-decorated name. Same output as
// GetInventoryItemLink / GetContainerItemLink for that item.
// - GUID zero → SetItemByID-style tooltip; we have no per-instance
// data, so build a basic colored link from the cached itemID and
// quality (`|cff..|Hitem:N:0:0:0:0:0:0:0|h[Name]|h|r`).
//
// Returns nothing for: non-item tooltip (itemID == 0), uncached
// itemID on the no-GUID path (fires a background cache warmup), or
// empty name.
static int __fastcall Script_GameTooltipGetItem(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GameTooltip:GetItem()");
return 0;
}
void *tooltipObj = Game::Lua::ResolveObject(L, 1);
if (tooltipObj == nullptr)
return 0;
auto *base = static_cast<const uint8_t *>(tooltipObj);
const int itemID = *reinterpret_cast<const int *>(base + Offsets::OFF_TOOLTIP_ITEM_ID);
if (itemID <= 0)
return 0;
const uint32_t guidLo = *reinterpret_cast<const uint32_t *>(
base + Offsets::OFF_TOOLTIP_ITEM_GUID_LO);
const uint32_t guidHi = *reinterpret_cast<const uint32_t *>(
base + Offsets::OFF_TOOLTIP_ITEM_GUID_HI);
if (guidLo != 0 || guidHi != 0) {
if (void *cgItem = ResolveItemByGuid(guidLo, guidHi)) {
const char *link = Item::Link::FromCGItem(
static_cast<const uint8_t *>(cgItem));
if (link != nullptr && *link != '\0') {
// Engine's link builder also writes the dressed
// (random-suffixed) name into the link's `[Name]`
// slot; pull it out by reading the cached base name
// for the return value. The engine doesn't expose
// the dressed name as a separate string, so we
// return the base name — matches modern semantics
// where (name, link) name is the cached display name
// and the link is the full hyperlink.
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record != nullptr) {
const char *name = *reinterpret_cast<const char *const *>(
record + Offsets::OFF_ITEMSTATS_NAME);
if (name != nullptr && *name != '\0') {
Game::Lua::PushString(L, name);
Game::Lua::PushString(L, link);
Game::Lua::PushNumber(L, static_cast<double>(itemID));
return 3;
}
}
}
}
// Fall through to the basic-link path if GUID resolve or link
// build failed for any reason — better to return *something*
// than nothing when we have an itemID.
}
// No-GUID path (SetItemByID, SetHyperlink for an item:N link with
// no per-instance data): build the basic colored link from cached
// itemID + quality + name.
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record == nullptr) {
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
return 0;
}
const char *name = *reinterpret_cast<const char *const *>(
record + Offsets::OFF_ITEMSTATS_NAME);
if (name == nullptr || *name == '\0')
return 0;
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_QUALITY);
char link[256];
std::snprintf(link, sizeof(link),
"%s|Hitem:%d:0:0:0:0:0:0:0|h[%s]|h|r",
Item::QualityColor::Prefix(static_cast<int>(quality)),
itemID, name);
Game::Lua::PushString(L, name);
Game::Lua::PushString(L, link);
Game::Lua::PushNumber(L, static_cast<double>(itemID));
return 3;
}
// `GameTooltip:HasItem()` — boolean companion to `GetItem`. Returns
// true iff the tooltip is currently showing an item (any path:
// SetBagItem, SetHyperlink, SetItemByID, SetMerchantItem, etc.). Same
// `[tooltip + OFF_TOOLTIP_ITEM_ID]` check `GetItem` does — that field
// is non-zero only while an item tooltip is live.
static int __fastcall Script_GameTooltipHasItem(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GameTooltip:HasItem()");
return 0;
}
void *tooltipObj = Game::Lua::ResolveObject(L, 1);
if (tooltipObj == nullptr) {
Game::Lua::PushBool(L, 0);
return 1;
}
const int itemID = *reinterpret_cast<const int *>(
static_cast<const uint8_t *>(tooltipObj) + Offsets::OFF_TOOLTIP_ITEM_ID);
Game::Lua::PushBoolean(L, itemID > 0);
return 1;
}
// `GameTooltip:SetItemByGUID(guidString)` — modern method that renders
// the tooltip for the specific item instance identified by GUID
// (`"0xHHHHHHHHLLLLLLLL"` per `C_Item.GetItemGUID`). Distinct from
// `SetItemByID`, which shows the base `ItemSparse` data with no
// per-instance state: this path uses the live CGItem so the tooltip
// includes enchant lines, random-suffix-decorated name + bonuses,
// locked/broken state, etc.
//
// Implementation: parse the GUID string, resolve via the engine's
// `FUN_OBJECT_RESOLVE_BY_GUID` (same path `C_Item.GetItemLocation`
// and `GameTooltip:GetItem` use), build the fully-decorated
// hyperlink via `Item::Link::FromCGItem` (engine helper at
// `FUN_GAMETOOLTIP_BUILD_ITEM_LINK`), then dispatch to the engine's
// existing `Script_GameTooltip_SetHyperlink`. SetHyperlink parses
// the link's `item:N:enchant:gem:gem:gem:gem:suffix:unique` payload
// back into instance-specific tooltip data — closing the loop
// without us needing a separate engine entry point for "tooltip
// from CGItem*".
//
// Silent no-op when the GUID is malformed, not loaded in the
// client, or doesn't resolve to an item (creature/object GUIDs go
// to a different resolver).
static int __fastcall Script_GameTooltipSetItemByGUID(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GameTooltip:SetItemByGUID(itemGUID)");
return 0;
}
if (Game::Lua::Type(L, 2) != Game::Lua::TYPE_STRING) {
Game::Lua::Error(L, "Usage: GameTooltip:SetItemByGUID(itemGUID)");
return 0;
}
const char *guidStr = Game::Lua::ToString(L, 2);
uint64_t guid = 0;
if (!Item::Location::ParseGUIDString(guidStr, &guid))
return 0;
const uint8_t *cgItem = Item::Location::ResolveByGUID(guid);
if (cgItem == nullptr)
return 0;
const char *link = Item::Link::FromCGItem(cgItem);
if (link == nullptr || *link == '\0')
return 0;
// `Item::Link::FromCGItem` returns the full
// `|cffXXXXXXXX|Hitem:N:E:S:S:S:S:R:U|h[Name]|h|r` decorated link
// (the public-link form addons paste into chat). `SetHyperlink`
// wants just the bare `item:N:E:S:S:S:S:R:U` payload — passing the
// decorated form trips its "Unknown link type" Lua error because
// the parser sees `|c` first and gives up. Extract the payload by
// finding `|Hitem:` and copying through to the closing `|h`.
const char *open = std::strstr(link, "|Hitem:");
if (open == nullptr)
return 0;
open += 2; // skip past `|H` so we start at `item:`
const char *close = std::strstr(open, "|h");
if (close == nullptr)
return 0;
const size_t payloadLen = static_cast<size_t>(close - open);
char payload[128];
if (payloadLen + 1 > sizeof(payload))
return 0;
std::memcpy(payload, open, payloadLen);
payload[payloadLen] = '\0';
// Replace stack[2] (the GUID) with the bare item link and
// dispatch to SetHyperlink with (self, "item:...").
Game::Lua::SetTop(L, 1);
Game::Lua::PushString(L, payload);
using Script_t = int(__fastcall *)(void *L);
auto fn = reinterpret_cast<Script_t>(Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_HYPERLINK);
return fn(L);
}
static const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetItemByID", &Script_GameTooltipSetItemByID},
{"SetItemByGUID", &Script_GameTooltipSetItemByGUID},
{"SetInventoryItemByID", &Script_GameTooltipSetInventoryItemByID},
{"GetItem", &Script_GameTooltipGetItem},
{"HasItem", &Script_GameTooltipHasItem},
};
static void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Item::Tooltip