mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 15:40:37 +00:00
Introduce Item::TooltipItem::CurrentID -- the single "what item is this tooltip showing" resolver. It trusts the link-path itemID (+0x398), and otherwise resolves the CGItem GUID (+0x380) but only when no unit / gameobject / spell id is set, since the clear does not zero the item GUID and it goes stale after the tooltip moves on. - GameTooltip:GetItem / HasItem now route through CurrentID: fixes GetItem returning nil for CGItem tooltips (bag / equipped / merchant / loot) and HasItem false-positiving on a unit tooltip after an item hover. - Add GameTooltip:IsEquippedItem() (3.3.5 backport, registry index 63), resolving the displayed item via CurrentID + the equipment-slot walk. - SetHyperlinkCompareItem: the comparisonTooltip (4th arg) fallback uses CurrentID; stat deltas now show by default and hide only for an explicit false / 0 (was: show only for a truthy shift). - GetItem round-trips the random-suffix id in the link for auction/compare items (no CGItem instance) via the compare descriptor. - FrameXML GameTooltip_ShowCompareItem falls back to self.compareLink, so a caller can hand it an explicit link when the tooltip's item state was rebuilt out from under it (e.g. an AH addon appending source lines). - Offsets: compare-descriptor + FontString offsets; drop the duplicate tooltip item-id/guid offsets in favor of the canonical OFF_TOOLTIP_*.
394 lines
17 KiB
C++
394 lines
17 KiB
C++
// This file is part of ClassicAPI.
|
|
//
|
|
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
|
// of the GNU 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 General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU 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 "item/TooltipItem.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 the displayed item on the tooltip per Set*
|
|
// call — but only ONE of two fields depending on the path:
|
|
// - tooltip+0x398 → itemID (link paths with no instance: SetItemByID,
|
|
// SetHyperlink)
|
|
// - tooltip+0x380/+0x384 → item GUID (CGItem paths: SetBagItem,
|
|
// SetInventoryItem, SetLootItem,
|
|
// SetMerchantItem, …) — these leave the itemID
|
|
// field 0, so when +0x398 is 0 we recover the
|
|
// itemID from the GUID's live CGItem.
|
|
// 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);
|
|
|
|
// Resolve the currently-displayed item (link-path itemID, or the
|
|
// CGItem-path GUID when no unit/GO/spell is shown) — see
|
|
// Item::TooltipItem::CurrentID for the stale-GUID guarding.
|
|
uint64_t itemGuid = 0;
|
|
const int itemID = Item::TooltipItem::CurrentID(base, &itemGuid);
|
|
if (itemID <= 0)
|
|
return 0;
|
|
const uint32_t guidLo = static_cast<uint32_t>(itemGuid);
|
|
const uint32_t guidHi = static_cast<uint32_t>(itemGuid >> 32);
|
|
|
|
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, SetAuctionItem — items with
|
|
// no CGItem instance): build the colored link from cached itemID +
|
|
// quality + name. Auction/compare items have no GUID but do carry a
|
|
// random-property (suffix) id in the compare descriptor at
|
|
// OFF_TOOLTIP_COMPARE_SUFFIX — include it (in the link's suffix field)
|
|
// when the compare-descriptor flag is set, so the link round-trips the
|
|
// suffix for stat comparison. Matches 3.3.5's auction GetItem.
|
|
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);
|
|
|
|
int suffix = 0;
|
|
if (*reinterpret_cast<const int *>(base + Offsets::OFF_TOOLTIP_COMPARE_FLAG) != 0)
|
|
suffix = *reinterpret_cast<const int *>(base + Offsets::OFF_TOOLTIP_COMPARE_SUFFIX);
|
|
|
|
char link[256];
|
|
std::snprintf(link, sizeof(link),
|
|
"%s|Hitem:%d:0:%d:0:0:0:0:0|h[%s]|h|r",
|
|
Item::QualityColor::Prefix(static_cast<int>(quality)),
|
|
itemID, suffix, 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;
|
|
}
|
|
Game::Lua::PushBoolean(L, Item::TooltipItem::CurrentID(tooltipObj, nullptr) > 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
|