-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTooltip.cpp
More file actions
309 lines (280 loc) · 13.4 KB
/
Tooltip.cpp
File metadata and controls
309 lines (280 loc) · 13.4 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
// 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>
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;
}
static const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetItemByID", &Script_GameTooltipSetItemByID},
{"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