mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 15:10:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdb515e0c3 | ||
|
|
d55613e88c | ||
|
|
3c0bf3abed | ||
|
|
3179d1741e | ||
|
|
63c003db26 | ||
|
|
d01e51d862 | ||
|
|
92d868424c | ||
|
|
eb4c42b1e0 | ||
|
|
2968f51661 |
@@ -1,3 +1,92 @@
|
||||
function GameTooltip_Hide()
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
GameTooltip.shoppingTooltips = GameTooltip.shoppingTooltips or { ShoppingTooltip1, ShoppingTooltip2 }
|
||||
|
||||
function GameTooltip_ShowCompareItem(self, shift)
|
||||
if ( not self ) then
|
||||
self = GameTooltip;
|
||||
end
|
||||
local item, link = self:GetItem();
|
||||
link = link or self.compareLink
|
||||
if ( not link ) then
|
||||
return;
|
||||
end
|
||||
|
||||
local shoppingTooltip1, shoppingTooltip2 = unpack(self.shoppingTooltips or { ShoppingTooltip1, ShoppingTooltip2 });
|
||||
|
||||
local SEPARATION = 6;
|
||||
local backdrop = shoppingTooltip1.GetBackdrop and shoppingTooltip1:GetBackdrop();
|
||||
local GAP = SEPARATION + ((type(backdrop) == "table" and backdrop.edgeSize) or 0);
|
||||
|
||||
local item1 = nil;
|
||||
local item2 = nil;
|
||||
local side = "left";
|
||||
if ( shoppingTooltip1:SetHyperlinkCompareItem(link, 1, shift, self) ) then
|
||||
item1 = true;
|
||||
end
|
||||
if ( shoppingTooltip2:SetHyperlinkCompareItem(link, 2, shift, self) ) then
|
||||
item2 = true;
|
||||
end
|
||||
|
||||
-- find correct side
|
||||
local rightDist = 0;
|
||||
local leftPos = self:GetLeft();
|
||||
local rightPos = self:GetRight();
|
||||
if ( not rightPos ) then
|
||||
rightPos = 0;
|
||||
end
|
||||
if ( not leftPos ) then
|
||||
leftPos = 0;
|
||||
end
|
||||
|
||||
rightDist = GetScreenWidth() - rightPos;
|
||||
|
||||
if (leftPos and (rightDist < leftPos)) then
|
||||
side = "left";
|
||||
else
|
||||
side = "right";
|
||||
end
|
||||
|
||||
-- see if we should slide the tooltip
|
||||
if ( self:GetAnchorType() and self:GetAnchorType() ~= "ANCHOR_PRESERVE" ) then
|
||||
local totalWidth = 0;
|
||||
if ( item1 ) then
|
||||
totalWidth = totalWidth + shoppingTooltip1:GetWidth();
|
||||
end
|
||||
if ( item2 ) then
|
||||
totalWidth = totalWidth + shoppingTooltip2:GetWidth();
|
||||
end
|
||||
|
||||
if ( (side == "left") and (totalWidth > leftPos) ) then
|
||||
self:SetAnchorType(self:GetAnchorType(), (totalWidth - leftPos), 0);
|
||||
elseif ( (side == "right") and (rightPos + totalWidth) > GetScreenWidth() ) then
|
||||
self:SetAnchorType(self:GetAnchorType(), -((rightPos + totalWidth) - GetScreenWidth()), 0);
|
||||
end
|
||||
end
|
||||
|
||||
if ( item1 ) then
|
||||
shoppingTooltip1:SetOwner(self, "ANCHOR_NONE");
|
||||
shoppingTooltip1:ClearAllPoints();
|
||||
if ( side and side == "left" ) then
|
||||
shoppingTooltip1:SetPoint("TOPRIGHT", self, "TOPLEFT", -GAP, -10);
|
||||
else
|
||||
shoppingTooltip1:SetPoint("TOPLEFT", self, "TOPRIGHT", GAP, -10);
|
||||
end
|
||||
shoppingTooltip1:SetHyperlinkCompareItem(link, 1, shift, self);
|
||||
shoppingTooltip1:Show();
|
||||
|
||||
if ( item2 ) then
|
||||
shoppingTooltip2:SetOwner(shoppingTooltip1, "ANCHOR_NONE");
|
||||
shoppingTooltip2:ClearAllPoints();
|
||||
if ( side and side == "left" ) then
|
||||
shoppingTooltip2:SetPoint("TOPRIGHT", shoppingTooltip1, "TOPLEFT", -GAP, 0);
|
||||
else
|
||||
shoppingTooltip2:SetPoint("TOPLEFT", shoppingTooltip1, "TOPRIGHT", GAP, 0);
|
||||
end
|
||||
shoppingTooltip2:SetHyperlinkCompareItem(link, 2, shift, self);
|
||||
shoppingTooltip2:Show();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ FOCUSTARGET = "Focus Target";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "If you replace this item, the following stat changes will occur:";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
|
||||
@@ -36,7 +36,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem` (script) |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
@@ -165,6 +165,14 @@ native targeting bindings instead of orphaned at the bottom.
|
||||
|
||||
See [Focus / Bindings](docs/API.md#bindings-focustarget--targetfocus) for the implementation note.
|
||||
|
||||
### Behaviors
|
||||
|
||||
Transparent engine tweaks — no API to call, they just fix a vanilla limitation:
|
||||
|
||||
| Tweak | Effect |
|
||||
|-------|--------|
|
||||
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
|
||||
|
||||
## Installation
|
||||
|
||||
Download the prebuilt `ClassicAPI.dll` from the
|
||||
|
||||
@@ -3301,3 +3301,272 @@ the matching read. Both should be straightforward once the field
|
||||
offset is identified — 3.3.5's `Script_*` equivalents are a good
|
||||
starting point for what the parameter shape and bounds-clamp
|
||||
behavior should be.
|
||||
|
||||
## ~~97. Lift GameTooltip's 30-line cap~~ — DONE
|
||||
|
||||
Shipped as [src/tooltip/LinePool.cpp](src/tooltip/LinePool.cpp): pure C++,
|
||||
no addon Lua. Co-hooks the XML-OnLoad region scan (FUN_00529650, vtable
|
||||
slot 9) via `Game::HookAutoRegister`; after the engine sets up the
|
||||
template's 30 lines it creates `TextLeft/Right 31..60` in C++ (alloc →
|
||||
ctor → name → font-copied-from-prev → anchor, offsets scaled pixel →
|
||||
internal like Script_SetPoint) and appends them to the tooltip's three
|
||||
line arrays, bumping `numLinesAllocated`. Runs once per GameTooltip-family
|
||||
frame at creation + per `/reload`, so it auto-covers GameTooltip,
|
||||
ShoppingTooltip1/2, ItemRefTooltip, AtlasLootTooltip, WorldMapTooltip, …
|
||||
Verified in-game: AtlasLoot tooltip rendered a 31st line correctly. Cap is
|
||||
`kMaxLines = 60`. The design notes below record the RE trail; Approach A
|
||||
(hook AddLine) / Approach B (addon Lua) were both explored and dropped in
|
||||
favor of the scan-hook.
|
||||
|
||||
Vanilla 1.12's `GameTooltip:AddLine` silently drops any line past the
|
||||
pool of `TextLeftN`/`TextRightN` FontStrings the XML template
|
||||
pre-declares (30). Modern (3.3.5+) engines grow the pool on demand.
|
||||
Addons that append comparison summaries, item-info footers, or aura
|
||||
details to a full item tooltip get their tail chopped. Practical
|
||||
example — pfUI's eqcompare block (base-stat deltas inline + extended
|
||||
stats appended at the bottom) hits the cap on stat-heavy items where
|
||||
the vendor tooltip already has ~22 lines and the compare block adds
|
||||
another 10; extended-stat rows past line 30 disappear silently.
|
||||
|
||||
Also affects `ShoppingTooltip1`/`ShoppingTooltip2` (same
|
||||
`GameTooltipTemplate`).
|
||||
|
||||
### Root cause (verified in Ghidra on the 1.12 Octo binary)
|
||||
|
||||
`AddLine` internal impl at `FUN_00530270` — the shared body called
|
||||
from both `Script_GameTooltipAddLine` (`FUN_00531630`, method ptr
|
||||
stored in the GameTooltip method table alongside the `"AddLine"`
|
||||
string at `0x00854560`) and `Script_GameTooltipAddDoubleLine`:
|
||||
|
||||
```c
|
||||
void FUN_00530270(int this, char *leftText, char *rightText,
|
||||
colorLeft, colorRight, wrapFlag) {
|
||||
if ((*(int *)(this + 0x31c) != *(int *)(this + 0x320)) && anyHasText) {
|
||||
// set leftText on textLeft[numLines_used],
|
||||
// set rightText on textRight[numLines_used],
|
||||
// wrapFlag[numLines_used] = wrapFlag,
|
||||
// ++numLines_used
|
||||
}
|
||||
// else: silently drop the line
|
||||
}
|
||||
```
|
||||
|
||||
`0x320 == 800` decimal, matches decompile. Pool exhausted ⇒ nothing
|
||||
happens.
|
||||
|
||||
### CGameTooltip struct offsets (1.12)
|
||||
|
||||
Derived from `FUN_00530270` and the pool-init at `FUN_00529650`:
|
||||
|
||||
| Offset | Field |
|
||||
|--------|----------------------------------------------------|
|
||||
| `+0x31c` | `numLines` — current write index (0-based) |
|
||||
| `+0x320` | `numLinesAllocated` — pool size (30 default) |
|
||||
| `+0x328..+0x32c` | `textLeft[]` — array of `CSimpleFontString *`, count `+0x320` |
|
||||
| `+0x334..+0x338` | `textRight[]` — same, symmetric right column |
|
||||
| `+0x340..+0x344` | `wrapFlag[]` — per-line wrap bool |
|
||||
|
||||
Actual layout of the array slots: `+0x328` is the first ptr in an
|
||||
external heap block whose base is `param_1[0xcb]` = `+0x32c`. The
|
||||
init function reallocates all three arrays whenever `numLinesAllocated`
|
||||
would change — see `FUN_00536c80` (the reallocator) and `FUN_004368c0`
|
||||
(a matching alloc for the wrap-flag array of a different element size).
|
||||
|
||||
### The pool-init function
|
||||
|
||||
`FUN_00529650` — walks `%sTextLeftN` / `%sTextRightN` FontString names
|
||||
from the FrameXML-declared pool, counts them, reallocates the three
|
||||
arrays, stores the pointers. Called via vtable slot at `0x00808f84`
|
||||
(only xref) — likely `SetOwner` or an equivalent virtual reset method
|
||||
runs it. It's the natural growth path — if we can force it to see a
|
||||
larger pool of Lua-visible FontStrings and trigger it, the arrays
|
||||
resize themselves.
|
||||
|
||||
### The 3.3.5 reference (already REd)
|
||||
|
||||
3.3.5's `GameTooltip::AddLine` at `0x0061fec0` has the grow-on-demand
|
||||
branch we want to backport. Shape:
|
||||
|
||||
```c
|
||||
if (currentLine == numLinesAllocated - 1) {
|
||||
format(buf, 256, "%sTextLeft%d", tooltipName, currentLine + 1);
|
||||
auto *fs = SimpleFontString::create(this, /*layer*/2, /*subLayer*/1);
|
||||
fs->setName(buf);
|
||||
fs->setFontObject(prevLeft->fontObject);
|
||||
fs->anchor(TOPLEFT, prevLeft, BOTTOMLEFT, 0.0, calculatedYOffset);
|
||||
// ... mirror for TextRight ...
|
||||
// ... append fs to textLeft[]/textRight[] at [numLinesAllocated],
|
||||
// then ++numLinesAllocated.
|
||||
}
|
||||
// fall through to the existing "set text on line[currentLine]" path
|
||||
```
|
||||
|
||||
3.3.5 helpers (offsets 3.3.5-specific — 1.12 equivalents need to be
|
||||
re-derived):
|
||||
- `FUN_00485240` — SimpleFontString constructor `(parent, layer, sublayer)`
|
||||
- `FUN_0048b6c0` — set-name helper (registers under global `_G[name]`)
|
||||
- `FUN_0048a260` — anchor helper `(this, anchorPoint, relativeTo, relativePoint, xoff, yoff)`
|
||||
|
||||
Struct offsets in 3.3.5 were `+0x2a4` (numLines) / `+0x2a8` (numAlloc) /
|
||||
`+0x2b4` (textLeft) / `+0x2c0` (textRight) / `+0x2cc` (wrapFlag). All
|
||||
shifted vs. 1.12 (see above), unsurprising given TBC/WotLK bloat.
|
||||
|
||||
### Implementation plan
|
||||
|
||||
**Approach A — pre-hook AddLine with a grow-if-needed prelude
|
||||
(preferred)**:
|
||||
|
||||
1. Locate the 1.12 equivalents of the three 3.3.5 helpers. Trace them
|
||||
from existing `CSimpleFontString`-creating sites in the engine —
|
||||
the CreateFontString API dispatch is a good starting point (it's
|
||||
Lua-visible and calls the same constructor).
|
||||
2. Locate the array reallocators used by `FUN_00529650`
|
||||
(`FUN_00536c80` for the two pointer arrays, `FUN_004368c0` for the
|
||||
wrap-flag byte array).
|
||||
3. MinHook `FUN_00530270`. Pre-hook body:
|
||||
- If `numLines != numLinesAllocated`, call original — done.
|
||||
- Otherwise: realloc the three arrays to `numLinesAllocated + 1`,
|
||||
create two new `CSimpleFontString` (left + right), name them
|
||||
`"%sTextLeft%d"` / `"%sTextRight%d"` with `tooltip->GetName()` and
|
||||
the new index, anchor each below the prior-slot FontString on the
|
||||
same side, use the prior FontString's `fontObject` (`+0xd8` in the
|
||||
3.3.5 layout — confirm on 1.12) as the default, write the pointers
|
||||
into `textLeft[numLinesAllocated]` / `textRight[numLinesAllocated]`,
|
||||
zero-init `wrapFlag[numLinesAllocated]`, and bump
|
||||
`numLinesAllocated`.
|
||||
- Then tail-call the original — it now succeeds normally.
|
||||
|
||||
**Approach B — pre-declare a wider pool via Lua** (fallback if the
|
||||
constructor helpers turn out to be too tangled):
|
||||
|
||||
1. In `!!!ClassicAPI` addon, at load time, call
|
||||
`GameTooltip:CreateFontString("GameTooltipTextLeft" .. i, "ARTWORK",
|
||||
"GameTooltipText")` for `i = 31..60`, plus `TextRightN`, plus set
|
||||
anchors matching the pattern in the template.
|
||||
2. Force the engine's pool-init to re-scan. If we can't reach the
|
||||
vtable trigger cleanly, expose a small C entrypoint that walks the
|
||||
pool the same way `FUN_00529650` does — takes `tooltip` as arg,
|
||||
reallocates arrays, refills.
|
||||
|
||||
Approach B is less code but ties the cap to a fixed number (60 lines
|
||||
is enough for pfUI's use case but arbitrary). Approach A grows on
|
||||
demand like retail and has no ceiling.
|
||||
|
||||
### Testing
|
||||
|
||||
- Hover a random-suffix weapon with several equip-spell bonuses (Krol
|
||||
Blade, Ironfoe, any ZG/AQ trinket) — tooltip should render >30 lines
|
||||
without truncation.
|
||||
- Hover an item that produces a 40+ line tooltip when pfUI's eqcompare
|
||||
block is enabled (Priest Judgement Set chest against another
|
||||
Judgement chest: item tooltip ~22 lines + compare block ~10 lines).
|
||||
- Verify no crash / no visual glitches on:
|
||||
- ShoppingTooltip1/2 (same class, same `GameTooltipTemplate`)
|
||||
- Rapid re-hovers (create/destroy pressure on the added FontStrings)
|
||||
- Item tooltips that stay under 30 lines (pre-existing behavior unchanged)
|
||||
|
||||
### Risks
|
||||
|
||||
- If the array reallocation logic in `FUN_00536c80` doesn't null-out
|
||||
the tail on grow, `textLeft[N]`/`textRight[N]` might contain
|
||||
garbage for a frame — anchor before write, or memset to zero.
|
||||
- FontString lifetime: 3.3.5 leaks the extras for the tooltip
|
||||
lifetime (they stay hidden when unused). GameTooltip is a
|
||||
singleton, so leak is bounded. Fine.
|
||||
- Interaction with `pfUI`'s existing tooltip skinning (frame borders,
|
||||
status bar reposition, etc.): the layer we're modifying is beneath
|
||||
the skinning; shouldn't touch anything visible unless anchor math
|
||||
is wrong.
|
||||
|
||||
### Related
|
||||
|
||||
If we ever consider dropping the whole "GameTooltipTemplate declares
|
||||
30 FontStrings in XML" scheme and instead having the engine start
|
||||
with 0 and grow entirely on demand — that would be even cleaner, but
|
||||
requires patching every consumer of the initial pool (there aren't
|
||||
many, but they need audit). Filed for future if the incremental grow
|
||||
turns out to be fragile.
|
||||
|
||||
### Investigation update (resolved unknowns)
|
||||
|
||||
Traced the pool machinery end-to-end. The open questions above are now
|
||||
answered, and they point away from both the AddLine hook (Approach A)
|
||||
and the "trigger the engine's scan" idea — a **hybrid** is cleaner than
|
||||
either.
|
||||
|
||||
**The scan is virtual slot 9, run only at XML-template instantiation.**
|
||||
`FUN_00529650` is slot 9 (`vtable+0x24`) of the CGameTooltip vtable
|
||||
(base `0x00808f60`; the null at `0x00808f5c` is the boundary, and slot 2
|
||||
`0x005368d0` is the `tooltip:method()` dispatcher — confirms the class).
|
||||
The two vtable-base xrefs are the constructors `FUN_00529240` /
|
||||
`FUN_00529480`. The scan's prologue calls
|
||||
`FUN_0076a2f0(node) → FUN_0076a060(node)`, which reads the XML
|
||||
`"inherits"` attribute and child `"Frames"` — i.e. the scan is the
|
||||
frame's **XML OnLoad / region-setup** virtual. It runs once when
|
||||
`GameTooltip` is built from `GameTooltipTemplate` (and again per
|
||||
`/reload`), **not** on `SetOwner`/show. So we can't cheaply re-trigger
|
||||
it — the `node` arg is a live XML-parse handle we don't have, and
|
||||
calling it with a bogus node runs the inherits/Frames path on garbage.
|
||||
|
||||
**The scan finds lines by `_G[name]`, so Lua-created FontStrings count.**
|
||||
`numLinesAllocated` (`+0x320`) is literally "how many contiguous
|
||||
`%sTextLeftN` **and** `%sTextRightN` exist by name": pass 0 counts (stop
|
||||
at the first missing index), pass 1 reallocs the three arrays to that
|
||||
count and fills the pointers. The name lookup `FUN_0076c760` does
|
||||
`lua_pushstring(name); gettable(_G); if type==TABLE: FrameScript_GetObject`
|
||||
— a plain `_G[name]` resolve, so a FontString made with
|
||||
`GameTooltip:CreateFontString("GameTooltipTextLeft31", …)` **is**
|
||||
visible to it. (Names must be contiguous and exist in *both* columns.)
|
||||
|
||||
**The array reallocators are simple and callable from C.**
|
||||
`FUN_00536c80` (pointer arrays) and `FUN_004368c0` (wrap-flag array) are
|
||||
the same routine with different SMem tags:
|
||||
`__thiscall(descriptor, newCount)` where the descriptor is
|
||||
`{ count@+0x0, cap@+0x4, data@+0x8 }` — reallocs `data` to `newCount*4`
|
||||
and copies the old contents. In the tooltip the three descriptors are:
|
||||
|
||||
| Descriptor `this` | count / cap / data | array |
|
||||
|-------------------|--------------------|-------|
|
||||
| `tooltip + 0x324` | `+0x324 / +0x328 / +0x32c` | `textLeft[]` (`CSimpleFontString*`) |
|
||||
| `tooltip + 0x330` | `+0x330 / +0x334 / +0x338` | `textRight[]` |
|
||||
| `tooltip + 0x33c` | `+0x33c / +0x340 / +0x344` | `wrapFlag[]` (int, 4-byte) |
|
||||
|
||||
`numLinesAllocated` (`+0x320`) is a separate field the scan sets equal
|
||||
to the count; AddLine (`FUN_00530270`) gates on
|
||||
`numLines(+0x31c) != numLinesAllocated(+0x320)`.
|
||||
|
||||
**Recommended approach — Lua creates FontStrings, C grows the arrays
|
||||
(no hook, no XML re-run).** Replicate the scan's pass 1 directly:
|
||||
|
||||
1. **Lua (`!!!ClassicAPI`), at load / `PLAYER_LOGIN`:** for each frame on
|
||||
`GameTooltipTemplate` (`GameTooltip`, `ShoppingTooltip1/2`,
|
||||
`ItemRefTooltip`, …), create `<name>TextLeft31..N` and
|
||||
`<name>TextRight31..N` via `frame:CreateFontString(nm, "ARTWORK",
|
||||
"GameTooltipText")`, each anchored `TOPLEFT`/`TOPRIGHT` to the prior
|
||||
line's `BOTTOMLEFT`/`BOTTOMRIGHT` (mirror the template's chain).
|
||||
This avoids re-deriving the 1.12 FontString ctor / setName / anchor
|
||||
helpers that Approach A needed — Lua already wraps them.
|
||||
2. **C entrypoint** `GrowTooltipPool(tooltipObj, newCount)` (expose as a
|
||||
Lua global, called once per frame right after step 1):
|
||||
- `FUN_00536c80(tooltip+0x324, newCount)`; set `+0x328 = newCount`.
|
||||
- `FUN_00536c80(tooltip+0x330, newCount)`; set `+0x334 = newCount`.
|
||||
- `FUN_004368c0(tooltip+0x33c, newCount)`; set `+0x340 = newCount`.
|
||||
- for `i` in `[oldAlloc, newCount)`: resolve `<name>TextLeftN`/
|
||||
`RightN` via the `_G[name]` dance (same as `FUN_0076c760`), and if
|
||||
either is missing stop at `i`; else write the two pointers into the
|
||||
new `textLeft`/`textRight` data slots and zero `wrapFlag[i]`.
|
||||
- set `numLinesAllocated (+0x320) = i`.
|
||||
Resolve `tooltipObj` from the Lua frame the same way our other
|
||||
tooltip methods do (`FrameScript_GetObject`).
|
||||
|
||||
Runs once per frame per session (re-runs on `/reload`, which recreates
|
||||
the frames → scan resets to 30 → our addon re-creates + re-grows). No
|
||||
per-line hook (dodges the MinHook-collision risk on a moderately hot
|
||||
function), no XML-node fabrication.
|
||||
|
||||
Remaining risk to validate in-game: whether AddLine's downstream layout
|
||||
/ `Show` sizing walks `numLines` FontStrings by their anchors (expected)
|
||||
vs. any hidden fixed-30 assumption elsewhere; and that the Lua-created
|
||||
lines inherit height/spacing identical to the template's 1..30 so the
|
||||
tooltip grows seamlessly. Both are observable on the first >30-line
|
||||
hover.
|
||||
|
||||
+120
-1
@@ -139,6 +139,9 @@ build instructions.
|
||||
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
|
||||
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
|
||||
- [`GameTooltip:SetInventoryItemByID(itemID)`](#gametooltipsetinventoryitembyiditemid)
|
||||
- [`GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])`](#gametooltipsethyperlinkcompareitemitemlink--offset-shiftbutton-comparisontooltip)
|
||||
- [`GameTooltip:IsEquippedItem()`](#gametooltipisequippeditem)
|
||||
- [`OnTooltipSetItem` script](#ontooltipsetitem-script)
|
||||
- [`GameTooltip:SetItemByGUID(itemGUID)`](#gametooltipsetitembyguiditemguid)
|
||||
- [`GameTooltip:SetEquipmentSet(name)`](#gametooltipsetequipmentsetname)
|
||||
- [`GameTooltip:GetItem()`](#gametooltipgetitem)
|
||||
@@ -3421,6 +3424,117 @@ end
|
||||
GameTooltip:Show()
|
||||
```
|
||||
|
||||
### `GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])`
|
||||
|
||||
Fills the tooltip with the item **currently equipped** in the slot the
|
||||
given item would occupy — a grey `Currently Equipped` header, the
|
||||
equipped item's own tooltip, then (when `shiftButton` is set) a
|
||||
green/red per-stat delta breakdown of how the given item compares.
|
||||
This is the method behind item-comparison ("shopping") tooltips;
|
||||
`FrameXML`'s `GameTooltip_ShowCompareItem` drives it.
|
||||
|
||||
Call it on the tooltip that should show the comparison (typically
|
||||
`ShoppingTooltip1`/`ShoppingTooltip2`, but any `GameTooltipTemplate`
|
||||
frame works).
|
||||
|
||||
| Arg | Meaning |
|
||||
|-----|---------|
|
||||
| `itemLink` | The item being compared (chat link, `item:` string, or bare itemID). Optional if `comparisonTooltip` is given. |
|
||||
| `offset` | 1-based slot selector for two-slot items — rings, trinkets, and one-hand weapons expose `offset` 1 and 2 (Finger1/2, Trinket1/2, MainHand/OffHand). Default 1. |
|
||||
| `shiftButton` | Gates the stat-change breakdown. Deltas show by default (`true`/`1`/`nil`/omitted) and are hidden only for an explicit `false`/`0` (header + equipped item only). Both the boolean and vanilla `1`/`nil` conventions are accepted. |
|
||||
| `comparisonTooltip` | Optional. When `itemLink` is omitted, the compared item is taken from whatever this tooltip is displaying (matches the retail tooltip-to-tooltip call). |
|
||||
|
||||
**Returns** the number of comparison slots for the item (1, or 2 for
|
||||
rings/trinkets/one-hand weapons; 0 if it isn't equippable, isn't
|
||||
cached, or nothing is equipped in the chosen slot) — so a caller knows
|
||||
whether a second `offset` is worth querying.
|
||||
|
||||
The stat deltas cover the same keys as
|
||||
[`C_Item.GetItemStats`](#c_itemgetitemstatsitemlink) (base stats,
|
||||
resistances, weapon DPS, and on-equip-spell bonuses like crit / attack
|
||||
power / spell power), colored with the client's
|
||||
`INCREASE_STAT_COLOR` / `DECREASE_STAT_COLOR`. The method only
|
||||
populates the tooltip; the caller shows/anchors it (as retail's
|
||||
`SetHyperlinkCompareItem` does).
|
||||
|
||||
```lua
|
||||
-- Compare a hovered bag item against what's equipped:
|
||||
local link = GetContainerItemLink(bag, slot)
|
||||
ShoppingTooltip1:SetOwner(GameTooltip, "ANCHOR_NONE")
|
||||
local slots = ShoppingTooltip1:SetHyperlinkCompareItem(link, 1, true)
|
||||
ShoppingTooltip1:Show()
|
||||
if slots == 2 then -- ring/trinket/1H: also compare the other slot
|
||||
ShoppingTooltip2:SetOwner(GameTooltip, "ANCHOR_NONE")
|
||||
ShoppingTooltip2:SetHyperlinkCompareItem(link, 2, true)
|
||||
ShoppingTooltip2:Show()
|
||||
end
|
||||
```
|
||||
|
||||
### `GameTooltip:IsEquippedItem()`
|
||||
|
||||
Returns `true` when the item the tooltip is **currently displaying** is
|
||||
equipped in one of the player's character-pane slots (1..19). Backports
|
||||
the 3.3.5 `GameTooltip:IsEquippedItem` method.
|
||||
|
||||
Works regardless of how the tooltip was populated — link paths
|
||||
(`SetItemByID`, `SetHyperlink`) and CGItem paths (`SetInventoryItem`,
|
||||
`SetBagItem`, `SetItemByGUID`, …) both resolve to the displayed item.
|
||||
Returns `false` for a tooltip that isn't showing an item (e.g. a spell
|
||||
tooltip) with no false positives.
|
||||
|
||||
```lua
|
||||
GameTooltip:SetInventoryItem("player", INVSLOT_HAND)
|
||||
if GameTooltip:IsEquippedItem() then ... end -- true
|
||||
|
||||
GameTooltip:SetBagItem(0, 1)
|
||||
if GameTooltip:IsEquippedItem() then ... end -- false unless that item is also worn
|
||||
```
|
||||
|
||||
### `OnTooltipSetItem` script
|
||||
|
||||
A real frame script — settable with the standard `SetScript` / `GetScript` /
|
||||
`HookScript` — that fires whenever a tooltip's **item** is set. Backports the
|
||||
modern tooltip script so addons annotate item tooltips by hooking one script
|
||||
instead of wrapping every `Set*` method.
|
||||
|
||||
Fires for every item-setting path (`SetBagItem`, `SetInventoryItem`,
|
||||
`SetHyperlink` for an `item:` link, `SetMerchantItem`, `SetAuctionItem`,
|
||||
`SetItemByID`, …). Available on all GameTooltip-type frames (`GameTooltip`,
|
||||
`ItemRefTooltip`, `ShoppingTooltip1/2`, `AtlasLootTooltip`, …).
|
||||
|
||||
The handler receives the tooltip as the **global `this`**, the 1.12
|
||||
frame-script convention — *not* a `self` argument (like every built-in vanilla
|
||||
script: `OnShow`, `OnEvent`, `OnTooltipCleared`, …). Modern-style
|
||||
`function(self) self:… end` handlers will see `self == nil`; use `this`.
|
||||
|
||||
```lua
|
||||
GameTooltip:HookScript("OnTooltipSetItem", function()
|
||||
local name, link, id = this:GetItem() -- the tooltip is the global `this`
|
||||
if id then
|
||||
this:AddLine("ID: " .. id, 0.6, 0.6, 0.6)
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
Vanilla only shipped `OnTooltipAddMoney` / `OnTooltipCleared` /
|
||||
`OnTooltipSetDefaultAnchor`; `OnTooltipSetSpell` / `OnTooltipSetUnit` are not
|
||||
yet provided.
|
||||
|
||||
**Caveat — the event fires from inside the item-tooltip build.** Lightweight
|
||||
handler work is fine: reading `this:GetItem()`, `this:AddLine(...)`, printing,
|
||||
etc. But avoid *re-entrant* tooltip rebuilds from the handler — e.g.
|
||||
`GameTooltip_ShowCompareItem()`, or a `Set*` call on another tooltip. Because
|
||||
the handler runs mid-build, re-entering the tooltip / FrameScript machinery can
|
||||
collide with other DLLs that hook the same Lua paths (nampower, SuperWoW,
|
||||
weirdutils, …) and crash the client. If you need heavy work like that, defer it
|
||||
to the next frame:
|
||||
|
||||
```lua
|
||||
GameTooltip:HookScript("OnTooltipSetItem", function()
|
||||
C_Timer.After(0, function() GameTooltip_ShowCompareItem() end)
|
||||
end)
|
||||
```
|
||||
|
||||
### `GameTooltip:SetEquipmentSet(name)`
|
||||
|
||||
Fills the tooltip with a summary of the named equipment set: header,
|
||||
@@ -4738,7 +4852,7 @@ what the item actually grants — not just its stored stat slots:
|
||||
| Key | Stat | Source |
|
||||
|-----|------|--------|
|
||||
| `ITEM_MOD_STRENGTH_SHORT` / `_AGILITY_` / `_STAMINA_` / `_INTELLECT_` / `_SPIRIT_SHORT` | Base attributes | record / `MOD_STAT` |
|
||||
| `ITEM_MOD_MANA_SHORT` / `ITEM_MOD_HEALTH_SHORT` | Mana / health on equip | record |
|
||||
| `ITEM_MOD_MANA_SHORT` / `ITEM_MOD_HEALTH_SHORT` | Mana / health on equip | record / `MOD_INCREASE_ENERGY` (35) / `MOD_INCREASE_HEALTH` (34) |
|
||||
| `RESISTANCE0_NAME` | Armor | record / `MOD_RESISTANCE` bit 0 |
|
||||
| `RESISTANCE1_NAME` … `RESISTANCE6_NAME` | Holy / Fire / Nature / Frost / Shadow / Arcane resistance | record / `MOD_RESISTANCE` / type-4 enchant |
|
||||
| `ITEM_MOD_DAMAGE_PER_SECOND_SHORT` | Weapon DPS (float) | avg damage ÷ swing time |
|
||||
@@ -4747,10 +4861,15 @@ what the item actually grants — not just its stored stat slots:
|
||||
| `ITEM_MOD_CRIT_MELEE_RATING` + `ITEM_MOD_CRIT_RANGED_RATING` | Crit % | weapon-crit aura (52) |
|
||||
| `ITEM_MOD_HIT_MELEE_RATING` + `ITEM_MOD_HIT_RANGED_RATING` | Hit % | hit aura (54) |
|
||||
| `ITEM_MOD_HIT_SPELL_RATING` | Spell hit % | spell-hit aura (55) |
|
||||
| `ITEM_MOD_CRIT_SPELL_RATING` | Spell crit % | spell-crit aura (57 / 71) |
|
||||
| `ITEM_MOD_SPELL_DAMAGE_DONE_SHORT` | Spell damage | `MOD_DAMAGE_DONE` (13) |
|
||||
| `ITEM_MOD_SPELL_HEALING_DONE_SHORT` | Healing | `MOD_HEALING_DONE` (135) |
|
||||
| `ITEM_MOD_MANA_REGENERATION` | mp5 (mana) | `MOD_POWER_REGEN` (85) |
|
||||
| `ITEM_MOD_DEFENSE_SKILL_RATING` | Defense skill | `MOD_SKILL` (30, misc 95) |
|
||||
| `ITEM_MOD_DODGE_RATING` | Dodge % | `MOD_DODGE_PERCENT` (49) |
|
||||
| `ITEM_MOD_PARRY_RATING` | Parry % | `MOD_PARRY_PERCENT` (47) |
|
||||
| `ITEM_MOD_BLOCK_RATING` | Block chance % | `MOD_BLOCK_PERCENT` (51) |
|
||||
| `ITEM_MOD_BLOCK_VALUE` | Shield block value | record (`m_block`) |
|
||||
|
||||
> **Values are vanilla-native, not ratings.** Vanilla has no rating
|
||||
> system, so the percent-based stats (crit / hit / defense) report the raw
|
||||
|
||||
+152
-2
@@ -87,6 +87,121 @@ enum Offsets {
|
||||
FUN_REGISTER_FRAME_METHODS = 0x00701D80,
|
||||
VAR_GAMETOOLTIP_METHOD_REGISTRY = 0x00C0CF20,
|
||||
|
||||
// CGameTooltip line pool (see TODO #97). The engine caps AddLine
|
||||
// (FUN_00530270) at `numLinesAllocated`, which the XML-OnLoad scan
|
||||
// (FUN_00529650, vtable slot 9) sets to the count of contiguous
|
||||
// `<name>TextLeftN`/`TextRightN` FontStrings the template declares
|
||||
// (30). Tooltip::LinePool grows it: it appends its own C++-created
|
||||
// FontStrings to the three parallel arrays and bumps this count. Each
|
||||
// array is a {count@+0x0, cap@+0x4, data@+0x8} descriptor; the
|
||||
// reallocators realloc `data` to newCount*4 and copy `cap` old elements.
|
||||
VAR_GAMETOOLTIP_VTABLE = 0x00808F60, // type guard for a resolved tooltip object
|
||||
OFF_GAMETOOLTIP_NUM_LINES_ALLOC = 0x320, // int — pool size AddLine gates on
|
||||
OFF_GAMETOOLTIP_TEXTLEFT_DESC = 0x324, // {count,cap,data}; data (+0x8) = CSimpleFontString*[]
|
||||
OFF_GAMETOOLTIP_TEXTRIGHT_DESC = 0x330, // symmetric right column
|
||||
OFF_GAMETOOLTIP_WRAPFLAG_DESC = 0x33C, // per-line wrap int[]
|
||||
FUN_TOOLTIP_REALLOC_PTR_ARRAY = 0x00536C80, // __thiscall(desc, newCount) — pointer arrays
|
||||
FUN_TOOLTIP_REALLOC_INT_ARRAY = 0x004368C0, // __thiscall(desc, newCount) — wrap-flag array
|
||||
|
||||
// GameTooltip:SetHyperlinkCompareItem (Tooltip::Compare) — builds the
|
||||
// equipped item's tooltip natively (no Lua roundtrip) and appends
|
||||
// colored stat deltas. FUN_GAMETOOLTIP_BUILD_ITEM is the engine's own
|
||||
// item-tooltip builder that SetInventoryItem/SetHyperlink call:
|
||||
// __thiscall(self, itemID, guid*, guid*, a4, a5, a6, headerFlag, a8, a9)
|
||||
// Proven arg shape from FUN_005353b0/FUN_00535700 (all flags 0 → a clean
|
||||
// item tooltip). The builder's own headerFlag (param_7) path merges the
|
||||
// "Currently Equipped" text into the name line and force-wraps it, so we
|
||||
// build clean (flag 0) and prepend the grey header ourselves by shifting
|
||||
// the line FontStrings — see FUN_FONTSTRING_SET_* / OFF_FONTSTRING_*.
|
||||
// FUN_GAMETOOLTIP_ADD_LINE is the raw add-line body (see LinePool notes);
|
||||
// its color args are pointers to a 4-byte packed color {b,g,r,a} — the
|
||||
// Lua AddLine handler (FUN_00531630) builds it as 0xFF<rr><gg><bb>, i.e.
|
||||
// uint32 `0xFF000000 | r<<16 | g<<8 | b` in little-endian memory.
|
||||
FUN_GAMETOOLTIP_BUILD_ITEM = 0x0052B650,
|
||||
// OnTooltipSetItem support. FUN_GAMETOOLTIP_SCRIPT_RESOLVER is the
|
||||
// CGGameTooltip vtable method (vtable 0x00808F60) that maps a script name
|
||||
// to its handler slot; __thiscall(self, const char *name) -> int* slot,
|
||||
// 0 if the name isn't a tooltip script. It first delegates to the base
|
||||
// frame resolver, then checks OnTooltipSetDefaultAnchor(+0x444) /
|
||||
// OnTooltipCleared(+0x44c) / OnTooltipAddMoney(+0x454) — each an 8-byte
|
||||
// {handler, context} slot. Vanilla has no OnTooltipSetItem, and the object
|
||||
// (alloc size 0x460) has no free 8-byte slot, so we co-hook this to hand
|
||||
// out a C-side per-tooltip cell for that name. FUN_FRAME_INVOKE_SCRIPT is
|
||||
// the engine's real script invoker __fastcall(handler /*ecx = slot[0]*/,
|
||||
// frame /*edx*/): it binds the global `this` = frame and runs the handler
|
||||
// under its own protected lua_pcall (0 args). We call it directly from the
|
||||
// item-builder co-hook to fire OnTooltipSetItem.
|
||||
// NOTE: the clear fires OnTooltipCleared via the wrapper FUN_00702690
|
||||
// (self, &slot), which additionally stamps the global exec-context
|
||||
// (DAT_00ceeac0) from slot[1]. That context mutation is only valid from
|
||||
// the engine's top-level frame-script dispatch — firing it from deep in a
|
||||
// nested Lua->C->Lua stack (e.g. an addon's SetAuctionItem wrapper)
|
||||
// corrupts the outer script's context and faults. So we skip the wrapper
|
||||
// and call the inner invoker, which is self-contained.
|
||||
FUN_GAMETOOLTIP_SCRIPT_RESOLVER = 0x005295D0,
|
||||
FUN_FRAME_INVOKE_SCRIPT = 0x00704D50,
|
||||
FUN_GAMETOOLTIP_ADD_LINE = 0x00530270, // __thiscall(self, left, right, lColorBGRA*, rColorBGRA*, wrap)
|
||||
OFF_GAMETOOLTIP_NUM_LINES = 0x31C, // int — live line count (AddLine index; +0x320 is the cap)
|
||||
// The displayed item's identity is read via the existing
|
||||
// OFF_TOOLTIP_ITEM_ID (+0x398) / OFF_TOOLTIP_ITEM_GUID_LO (+0x380) —
|
||||
// link paths set the itemID, CGItem paths (SetInventoryItem/…) set only
|
||||
// the GUID (see Tooltip::Compare::TooltipItemID and item/Tooltip.cpp).
|
||||
|
||||
// Line-shift primitives for prepending the grey "Currently Equipped"
|
||||
// header. The per-line CSimpleFontString objects live in the parallel
|
||||
// arrays at OFF_GAMETOOLTIP_TEXTLEFT/RIGHT_DESC (+8 = data). Each stores
|
||||
// its text buffer pointer at +0xF0 and its 4-byte {b,g,r,a} color behind
|
||||
// the pointer at +0xB8 (both verified from the setters below). We read a
|
||||
// line's text/color and re-apply them one slot down, then set line 0.
|
||||
FUN_FONTSTRING_SET_TEXT = 0x00771D80, // __thiscall(fs, text, flag=0)
|
||||
FUN_FONTSTRING_SET_COLOR = 0x0077F750, // __thiscall(fs, colorBGRA*)
|
||||
OFF_FONTSTRING_TEXT = 0xF0, // char* — current text buffer
|
||||
OFF_FONTSTRING_COLOR_PTR = 0xB8, // ptr → 4-byte {b,g,r,a} color storage
|
||||
// A line's FontString is only positioned once shown: set the desired
|
||||
// flag at +0xC4 then call SHOW (FUN_0077fcb0) / HIDE (FUN_0077fc60) —
|
||||
// the exact pair AddLine (FUN_00530270) and the clear use. Moving text
|
||||
// into a previously-empty (hidden) cell without this leaves it
|
||||
// unpositioned, so the line-shift must replicate it.
|
||||
FUN_FONTSTRING_SHOW = 0x0077FCB0, // __fastcall(fs) — realizes +0xC4
|
||||
FUN_FONTSTRING_HIDE = 0x0077FC60, // __fastcall(fs)
|
||||
OFF_FONTSTRING_SHOWN_FLAG = 0xC4, // int — desired-shown flag
|
||||
// The per-tooltip clear only HIDES line FontStrings (sets +0xC8 = 0)
|
||||
// and clears the left text; right-column text buffers are left intact.
|
||||
// So a shift must decide a cell's content by its actually-shown flag
|
||||
// (+0xC8), not by whether +0xF0 still holds (stale) text.
|
||||
OFF_FONTSTRING_VISIBLE = 0xC8, // int — actually-shown flag
|
||||
|
||||
// FontString creation primitives used by Tooltip::LinePool to build the
|
||||
// extra lines entirely in C++ (no addon Lua). Calling conventions and
|
||||
// offsets mirrored from Script_CreateFontString (0x00773C30),
|
||||
// Script_SetFontObject (0x0079D1A0 → 0x00770C60), and Script_SetPoint
|
||||
// (0x007A2540 → 0x00767C70) — see TODO #97's investigation update.
|
||||
FUN_GAMETOOLTIP_SETUP_LINES = 0x00529650, // vtable slot 9 — the pool scan we co-hook
|
||||
VAR_SIMPLEFONTSTRING_POOL = 0x00CF4D10, // CSimpleFontString free-list pool (allocator `this`)
|
||||
VAR_SIMPLEFONTSTRING_CLASS_TAG = 0x00846544, // ".?AVCSimpleFontString@@" (alloc debug tag)
|
||||
FUN_REGION_POOL_ALLOC = 0x00760450, // __thiscall(pool, zeroInit=0, tag, line=-2) -> raw mem
|
||||
FUN_SIMPLEFONTSTRING_CTOR = 0x00770D30, // __thiscall(mem, parent, layer, sublayer) -> fs
|
||||
FUN_REGION_SET_NAME = 0x0076C650, // __thiscall(region, name) — registers _G[name]
|
||||
FUN_FONTSTRING_SET_FONT = 0x00770C60, // __thiscall(fs + OFF_FONTSTRING_FONT_HOLDER, fontObject)
|
||||
FUN_REGION_SET_POINT = 0x00767C70, // __thiscall(region+OFF_REGION_ANCHOR, point, relAnchor, relPoint, x, y, flag=1)
|
||||
OFF_FONTSTRING_FONT_HOLDER = 0xCC, // font-reference sub-object (SetFont `this`)
|
||||
OFF_FONTSTRING_FONT_OBJECT = 0xD0, // font object pointer (holder + 4)
|
||||
OFF_REGION_ANCHOR = 0x24, // LayoutFrame anchor sub-object (SetPoint `this` / relativeTo base)
|
||||
DRAWLAYER_ARTWORK = 2,
|
||||
FRAMEPOINT_TOPLEFT = 0,
|
||||
FRAMEPOINT_LEFT = 3,
|
||||
FRAMEPOINT_RIGHT = 5,
|
||||
FRAMEPOINT_BOTTOMLEFT = 6,
|
||||
|
||||
// FUN_REGION_SET_POINT stores offsets in *internal* coordinates, not
|
||||
// pixels. Script_SetPoint converts: `internal = pixel * [0x00832A44] /
|
||||
// ([0x00832A4C] * 1024)` (FUN_0041ae60's factor × input ÷
|
||||
// (FUN_0041ad70's return × DAT_007ffd68)). Both globals are runtime
|
||||
// UI-scale floats; passing raw pixels makes offsets ~1000× too large.
|
||||
VAR_UI_COORD_SCALE_MUL = 0x00832A44, // float numerator
|
||||
VAR_UI_COORD_SCALE_DIV = 0x00832A4C, // float denominator base
|
||||
UI_COORD_SCALE_UNIT = 1024, // DAT_007ffd68
|
||||
|
||||
// "Currently displayed thing" state fields on a GameTooltip frame
|
||||
// instance. Each Set* path writes one of these (and zero or two
|
||||
// others), and the per-tooltip Clear at FUN_00530050 zeroes all of
|
||||
@@ -106,11 +221,32 @@ enum Offsets {
|
||||
// (gameobject GUID) at 0x0052AA52 / 0x0052AA59.
|
||||
// Only call site is the in-world hover handler
|
||||
// FUN_00492890; no Lua `SetGameObject` method.
|
||||
// - Clear (0x00530050) zeroes all of them.
|
||||
OFF_TOOLTIP_ITEM_GUID_LO = 0x380, // 0 for SetItemByID (no CGItem)
|
||||
// - Clear (FUN_GAMETOOLTIP_CLEAR) zeroes unit(+0x368)/GO(+0x370)/
|
||||
// itemID(+0x398)/spell(+0x39c) — but NOT the item GUID +0x380/+0x384.
|
||||
// That omission means the item GUID goes stale across a switch to a
|
||||
// tooltip that shows no item (SetUnitAura, SetEquipmentSet, …), so
|
||||
// GameTooltip:GetItem would resolve the *previous* item. We co-hook
|
||||
// the clear (Tooltip::ClearItemGuid) to zero +0x380/+0x384 too, so
|
||||
// every fresh tooltip resets it and only the item builder re-sets it.
|
||||
// All 14 Set*/builder paths route through this clear, so the co-hook
|
||||
// covers them uniformly. __fastcall(self).
|
||||
FUN_GAMETOOLTIP_CLEAR = 0x00530050,
|
||||
OFF_TOOLTIP_ITEM_GUID_LO = 0x380, // 0 for SetItemByID (no CGItem); stale-safe via the clear co-hook
|
||||
OFF_TOOLTIP_ITEM_GUID_HI = 0x384,
|
||||
OFF_TOOLTIP_ITEM_ID = 0x398,
|
||||
OFF_TOOLTIP_SPELL_ID = 0x39C,
|
||||
// Auction/compare descriptor. Script_GameTooltip_SetAuctionItem
|
||||
// (0x00535810) has no CGItem instance, so it stashes the listing's
|
||||
// random-property (suffix) id at tooltip+0x424 and marks the
|
||||
// compare-descriptor valid by writing the builder's compare flag to
|
||||
// this[0x110] (tooltip+0x440) = 1 (SetAuctionItem passes param_6=1;
|
||||
// every other Set* path passes 0, and the builder writes it on each
|
||||
// param_7==0 build — so +0x440 is a reliable per-build gate, not
|
||||
// stale). This is 1.12's analog of 3.3.5's `tooltip[0x130]`-gated
|
||||
// `piVar5[0x2d]` random-property read in GameTooltip:GetItem. GetItem
|
||||
// reads +0x424 (only when +0x440 is set) to put the suffix in the link.
|
||||
OFF_TOOLTIP_COMPARE_FLAG = 0x440,
|
||||
OFF_TOOLTIP_COMPARE_SUFFIX = 0x424,
|
||||
// Unit GUID written by the inner unit-tooltip builder
|
||||
// (FUN_00529FE0) when `tooltip:SetUnit(token)` resolves the token
|
||||
// to a non-zero GUID. Cleared by the same `FUN_00530050` clear
|
||||
@@ -1045,6 +1181,13 @@ enum Offsets {
|
||||
// descriptor bytes for a worn-and-bound item.
|
||||
OFF_DESCRIPTOR_FLAGS = 0x3C,
|
||||
ITEM_FLAG_SOULBOUND = 0x01,
|
||||
// Within the descriptor, the item's random-property index
|
||||
// (ITEM_FIELD_RANDOM_PROPERTIES_ID) is at +0x98 — this is the live
|
||||
// instance's suffix, indexing ItemRandomProperties.dbc the same way
|
||||
// StatAccum::ApplyRandomSuffix expects. Derived from the item-tooltip
|
||||
// builder's `[descriptor + 0x26*4]` read (FUN_0052B650 @ ~0x52b7xx),
|
||||
// which feeds it straight into `ItemRandomProperties[idx]`.
|
||||
OFF_DESCRIPTOR_RANDOM_PROPERTY = 0x98,
|
||||
// Bit 2 — instance is "unlocked", i.e. the lock referenced by
|
||||
// `ItemStats.LockID` has been opened (rogue Pick Lock, key item,
|
||||
// etc.). For unlockable items, the engine gates the
|
||||
@@ -4111,6 +4254,13 @@ enum Offsets {
|
||||
// stat auras. Parallel [3] array to APPLY_AURA_NAME / MISC_VALUE.
|
||||
OFF_SPELL_RECORD_EFFECT_BASE_POINTS = 0x130, // int32[3]
|
||||
|
||||
// EffectBaseDice[3] — added to EffectBasePoints for the effect's fixed
|
||||
// magnitude (mangos `CalculateSimpleValue = BasePoints + BaseDice`).
|
||||
// For every item-stat aura BaseDice == 1, so value = BasePoints + 1;
|
||||
// reading it directly matches the engine exactly. Verified at field 67
|
||||
// (Effect@61, DieSides@64, BaseDice@67, BasePoints@76) against 7468.
|
||||
OFF_SPELL_RECORD_EFFECT_BASE_DICE = 0x10C, // int32[3]
|
||||
|
||||
// Per-effect EffectRadiusIndex[3] → SpellRadius.dbc. Verified by
|
||||
// FUN_006e6350, which reads spellRec[+0x160] (effect 0) and
|
||||
// spellRec[+0x164] (effect 1) as radius indices. 0 = effect has no
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
// 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/>.
|
||||
|
||||
// Shared item-stat accumulation engine — see [src/item/StatAccum.h] for
|
||||
// the public surface. This file holds the value derivation:
|
||||
//
|
||||
// Three sources feed the accumulator:
|
||||
// 1. The base `ItemStats_C` record — the stored attributes
|
||||
// (Str/Agi/Sta/Int/Spi, mana/health-on-equip), armor, six resists,
|
||||
// and (for weapons) DPS derived from the damage/delay fields.
|
||||
// 2. The item's on-equip spells (`m_spellTrigger == 1`) — vanilla
|
||||
// (and Turtle's custom gear) store "special" bonuses (crit, hit,
|
||||
// attack power, spell power/healing, mp5, defense, dodge, parry,
|
||||
// block, spell crit, flat health/mana, …) as an equip spell whose
|
||||
// aura effect carries the value, not as a stat slot. We decode those
|
||||
// auras (the aura → key map is in AddSpellStatAuras).
|
||||
// 3. The link's random suffix (`item:id:enchant:SUFFIX:unique`) —
|
||||
// ItemRandomProperties.dbc → SpellItemEnchantment.dbc, whose stat
|
||||
// suffixes are themselves equip spells (enchant type 3) and whose
|
||||
// armor suffixes are direct resistance enchants (type 4).
|
||||
//
|
||||
// **Values are vanilla-native.** The percent-based stats (crit / hit /
|
||||
// defense) are reported as the raw vanilla percentage under the modern
|
||||
// rating key — e.g. Krol Blade's "+1% crit" is
|
||||
// `ITEM_MOD_CRIT_MELEE_RATING = 1`, NOT the level-scaled rating (13) that
|
||||
// TBC/Era would show. Vanilla has no rating conversion (it's
|
||||
// level-dependent and doesn't exist in this client), so a native percent
|
||||
// is the only honest, level-independent value.
|
||||
//
|
||||
// The aura → key map (below) covers the common gear stats. Rarer auras
|
||||
// (percent attack power, weapon-skill-per-line, damage-taken, procs) are
|
||||
// intentionally skipped — they either lack a clean single key or aren't
|
||||
// part of the stat set.
|
||||
|
||||
#include "item/StatAccum.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace Item::StatAccum {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
// Base-record m_statType uses the ItemModType enum: 0=mana, 1=health,
|
||||
// 3=agility, 4=strength, 5=intellect, 6=spirit, 7=stamina (2 is
|
||||
// deprecated). Returns nullptr for types with no vanilla key.
|
||||
const char *StatKeyForItemModType(uint32_t statType) {
|
||||
switch (statType) {
|
||||
case 0: return "ITEM_MOD_MANA_SHORT";
|
||||
case 1: return "ITEM_MOD_HEALTH_SHORT";
|
||||
case 3: return "ITEM_MOD_AGILITY_SHORT";
|
||||
case 4: return "ITEM_MOD_STRENGTH_SHORT";
|
||||
case 5: return "ITEM_MOD_INTELLECT_SHORT";
|
||||
case 6: return "ITEM_MOD_SPIRIT_SHORT";
|
||||
case 7: return "ITEM_MOD_STAMINA_SHORT";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// SPELL_AURA_MOD_STAT (29) misc uses the UnitStat enum instead: 0=Str,
|
||||
// 1=Agi, 2=Sta, 3=Int, 4=Spi (-1 = all stats, not a single key).
|
||||
// Verified against the "Increased <stat> 01" random-property spells.
|
||||
const char *StatKeyForUnitStat(int stat) {
|
||||
switch (stat) {
|
||||
case 0: return "ITEM_MOD_STRENGTH_SHORT";
|
||||
case 1: return "ITEM_MOD_AGILITY_SHORT";
|
||||
case 2: return "ITEM_MOD_STAMINA_SHORT";
|
||||
case 3: return "ITEM_MOD_INTELLECT_SHORT";
|
||||
case 4: return "ITEM_MOD_SPIRIT_SHORT";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Resistance school index → key. 0 = armor (physical), 1..6 = the magic
|
||||
// schools in Holy/Fire/Nature/Frost/Shadow/Arcane order.
|
||||
const char *ResistKey(int school) {
|
||||
static const char *const kKeys[7] = {
|
||||
"RESISTANCE0_NAME", "RESISTANCE1_NAME", "RESISTANCE2_NAME",
|
||||
"RESISTANCE3_NAME", "RESISTANCE4_NAME", "RESISTANCE5_NAME",
|
||||
"RESISTANCE6_NAME",
|
||||
};
|
||||
return (school >= 0 && school <= 6) ? kKeys[school] : nullptr;
|
||||
}
|
||||
|
||||
// Keys in this list are the ones AddByKey ever writes: the base stats +
|
||||
// armor + six resistances, plus the equip-spell "special" stats. The
|
||||
// equip-spell keys past the resistances are best-effort names verified
|
||||
// against a 1.15.x client (crit melee/ranged are confirmed from a TBC
|
||||
// GetItemStats dump); adjust here if the live client differs.
|
||||
constexpr const char *kKeys[] = {
|
||||
"ITEM_MOD_MANA_SHORT",
|
||||
"ITEM_MOD_HEALTH_SHORT",
|
||||
"ITEM_MOD_AGILITY_SHORT",
|
||||
"ITEM_MOD_STRENGTH_SHORT",
|
||||
"ITEM_MOD_INTELLECT_SHORT",
|
||||
"ITEM_MOD_SPIRIT_SHORT",
|
||||
"ITEM_MOD_STAMINA_SHORT",
|
||||
"RESISTANCE0_NAME",
|
||||
"RESISTANCE1_NAME",
|
||||
"RESISTANCE2_NAME",
|
||||
"RESISTANCE3_NAME",
|
||||
"RESISTANCE4_NAME",
|
||||
"RESISTANCE5_NAME",
|
||||
"RESISTANCE6_NAME",
|
||||
"ITEM_MOD_ATTACK_POWER_SHORT",
|
||||
"ITEM_MOD_RANGED_ATTACK_POWER_SHORT",
|
||||
"ITEM_MOD_CRIT_MELEE_RATING",
|
||||
"ITEM_MOD_CRIT_RANGED_RATING",
|
||||
"ITEM_MOD_HIT_MELEE_RATING",
|
||||
"ITEM_MOD_HIT_RANGED_RATING",
|
||||
"ITEM_MOD_HIT_SPELL_RATING",
|
||||
"ITEM_MOD_CRIT_SPELL_RATING",
|
||||
"ITEM_MOD_SPELL_DAMAGE_DONE_SHORT",
|
||||
"ITEM_MOD_SPELL_HEALING_DONE_SHORT",
|
||||
"ITEM_MOD_MANA_REGENERATION",
|
||||
"ITEM_MOD_DEFENSE_SKILL_RATING",
|
||||
"ITEM_MOD_DODGE_RATING",
|
||||
"ITEM_MOD_PARRY_RATING",
|
||||
"ITEM_MOD_BLOCK_RATING",
|
||||
"ITEM_MOD_BLOCK_VALUE",
|
||||
};
|
||||
static_assert(sizeof(kKeys) / sizeof(kKeys[0]) == kCount,
|
||||
"kCount must match the kKeys list");
|
||||
|
||||
long U32(const uint8_t *record, int offset) {
|
||||
return static_cast<long>(*reinterpret_cast<const uint32_t *>(record + offset));
|
||||
}
|
||||
|
||||
// Folds a spell's stat/resistance auras into `acc`. Used for both the
|
||||
// item's own on-equip spells and random-suffix equip enchants. The value
|
||||
// is the fixed-die `EffectBasePoints + EffectBaseDice` (mangos
|
||||
// CalculateSimpleValue). Percent stats keep their vanilla percentage.
|
||||
// Auras with no clean stat key are ignored (procs, dummies,
|
||||
// weapon-skill-per-line, damage-taken, etc.).
|
||||
void AddSpellStatAuras(Accum *acc, int spellID, long sign) {
|
||||
if (spellID <= 0)
|
||||
return;
|
||||
const uint8_t *sp = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (sp == nullptr)
|
||||
return;
|
||||
auto aura = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto misc = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
|
||||
auto base = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
auto dice = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_DICE);
|
||||
|
||||
// Vanilla "+N Attack Power" spells carry a melee (99) AND a ranged
|
||||
// (124) aura with the same value; the generic melee key subsumes the
|
||||
// ranged one, so we emit ranged only for ranged-only spells (scopes).
|
||||
bool hasMeleeAP = false;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == 99)
|
||||
hasMeleeAP = true;
|
||||
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const long v = static_cast<long>(base[i]) + static_cast<long>(dice[i]);
|
||||
switch (aura[i]) {
|
||||
case 29: // SPELL_AURA_MOD_STAT — misc = UnitStat index
|
||||
AddByKey(acc, StatKeyForUnitStat(misc[i]), sign * v);
|
||||
break;
|
||||
case 34: // SPELL_AURA_MOD_INCREASE_HEALTH — flat +health
|
||||
AddByKey(acc, "ITEM_MOD_HEALTH_SHORT", sign * v);
|
||||
break;
|
||||
case 35: // SPELL_AURA_MOD_INCREASE_ENERGY — misc = power type
|
||||
if (misc[i] == 0) // mana
|
||||
AddByKey(acc, "ITEM_MOD_MANA_SHORT", sign * v);
|
||||
break;
|
||||
case 22: // SPELL_AURA_MOD_RESISTANCE — misc = school bitmask
|
||||
for (int school = 0; school <= 6; ++school)
|
||||
if (misc[i] & (1 << school))
|
||||
AddByKey(acc, ResistKey(school), sign * v);
|
||||
break;
|
||||
case 99: // SPELL_AURA_MOD_ATTACK_POWER
|
||||
AddByKey(acc, "ITEM_MOD_ATTACK_POWER_SHORT", sign * v);
|
||||
break;
|
||||
case 124: // SPELL_AURA_MOD_RANGED_ATTACK_POWER
|
||||
if (!hasMeleeAP)
|
||||
AddByKey(acc, "ITEM_MOD_RANGED_ATTACK_POWER_SHORT", sign * v);
|
||||
break;
|
||||
case 52: // weapon crit % — TBC surfaces it as melee AND ranged
|
||||
AddByKey(acc, "ITEM_MOD_CRIT_MELEE_RATING", sign * v);
|
||||
AddByKey(acc, "ITEM_MOD_CRIT_RANGED_RATING", sign * v);
|
||||
break;
|
||||
case 54: // melee/ranged hit %
|
||||
AddByKey(acc, "ITEM_MOD_HIT_MELEE_RATING", sign * v);
|
||||
AddByKey(acc, "ITEM_MOD_HIT_RANGED_RATING", sign * v);
|
||||
break;
|
||||
case 55: // spell hit %
|
||||
AddByKey(acc, "ITEM_MOD_HIT_SPELL_RATING", sign * v);
|
||||
break;
|
||||
case 57: // SPELL_AURA_MOD_SPELL_CRIT_CHANCE
|
||||
case 71: // ..._SCHOOL (misc = school mask) — same stat
|
||||
AddByKey(acc, "ITEM_MOD_CRIT_SPELL_RATING", sign * v);
|
||||
break;
|
||||
case 47: // SPELL_AURA_MOD_PARRY_PERCENT
|
||||
AddByKey(acc, "ITEM_MOD_PARRY_RATING", sign * v);
|
||||
break;
|
||||
case 49: // SPELL_AURA_MOD_DODGE_PERCENT
|
||||
AddByKey(acc, "ITEM_MOD_DODGE_RATING", sign * v);
|
||||
break;
|
||||
case 51: // SPELL_AURA_MOD_BLOCK_PERCENT (block chance)
|
||||
AddByKey(acc, "ITEM_MOD_BLOCK_RATING", sign * v);
|
||||
break;
|
||||
case 13: // SPELL_AURA_MOD_DAMAGE_DONE — misc = school mask
|
||||
if (misc[i] & 0x7E) // any magic school (bits 1..6)
|
||||
AddByKey(acc, "ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", sign * v);
|
||||
break;
|
||||
case 135: // SPELL_AURA_MOD_HEALING_DONE
|
||||
AddByKey(acc, "ITEM_MOD_SPELL_HEALING_DONE_SHORT", sign * v);
|
||||
break;
|
||||
case 85: // SPELL_AURA_MOD_POWER_REGEN (mp5) — misc = power type
|
||||
if (misc[i] == 0) // mana
|
||||
AddByKey(acc, "ITEM_MOD_MANA_REGENERATION", sign * v);
|
||||
break;
|
||||
case 30: // SPELL_AURA_MOD_SKILL — misc = skill line (95 = Defense)
|
||||
if (misc[i] == 95)
|
||||
AddByKey(acc, "ITEM_MOD_DEFENSE_SKILL_RATING", sign * v);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes the item's own on-equip spells (SpellTrigger == 1) into `acc`.
|
||||
// On-use (0) and chance-on-hit (2) spells are skipped — they aren't
|
||||
// passive stats. This is what surfaces crit / AP / spell power / hit /
|
||||
// mp5 / defense on base items (vanilla stores them as equip spells, not
|
||||
// stat slots).
|
||||
void AddEquipSpellStats(Accum *acc, const uint8_t *record, long sign) {
|
||||
auto ids = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i)
|
||||
if (ids[i] != 0 && triggers[i] == 1) // ON_EQUIP
|
||||
AddSpellStatAuras(acc, static_cast<int>(ids[i]), sign);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Init(Accum *acc) {
|
||||
for (int i = 0; i < kCount; ++i) {
|
||||
acc[i].key = kKeys[i];
|
||||
acc[i].value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void AddByKey(Accum *acc, const char *key, long delta) {
|
||||
if (key == nullptr || delta == 0)
|
||||
return;
|
||||
for (int i = 0; i < kCount; ++i)
|
||||
if (std::strcmp(acc[i].key, key) == 0) {
|
||||
acc[i].value += delta;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t *FetchRecord(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, false);
|
||||
}
|
||||
|
||||
double ComputeDPS(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return 0.0;
|
||||
const uint32_t delayMs = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DELAY);
|
||||
if (delayMs == 0)
|
||||
return 0.0;
|
||||
auto *dmgMin = reinterpret_cast<const float *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DAMAGE_MIN);
|
||||
auto *dmgMax = reinterpret_cast<const float *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DAMAGE_MAX);
|
||||
double avgTotal = 0.0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT; ++i)
|
||||
avgTotal += (static_cast<double>(dmgMin[i]) +
|
||||
static_cast<double>(dmgMax[i])) * 0.5;
|
||||
if (avgTotal <= 0.0)
|
||||
return 0.0;
|
||||
return avgTotal / (static_cast<double>(delayMs) / 1000.0);
|
||||
}
|
||||
|
||||
void ApplyRandomSuffix(Accum *acc, int suffixID, long sign) {
|
||||
if (suffixID <= 0)
|
||||
return;
|
||||
const uint8_t *rp = DBC::Record(Offsets::VAR_ITEMRANDOMPROP_RECORDS,
|
||||
Offsets::VAR_ITEMRANDOMPROP_COUNT,
|
||||
static_cast<uint32_t>(suffixID));
|
||||
if (rp == nullptr)
|
||||
return;
|
||||
auto enchants = reinterpret_cast<const uint32_t *>(
|
||||
rp + Offsets::OFF_ITEMRANDOMPROP_ENCHANT);
|
||||
for (int s = 0; s < Offsets::ITEMRANDOMPROP_ENCHANT_SLOT_COUNT; ++s) {
|
||||
const uint32_t enchantID = enchants[s];
|
||||
if (enchantID == 0)
|
||||
continue;
|
||||
const uint8_t *en = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
if (en == nullptr)
|
||||
continue;
|
||||
auto type = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
for (int i = 0; i < 3; ++i) { // SpellItemEnchantment has 3 effect slots
|
||||
switch (type[i]) {
|
||||
case 3: // equip spell — stat/resist value lives in the spell
|
||||
AddSpellStatAuras(acc, arg[i], sign);
|
||||
break;
|
||||
case 4: // resistance — arg = school index (0 = armor)
|
||||
AddByKey(acc, ResistKey(arg[i]), sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
case 5: // direct stat — arg = ItemModType, amount = value
|
||||
AddByKey(acc, StatKeyForItemModType(static_cast<uint32_t>(arg[i])),
|
||||
sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
default: // 1 proc / 2 weapon-damage / 6 totem / 7 use spell
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AccumulateRecord(Accum *acc, const uint8_t *record, long sign) {
|
||||
if (record == nullptr)
|
||||
return;
|
||||
auto *types = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_STAT_SLOT_COUNT; ++i)
|
||||
AddByKey(acc, StatKeyForItemModType(types[i]),
|
||||
sign * static_cast<long>(values[i]));
|
||||
|
||||
AddByKey(acc, "RESISTANCE0_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_ARMOR));
|
||||
AddByKey(acc, "RESISTANCE1_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_HOLY));
|
||||
AddByKey(acc, "RESISTANCE2_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_FIRE));
|
||||
AddByKey(acc, "RESISTANCE3_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_NATURE));
|
||||
AddByKey(acc, "RESISTANCE4_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_FROST));
|
||||
AddByKey(acc, "RESISTANCE5_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_SHADOW));
|
||||
AddByKey(acc, "RESISTANCE6_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_ARCANE));
|
||||
|
||||
// Shield block value is a stored field, not a stat slot or aura.
|
||||
AddByKey(acc, "ITEM_MOD_BLOCK_VALUE", sign * U32(record, Offsets::OFF_ITEMSTATS_BLOCK));
|
||||
|
||||
AddEquipSpellStats(acc, record, sign);
|
||||
}
|
||||
|
||||
int ParseRandomSuffixFromLink(void *L, int idx) {
|
||||
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_STRING)
|
||||
return 0;
|
||||
const char *s = Game::Lua::ToString(L, idx);
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
const char *m = std::strstr(s, "item:");
|
||||
if (m == nullptr)
|
||||
return 0;
|
||||
m += 5; // start of field 0 (itemID)
|
||||
// Advance to the start of field 2 (past two ':' separators), stopping
|
||||
// at the end of the hyperlink payload if the link is too short.
|
||||
int colons = 0;
|
||||
while (*m != '\0' && *m != '|') {
|
||||
if (*m == ':') {
|
||||
if (++colons == 2) {
|
||||
++m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
++m;
|
||||
}
|
||||
if (colons < 2)
|
||||
return 0;
|
||||
return std::atoi(m);
|
||||
}
|
||||
|
||||
} // namespace Item::StatAccum
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared item-stat accumulation engine. Reads an item's stats (base
|
||||
// record + on-equip spell auras + random-suffix enchants) into a
|
||||
// fixed key→value table keyed by FrameXML global-string names
|
||||
// (`ITEM_MOD_*`, `RESISTANCE*_NAME`). Two consumers:
|
||||
// - `Item::Stats` — the `C_Item.GetItemStats` / `GetItemStatDelta`
|
||||
// Lua bindings, which turn an `Accum` into a Lua table.
|
||||
// - `Tooltip::Compare` — `GameTooltip:SetHyperlinkCompareItem`, which
|
||||
// diffs a hovered item against the equipped item and renders the
|
||||
// per-stat delta as colored tooltip lines.
|
||||
//
|
||||
// The full rationale for each stat source and the aura→key map lives in
|
||||
// [src/item/StatAccum.cpp]; see also the "Item lookups" notes in
|
||||
// CLAUDE.md for the `ItemStats_C` record shape.
|
||||
namespace Item::StatAccum {
|
||||
|
||||
// One key→value pair. `key` is a FrameXML global-string name; `value`
|
||||
// is the accumulated magnitude (native vanilla units — percents stay
|
||||
// percents, not ratings).
|
||||
struct Accum {
|
||||
const char *key;
|
||||
long value;
|
||||
};
|
||||
|
||||
// Number of distinct keys an item can touch (base stats + armor + six
|
||||
// resistances + the equip-spell "special" stats). Compile-time so
|
||||
// callers can stack-allocate `Accum acc[kCount]`.
|
||||
inline constexpr int kCount = 30;
|
||||
|
||||
// Fill `acc` (kCount entries) with the canonical key list and zero
|
||||
// values. Call once before accumulating.
|
||||
void Init(Accum *acc);
|
||||
|
||||
// Add `delta` to the entry matching `key` (no-op for an unknown key or
|
||||
// a zero delta). Linear scan — the table is small and fixed.
|
||||
void AddByKey(Accum *acc, const char *key, long delta);
|
||||
|
||||
// Passive cache peek for an item's `ItemStats_C` record. Does NOT warm
|
||||
// the cache (returns nullptr for an uncached item) — see the item-cache
|
||||
// race note in CLAUDE.md.
|
||||
const uint8_t *FetchRecord(uint32_t itemID);
|
||||
|
||||
// Fold an item record's base stats + on-equip-spell stats into `acc`,
|
||||
// scaled by `sign` (+1 to add, -1 to subtract for a delta). No-op for a
|
||||
// null record.
|
||||
void AccumulateRecord(Accum *acc, const uint8_t *record, long sign);
|
||||
|
||||
// Fold an item link's random-suffix bonus (ItemRandomProperties.dbc →
|
||||
// SpellItemEnchantment.dbc) into `acc`, scaled by `sign`. No-op for
|
||||
// suffixID <= 0.
|
||||
void ApplyRandomSuffix(Accum *acc, int suffixID, long sign);
|
||||
|
||||
// Weapon DPS = average damage / swing time, or 0 for non-weapons.
|
||||
double ComputeDPS(const uint8_t *record);
|
||||
|
||||
// Parse the random-property/suffix ID from the item link at 1-based Lua
|
||||
// stack `idx` (`item:id:enchant:SUFFIX:unique`). 0 for a non-string
|
||||
// arg, a bare `item:N`, or no `item:` payload.
|
||||
int ParseRandomSuffixFromLink(void *L, int idx);
|
||||
|
||||
} // namespace Item::StatAccum
|
||||
+25
-386
@@ -17,383 +17,22 @@
|
||||
// uses (so `_G[key]` yields the display label). `GetItemStatDelta` is the
|
||||
// per-stat difference of two items.
|
||||
//
|
||||
// Three sources feed the table:
|
||||
// 1. The base `ItemStats_C` record — the stored attributes
|
||||
// (Str/Agi/Sta/Int/Spi, mana/health-on-equip), armor, six resists,
|
||||
// and (for weapons) DPS derived from the damage/delay fields.
|
||||
// 2. The item's on-equip spells (`m_spellTrigger == 1`) — vanilla
|
||||
// stores "special" bonuses (crit, attack power, spell power, hit,
|
||||
// mp5, defense, …) as an equip spell whose aura effect carries the
|
||||
// value, not as a stat slot. We decode those auras.
|
||||
// 3. The link's random suffix (`item:id:enchant:SUFFIX:unique`) —
|
||||
// ItemRandomProperties.dbc → SpellItemEnchantment.dbc, whose stat
|
||||
// suffixes are themselves equip spells (enchant type 3) and whose
|
||||
// armor suffixes are direct resistance enchants (type 4).
|
||||
//
|
||||
// **Values are vanilla-native.** The percent-based stats (crit / hit /
|
||||
// defense) are reported as the raw vanilla percentage under the modern
|
||||
// rating key — e.g. Krol Blade's "+1% crit" is
|
||||
// `ITEM_MOD_CRIT_MELEE_RATING = 1`, NOT the level-scaled rating (13) that
|
||||
// TBC/Era would show. Vanilla has no rating conversion (it's
|
||||
// level-dependent and doesn't exist in this client), so a native percent
|
||||
// is the only honest, level-independent value.
|
||||
//
|
||||
// The aura → key map (below) covers the common gear stats. Rarer auras
|
||||
// (percent attack power, weapon-skill-per-line, damage-taken, procs) are
|
||||
// intentionally skipped — they either lack a clean single key or aren't
|
||||
// part of the stat set.
|
||||
// The stat derivation (base record + on-equip spell auras + random
|
||||
// suffix) lives in the shared `Item::StatAccum` engine
|
||||
// ([src/item/StatAccum.cpp]); this file is just the Lua bindings that
|
||||
// turn an accumulator into a returned table.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/StatAccum.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace Item::Stats {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
// Passive peek — same contract as GetItemData / GetItemInfoInstant. Does
|
||||
// not warm the cache (callback = nullptr, requestIfMissing = false), so
|
||||
// an uncached item reads back as nullptr rather than orphaning an entry
|
||||
// (see the item-cache race note in CLAUDE.md).
|
||||
const uint8_t *FetchItemRecord(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, false);
|
||||
}
|
||||
|
||||
// Base-record m_statType uses the ItemModType enum: 0=mana, 1=health,
|
||||
// 3=agility, 4=strength, 5=intellect, 6=spirit, 7=stamina (2 is
|
||||
// deprecated). Returns nullptr for types with no vanilla key.
|
||||
const char *StatKeyForItemModType(uint32_t statType) {
|
||||
switch (statType) {
|
||||
case 0: return "ITEM_MOD_MANA_SHORT";
|
||||
case 1: return "ITEM_MOD_HEALTH_SHORT";
|
||||
case 3: return "ITEM_MOD_AGILITY_SHORT";
|
||||
case 4: return "ITEM_MOD_STRENGTH_SHORT";
|
||||
case 5: return "ITEM_MOD_INTELLECT_SHORT";
|
||||
case 6: return "ITEM_MOD_SPIRIT_SHORT";
|
||||
case 7: return "ITEM_MOD_STAMINA_SHORT";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// SPELL_AURA_MOD_STAT (29) misc uses the UnitStat enum instead: 0=Str,
|
||||
// 1=Agi, 2=Sta, 3=Int, 4=Spi (-1 = all stats, not a single key).
|
||||
// Verified against the "Increased <stat> 01" random-property spells.
|
||||
const char *StatKeyForUnitStat(int stat) {
|
||||
switch (stat) {
|
||||
case 0: return "ITEM_MOD_STRENGTH_SHORT";
|
||||
case 1: return "ITEM_MOD_AGILITY_SHORT";
|
||||
case 2: return "ITEM_MOD_STAMINA_SHORT";
|
||||
case 3: return "ITEM_MOD_INTELLECT_SHORT";
|
||||
case 4: return "ITEM_MOD_SPIRIT_SHORT";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Resistance school index → key. 0 = armor (physical), 1..6 = the magic
|
||||
// schools in Holy/Fire/Nature/Frost/Shadow/Arcane order.
|
||||
const char *ResistKey(int school) {
|
||||
static const char *const kKeys[7] = {
|
||||
"RESISTANCE0_NAME", "RESISTANCE1_NAME", "RESISTANCE2_NAME",
|
||||
"RESISTANCE3_NAME", "RESISTANCE4_NAME", "RESISTANCE5_NAME",
|
||||
"RESISTANCE6_NAME",
|
||||
};
|
||||
return (school >= 0 && school <= 6) ? kKeys[school] : nullptr;
|
||||
}
|
||||
|
||||
// Accumulator over every key an item can touch: the base stats + armor +
|
||||
// six resistances, plus the equip-spell "special" stats. Fixed and
|
||||
// small — linear scan by key is fine.
|
||||
struct Accum {
|
||||
const char *key;
|
||||
long value;
|
||||
};
|
||||
|
||||
// Keys in this list are the ones AddByKey / Accumulate ever write. The
|
||||
// equip-spell keys past the resistances are best-effort names to verify
|
||||
// against a 1.15.x client (crit melee/ranged are confirmed from a TBC
|
||||
// GetItemStats dump); adjust here if the live client differs.
|
||||
constexpr const char *kKeys[] = {
|
||||
"ITEM_MOD_MANA_SHORT",
|
||||
"ITEM_MOD_HEALTH_SHORT",
|
||||
"ITEM_MOD_AGILITY_SHORT",
|
||||
"ITEM_MOD_STRENGTH_SHORT",
|
||||
"ITEM_MOD_INTELLECT_SHORT",
|
||||
"ITEM_MOD_SPIRIT_SHORT",
|
||||
"ITEM_MOD_STAMINA_SHORT",
|
||||
"RESISTANCE0_NAME",
|
||||
"RESISTANCE1_NAME",
|
||||
"RESISTANCE2_NAME",
|
||||
"RESISTANCE3_NAME",
|
||||
"RESISTANCE4_NAME",
|
||||
"RESISTANCE5_NAME",
|
||||
"RESISTANCE6_NAME",
|
||||
"ITEM_MOD_ATTACK_POWER_SHORT",
|
||||
"ITEM_MOD_RANGED_ATTACK_POWER_SHORT",
|
||||
"ITEM_MOD_CRIT_MELEE_RATING",
|
||||
"ITEM_MOD_CRIT_RANGED_RATING",
|
||||
"ITEM_MOD_HIT_MELEE_RATING",
|
||||
"ITEM_MOD_HIT_RANGED_RATING",
|
||||
"ITEM_MOD_HIT_SPELL_RATING",
|
||||
"ITEM_MOD_SPELL_DAMAGE_DONE_SHORT",
|
||||
"ITEM_MOD_SPELL_HEALING_DONE_SHORT",
|
||||
"ITEM_MOD_MANA_REGENERATION",
|
||||
"ITEM_MOD_DEFENSE_SKILL_RATING",
|
||||
};
|
||||
constexpr int kAccumCount = static_cast<int>(sizeof(kKeys) / sizeof(kKeys[0]));
|
||||
|
||||
void InitAccum(Accum *acc) {
|
||||
for (int i = 0; i < kAccumCount; ++i) {
|
||||
acc[i].key = kKeys[i];
|
||||
acc[i].value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void AddByKey(Accum *acc, const char *key, long delta) {
|
||||
if (key == nullptr || delta == 0)
|
||||
return;
|
||||
for (int i = 0; i < kAccumCount; ++i)
|
||||
if (std::strcmp(acc[i].key, key) == 0) {
|
||||
acc[i].value += delta;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
long U32(const uint8_t *record, int offset) {
|
||||
return static_cast<long>(*reinterpret_cast<const uint32_t *>(record + offset));
|
||||
}
|
||||
|
||||
// Weapon DPS = average damage across all damage slots / swing time. The
|
||||
// record stores min/max as floats per slot (vanilla weapons use slot 0,
|
||||
// occasionally a second for a split physical/elemental line) and m_delay
|
||||
// in milliseconds. Returns 0 for non-weapons (no delay or no damage),
|
||||
// which the callers treat as "omit the key" — mirrors 3.3.5's range
|
||||
// guard on `ITEM_MOD_DAMAGE_PER_SECOND_SHORT`.
|
||||
double ComputeDPS(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return 0.0;
|
||||
const uint32_t delayMs = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DELAY);
|
||||
if (delayMs == 0)
|
||||
return 0.0;
|
||||
auto *dmgMin = reinterpret_cast<const float *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DAMAGE_MIN);
|
||||
auto *dmgMax = reinterpret_cast<const float *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DAMAGE_MAX);
|
||||
double avgTotal = 0.0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT; ++i)
|
||||
avgTotal += (static_cast<double>(dmgMin[i]) +
|
||||
static_cast<double>(dmgMax[i])) * 0.5;
|
||||
if (avgTotal <= 0.0)
|
||||
return 0.0;
|
||||
return avgTotal / (static_cast<double>(delayMs) / 1000.0);
|
||||
}
|
||||
|
||||
// Folds a spell's stat/resistance auras into `acc`. Used for both the
|
||||
// item's own on-equip spells and random-suffix equip enchants. The value
|
||||
// is the fixed-die `EffectBasePoints + 1` (all item-stat spells store a
|
||||
// single fixed magnitude that way). Percent stats keep their vanilla
|
||||
// percentage. Auras with no clean stat key are ignored (procs, dummies,
|
||||
// weapon-skill-per-line, damage-taken, etc.).
|
||||
void AddSpellStatAuras(Accum *acc, int spellID, long sign) {
|
||||
if (spellID <= 0)
|
||||
return;
|
||||
const uint8_t *sp = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (sp == nullptr)
|
||||
return;
|
||||
auto aura = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto misc = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE);
|
||||
auto base = reinterpret_cast<const int32_t *>(
|
||||
sp + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS);
|
||||
|
||||
// Vanilla "+N Attack Power" spells carry a melee (99) AND a ranged
|
||||
// (124) aura with the same value; the generic melee key subsumes the
|
||||
// ranged one, so we emit ranged only for ranged-only spells (scopes).
|
||||
bool hasMeleeAP = false;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == 99)
|
||||
hasMeleeAP = true;
|
||||
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const long v = static_cast<long>(base[i]) + 1;
|
||||
switch (aura[i]) {
|
||||
case 29: // SPELL_AURA_MOD_STAT — misc = UnitStat index
|
||||
AddByKey(acc, StatKeyForUnitStat(misc[i]), sign * v);
|
||||
break;
|
||||
case 22: // SPELL_AURA_MOD_RESISTANCE — misc = school bitmask
|
||||
for (int school = 0; school <= 6; ++school)
|
||||
if (misc[i] & (1 << school))
|
||||
AddByKey(acc, ResistKey(school), sign * v);
|
||||
break;
|
||||
case 99: // SPELL_AURA_MOD_ATTACK_POWER
|
||||
AddByKey(acc, "ITEM_MOD_ATTACK_POWER_SHORT", sign * v);
|
||||
break;
|
||||
case 124: // SPELL_AURA_MOD_RANGED_ATTACK_POWER
|
||||
if (!hasMeleeAP)
|
||||
AddByKey(acc, "ITEM_MOD_RANGED_ATTACK_POWER_SHORT", sign * v);
|
||||
break;
|
||||
case 52: // weapon crit % — TBC surfaces it as melee AND ranged
|
||||
AddByKey(acc, "ITEM_MOD_CRIT_MELEE_RATING", sign * v);
|
||||
AddByKey(acc, "ITEM_MOD_CRIT_RANGED_RATING", sign * v);
|
||||
break;
|
||||
case 54: // melee/ranged hit %
|
||||
AddByKey(acc, "ITEM_MOD_HIT_MELEE_RATING", sign * v);
|
||||
AddByKey(acc, "ITEM_MOD_HIT_RANGED_RATING", sign * v);
|
||||
break;
|
||||
case 55: // spell hit %
|
||||
AddByKey(acc, "ITEM_MOD_HIT_SPELL_RATING", sign * v);
|
||||
break;
|
||||
case 13: // SPELL_AURA_MOD_DAMAGE_DONE — misc = school mask
|
||||
if (misc[i] & 0x7E) // any magic school (bits 1..6)
|
||||
AddByKey(acc, "ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", sign * v);
|
||||
break;
|
||||
case 135: // SPELL_AURA_MOD_HEALING_DONE
|
||||
AddByKey(acc, "ITEM_MOD_SPELL_HEALING_DONE_SHORT", sign * v);
|
||||
break;
|
||||
case 85: // SPELL_AURA_MOD_POWER_REGEN (mp5) — misc = power type
|
||||
if (misc[i] == 0) // mana
|
||||
AddByKey(acc, "ITEM_MOD_MANA_REGENERATION", sign * v);
|
||||
break;
|
||||
case 30: // SPELL_AURA_MOD_SKILL — misc = skill line (95 = Defense)
|
||||
if (misc[i] == 95)
|
||||
AddByKey(acc, "ITEM_MOD_DEFENSE_SKILL_RATING", sign * v);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes the item's own on-equip spells (SpellTrigger == 1) into `acc`.
|
||||
// On-use (0) and chance-on-hit (2) spells are skipped — they aren't
|
||||
// passive stats. This is what surfaces crit / AP / spell power / hit /
|
||||
// mp5 / defense on base items (vanilla stores them as equip spells, not
|
||||
// stat slots).
|
||||
void AddEquipSpellStats(Accum *acc, const uint8_t *record, long sign) {
|
||||
auto ids = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i)
|
||||
if (ids[i] != 0 && triggers[i] == 1) // ON_EQUIP
|
||||
AddSpellStatAuras(acc, static_cast<int>(ids[i]), sign);
|
||||
}
|
||||
|
||||
// Applies the item's random-suffix bonus (link field 2 → suffixID) to
|
||||
// `acc`. Walks ItemRandomProperties.dbc → its SpellItemEnchantment IDs;
|
||||
// type 3 enchants defer to the equip spell's auras, type 4 add a direct
|
||||
// resistance, type 5 a direct stat. No-op for suffixID <= 0.
|
||||
void ApplyRandomSuffix(Accum *acc, int suffixID, long sign) {
|
||||
if (suffixID <= 0)
|
||||
return;
|
||||
const uint8_t *rp = DBC::Record(Offsets::VAR_ITEMRANDOMPROP_RECORDS,
|
||||
Offsets::VAR_ITEMRANDOMPROP_COUNT,
|
||||
static_cast<uint32_t>(suffixID));
|
||||
if (rp == nullptr)
|
||||
return;
|
||||
auto enchants = reinterpret_cast<const uint32_t *>(
|
||||
rp + Offsets::OFF_ITEMRANDOMPROP_ENCHANT);
|
||||
for (int s = 0; s < Offsets::ITEMRANDOMPROP_ENCHANT_SLOT_COUNT; ++s) {
|
||||
const uint32_t enchantID = enchants[s];
|
||||
if (enchantID == 0)
|
||||
continue;
|
||||
const uint8_t *en = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
|
||||
Offsets::VAR_SPELLITEMENCHANT_COUNT, enchantID);
|
||||
if (en == nullptr)
|
||||
continue;
|
||||
auto type = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_TYPE);
|
||||
auto amount = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
|
||||
auto arg = reinterpret_cast<const int32_t *>(en + Offsets::OFF_SPELLITEMENCHANT_ARG);
|
||||
for (int i = 0; i < 3; ++i) { // SpellItemEnchantment has 3 effect slots
|
||||
switch (type[i]) {
|
||||
case 3: // equip spell — stat/resist value lives in the spell
|
||||
AddSpellStatAuras(acc, arg[i], sign);
|
||||
break;
|
||||
case 4: // resistance — arg = school index (0 = armor)
|
||||
AddByKey(acc, ResistKey(arg[i]), sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
case 5: // direct stat — arg = ItemModType, amount = value
|
||||
AddByKey(acc, StatKeyForItemModType(static_cast<uint32_t>(arg[i])),
|
||||
sign * static_cast<long>(amount[i]));
|
||||
break;
|
||||
default: // 1 proc / 2 weapon-damage / 6 totem / 7 use spell
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds item `record`'s base-record stats + on-equip-spell stats to `acc`
|
||||
// scaled by `sign` (+1 to add, -1 to subtract — the delta path adds
|
||||
// item2 then subtracts item1).
|
||||
void Accumulate(Accum *acc, const uint8_t *record, long sign) {
|
||||
if (record == nullptr)
|
||||
return;
|
||||
auto *types = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_STAT_SLOT_COUNT; ++i)
|
||||
AddByKey(acc, StatKeyForItemModType(types[i]),
|
||||
sign * static_cast<long>(values[i]));
|
||||
|
||||
AddByKey(acc, "RESISTANCE0_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_ARMOR));
|
||||
AddByKey(acc, "RESISTANCE1_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_HOLY));
|
||||
AddByKey(acc, "RESISTANCE2_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_FIRE));
|
||||
AddByKey(acc, "RESISTANCE3_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_NATURE));
|
||||
AddByKey(acc, "RESISTANCE4_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_FROST));
|
||||
AddByKey(acc, "RESISTANCE5_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_SHADOW));
|
||||
AddByKey(acc, "RESISTANCE6_NAME", sign * U32(record, Offsets::OFF_ITEMSTATS_RES_ARCANE));
|
||||
|
||||
AddEquipSpellStats(acc, record, sign);
|
||||
}
|
||||
|
||||
// Parses the random-property/suffix ID from an item link's third field
|
||||
// (`item:id:enchant:SUFFIX:unique`). Returns 0 for a non-string arg, a
|
||||
// link with no `item:` payload, or a short link that omits the field
|
||||
// (bare `item:N`, `item:N:E`). The engine's own link format is the
|
||||
// four-field `item:%d:%d:%d:%d`, so the suffix is always field index 2.
|
||||
int ParseRandomSuffix(void *L, int idx) {
|
||||
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_STRING)
|
||||
return 0;
|
||||
const char *s = Game::Lua::ToString(L, idx);
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
const char *m = std::strstr(s, "item:");
|
||||
if (m == nullptr)
|
||||
return 0;
|
||||
m += 5; // start of field 0 (itemID)
|
||||
// Advance to the start of field 2 (past two ':' separators), stopping
|
||||
// at the end of the hyperlink payload if the link is too short.
|
||||
int colons = 0;
|
||||
while (*m != '\0' && *m != '|') {
|
||||
if (*m == ':') {
|
||||
if (++colons == 2) {
|
||||
++m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
++m;
|
||||
}
|
||||
if (colons < 2)
|
||||
return 0;
|
||||
return std::atoi(m);
|
||||
}
|
||||
using StatAccum::Accum;
|
||||
|
||||
// Pushes a fresh Lua table with each non-zero accumulator entry as
|
||||
// `t[key] = value`, plus the weapon DPS under
|
||||
@@ -405,7 +44,7 @@ int ParseRandomSuffix(void *L, int idx) {
|
||||
// noise producing a spurious near-zero delta.
|
||||
void PushAccumTable(void *L, const Accum *acc, double dps) {
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < kAccumCount; ++i)
|
||||
for (int i = 0; i < StatAccum::kCount; ++i)
|
||||
if (acc[i].value != 0)
|
||||
Game::Lua::SetFieldNumber(L, acc[i].key,
|
||||
static_cast<double>(acc[i].value));
|
||||
@@ -430,17 +69,17 @@ int __fastcall Script_GetItemStats(void *L) {
|
||||
Game::Lua::Error(L, "Usage: C_Item.GetItemStats(itemLink)");
|
||||
return 0;
|
||||
}
|
||||
const int suffixID = ParseRandomSuffix(L, 1);
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
const int suffixID = StatAccum::ParseRandomSuffixFromLink(L, 1);
|
||||
const uint8_t *record = StatAccum::FetchRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Accum acc[kAccumCount];
|
||||
InitAccum(acc);
|
||||
Accumulate(acc, record, +1);
|
||||
ApplyRandomSuffix(acc, suffixID, +1);
|
||||
PushAccumTable(L, acc, ComputeDPS(record));
|
||||
Accum acc[StatAccum::kCount];
|
||||
StatAccum::Init(acc);
|
||||
StatAccum::AccumulateRecord(acc, record, +1);
|
||||
StatAccum::ApplyRandomSuffix(acc, suffixID, +1);
|
||||
PushAccumTable(L, acc, StatAccum::ComputeDPS(record));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -458,21 +97,21 @@ int __fastcall Script_GetItemStatDelta(void *L) {
|
||||
L, "Usage: C_Item.GetItemStatDelta(itemLink1, itemLink2)");
|
||||
return 0;
|
||||
}
|
||||
const int suffix1 = ParseRandomSuffix(L, 1);
|
||||
const int suffix2 = ParseRandomSuffix(L, 2);
|
||||
const uint8_t *r1 = FetchItemRecord(static_cast<uint32_t>(id1));
|
||||
const uint8_t *r2 = FetchItemRecord(static_cast<uint32_t>(id2));
|
||||
const int suffix1 = StatAccum::ParseRandomSuffixFromLink(L, 1);
|
||||
const int suffix2 = StatAccum::ParseRandomSuffixFromLink(L, 2);
|
||||
const uint8_t *r1 = StatAccum::FetchRecord(static_cast<uint32_t>(id1));
|
||||
const uint8_t *r2 = StatAccum::FetchRecord(static_cast<uint32_t>(id2));
|
||||
if (r1 == nullptr || r2 == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Accum acc[kAccumCount];
|
||||
InitAccum(acc);
|
||||
Accumulate(acc, r2, +1);
|
||||
ApplyRandomSuffix(acc, suffix2, +1);
|
||||
Accumulate(acc, r1, -1);
|
||||
ApplyRandomSuffix(acc, suffix1, -1);
|
||||
PushAccumTable(L, acc, ComputeDPS(r2) - ComputeDPS(r1));
|
||||
Accum acc[StatAccum::kCount];
|
||||
StatAccum::Init(acc);
|
||||
StatAccum::AccumulateRecord(acc, r2, +1);
|
||||
StatAccum::ApplyRandomSuffix(acc, suffix2, +1);
|
||||
StatAccum::AccumulateRecord(acc, r1, -1);
|
||||
StatAccum::ApplyRandomSuffix(acc, suffix1, -1);
|
||||
PushAccumTable(L, acc, StatAccum::ComputeDPS(r2) - StatAccum::ComputeDPS(r1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
+31
-22
@@ -18,6 +18,7 @@
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/QualityColor.h"
|
||||
#include "item/TooltipItem.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -170,14 +171,15 @@ static void *ResolveItemByGuid(uint32_t guidLo, uint32_t guidHi) {
|
||||
// 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.)
|
||||
// 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:
|
||||
@@ -203,14 +205,15 @@ static int __fastcall Script_GameTooltipGetItem(void *L) {
|
||||
return 0;
|
||||
auto *base = static_cast<const uint8_t *>(tooltipObj);
|
||||
|
||||
const int itemID = *reinterpret_cast<const int *>(base + Offsets::OFF_TOOLTIP_ITEM_ID);
|
||||
// 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 = *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);
|
||||
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)) {
|
||||
@@ -243,9 +246,13 @@ static int __fastcall Script_GameTooltipGetItem(void *L) {
|
||||
// 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.
|
||||
// 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));
|
||||
@@ -258,11 +265,15 @@ static int __fastcall Script_GameTooltipGetItem(void *L) {
|
||||
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:0:0:0:0:0:0|h[%s]|h|r",
|
||||
"%s|Hitem:%d:0:%d:0:0:0:0:0|h[%s]|h|r",
|
||||
Item::QualityColor::Prefix(static_cast<int>(quality)),
|
||||
itemID, name);
|
||||
itemID, suffix, name);
|
||||
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::PushString(L, link);
|
||||
@@ -285,9 +296,7 @@ static int __fastcall Script_GameTooltipHasItem(void *L) {
|
||||
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);
|
||||
Game::Lua::PushBoolean(L, Item::TooltipItem::CurrentID(tooltipObj, nullptr) > 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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 "item/TooltipItem.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
namespace Item::TooltipItem {
|
||||
|
||||
int CurrentID(const void *tooltipObj, uint64_t *outGuid) {
|
||||
if (outGuid != nullptr)
|
||||
*outGuid = 0;
|
||||
if (tooltipObj == nullptr)
|
||||
return 0;
|
||||
auto *c = static_cast<const uint8_t *>(tooltipObj);
|
||||
|
||||
const uint64_t itemGuid =
|
||||
*reinterpret_cast<const uint64_t *>(c + Offsets::OFF_TOOLTIP_ITEM_GUID_LO);
|
||||
|
||||
// Link item: the itemID field is authoritative (and reliably cleared).
|
||||
const int linkItemID = *reinterpret_cast<const int *>(c + Offsets::OFF_TOOLTIP_ITEM_ID);
|
||||
if (linkItemID > 0) {
|
||||
if (outGuid != nullptr)
|
||||
*outGuid = itemGuid;
|
||||
return linkItemID;
|
||||
}
|
||||
|
||||
// CGItem item: only the GUID is stored, and +0x380 isn't zeroed by the
|
||||
// clear — so it lingers after the tooltip switches to a unit / GO /
|
||||
// spell. Each of those sets its own (cleared-then-written) id, so trust
|
||||
// the item GUID only when none of them is currently shown, then resolve
|
||||
// it to the itemID (this also yields the dressed link for callers).
|
||||
if (itemGuid == 0)
|
||||
return 0;
|
||||
if (*reinterpret_cast<const uint64_t *>(c + Offsets::OFF_TOOLTIP_UNIT_GUID_LO) != 0)
|
||||
return 0;
|
||||
if (*reinterpret_cast<const uint64_t *>(c + Offsets::OFF_TOOLTIP_GAMEOBJECT_GUID_LO) != 0)
|
||||
return 0;
|
||||
if (*reinterpret_cast<const int *>(c + Offsets::OFF_TOOLTIP_SPELL_ID) > 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *cg = Item::Location::ResolveByGUID(itemGuid);
|
||||
if (cg == nullptr)
|
||||
return 0;
|
||||
const int id = Item::ID::FromCGItem(cg);
|
||||
if (id <= 0)
|
||||
return 0;
|
||||
if (outGuid != nullptr)
|
||||
*outGuid = itemGuid;
|
||||
return id;
|
||||
}
|
||||
|
||||
} // namespace Item::TooltipItem
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::TooltipItem {
|
||||
|
||||
// The itemID a tooltip is currently displaying, or 0 when it isn't
|
||||
// showing an item. Two storage paths:
|
||||
// - link paths (SetItemByID / SetHyperlink) set the itemID at
|
||||
// OFF_TOOLTIP_ITEM_ID (+0x398), which the per-tooltip clear zeroes;
|
||||
// - CGItem paths (SetInventoryItem / SetBagItem / SetItemByGUID) set the
|
||||
// instance GUID at OFF_TOOLTIP_ITEM_GUID_LO (+0x380), which the clear
|
||||
// does NOT zero — so it goes stale once the tooltip moves on to a unit
|
||||
// / gameobject / spell.
|
||||
// We trust the itemID directly, and otherwise resolve the GUID — but only
|
||||
// when no unit / gameobject / spell id is currently set (each of those IS
|
||||
// cleared-and-set on show, so their presence means the lingering item GUID
|
||||
// isn't the live content). `outGuid` (optional) receives the item GUID (0
|
||||
// when none), which lets callers build the dressed (suffixed) link.
|
||||
// Returns 0 for a null tooltip or any non-item tooltip.
|
||||
//
|
||||
// Note: auction tooltips (SetAuctionItem) stash the itemID in a separate
|
||||
// descriptor (+0x3D0) with no CGItem/GUID, so no dressed link is
|
||||
// recoverable from them — we deliberately don't surface those here rather
|
||||
// than return a suffix-stripped link.
|
||||
int CurrentID(const void *tooltipObj, uint64_t *outGuid);
|
||||
|
||||
} // namespace Item::TooltipItem
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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/>.
|
||||
|
||||
// Completes the per-tooltip clear so the item GUID doesn't go stale.
|
||||
//
|
||||
// The engine's tooltip clear FUN_GAMETOOLTIP_CLEAR (FUN_00530050) zeroes the
|
||||
// unit (+0x368), gameobject (+0x370), itemID (+0x398) and spell (+0x39c)
|
||||
// identity fields — but NOT the item GUID (+0x380/+0x384) that the item builder
|
||||
// writes for CGItem tooltips (SetBagItem / SetInventoryItem / …). The engine
|
||||
// never reads that GUID as "current item" (the builder always overwrites it
|
||||
// before use), so leaving it stale is harmless to the engine — but
|
||||
// GameTooltip:GetItem / HasItem / IsEquippedItem read it (via
|
||||
// Item::TooltipItem::CurrentID's GUID fallback, which exists because CGItem
|
||||
// tooltips leave the itemID field 0). So after the tooltip switches to content
|
||||
// that sets no item — SetUnitAura (→ FUN_0052f880), SetEquipmentSet (→ SetText),
|
||||
// etc. — the stale GUID made GetItem resolve the *previous* item.
|
||||
//
|
||||
// All 14 Set*/builder paths route through this clear (verified via its xrefs),
|
||||
// and the item builder calls it *before* writing the GUID, so co-hooking the
|
||||
// clear to also zero +0x380/+0x384 fixes every case uniformly: a fresh tooltip
|
||||
// always resets the item GUID, and only a real item build re-sets it. The hook
|
||||
// runs no Lua and touches nothing but those 8 bytes after the original clear —
|
||||
// none of the fragility of the OnTooltipSetItem fire.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Tooltip::ClearItemGuid {
|
||||
|
||||
namespace {
|
||||
|
||||
using Clear_t = void(__fastcall *)(void *self);
|
||||
Clear_t g_clearOriginal = nullptr;
|
||||
|
||||
void __fastcall Clear_h(void *self) {
|
||||
g_clearOriginal(self);
|
||||
if (self != nullptr)
|
||||
*reinterpret_cast<uint64_t *>(static_cast<uint8_t *>(self) +
|
||||
Offsets::OFF_TOOLTIP_ITEM_GUID_LO) = 0;
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hook{
|
||||
Offsets::FUN_GAMETOOLTIP_CLEAR,
|
||||
reinterpret_cast<void *>(&Clear_h),
|
||||
reinterpret_cast<void **>(&g_clearOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Tooltip::ClearItemGuid
|
||||
@@ -0,0 +1,552 @@
|
||||
// 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/>.
|
||||
|
||||
// `GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])`
|
||||
// — a backport of the 3.3.5+ tooltip method that powers item-comparison
|
||||
// ("shopping") tooltips. The compared item comes from `itemLink` when
|
||||
// given; otherwise it's taken from the item the `comparisonTooltip` (4th
|
||||
// positional arg — the tooltip FrameXML's GameTooltip_ShowCompareItem
|
||||
// passes) is displaying, so the method works purely tooltip-to-tooltip
|
||||
// with no link, matching the retail call. Called on a tooltip frame (typically
|
||||
// ShoppingTooltip1/2, but works on any GameTooltip-type frame), it:
|
||||
//
|
||||
// 1. Parses the hovered item link and finds which equipment slot(s)
|
||||
// that item would occupy (via its inventoryType).
|
||||
// 2. Picks the slot indicated by `offset` (1-based; two-slot items —
|
||||
// rings, trinkets, one-hand weapons — expose slots at offset 1 and
|
||||
// 2, so a caller iterates offset = 1 .. returnValue).
|
||||
// 3. Fills the tooltip with the item currently EQUIPPED in that slot,
|
||||
// led by a grey "Currently Equipped" header — done by calling the
|
||||
// engine's own item-tooltip builder (FUN_GAMETOOLTIP_BUILD_ITEM) in
|
||||
// its header mode. No Lua roundtrip.
|
||||
// 4. Appends colored per-stat delta lines — yellow ITEM_DELTA_DESCRIPTION
|
||||
// header, then green for a gain / red for a loss — showing how the
|
||||
// hovered item compares to the equipped one. The delta is
|
||||
// `hovered − equipped`, computed by the shared `Item::StatAccum`
|
||||
// engine (the same one behind `C_Item.GetItemStatDelta`); the lines
|
||||
// are added with the engine's raw add-line (FUN_GAMETOOLTIP_ADD_LINE).
|
||||
//
|
||||
// Returns the number of comparison slots for the hovered item (0 if it
|
||||
// isn't equippable, isn't cached yet, the tooltip object can't be
|
||||
// resolved, or nothing is equipped in the chosen slot) — a ClassicAPI
|
||||
// extension over the retail signature, so a caller knows whether a second
|
||||
// offset is worth querying.
|
||||
//
|
||||
// Everything is done with direct engine calls (the builder, clear, and
|
||||
// add-line functions the engine uses itself) — no `lua_pcall` into
|
||||
// registered Lua methods. The tooltip's final size/show is left to the
|
||||
// caller (matching retail `SetHyperlinkCompareItem`, which populates but
|
||||
// doesn't show). The 60-line pool from Tooltip::LinePool keeps the
|
||||
// appended deltas under the AddLine cap.
|
||||
//
|
||||
// `shiftButton`: gates the stat-change breakdown. Deltas show by default
|
||||
// (`true`, `1`, `nil`, or omitted) and are hidden only for an explicit
|
||||
// `false` or `0` — just the grey header + the equipped item's tooltip.
|
||||
// Both the boolean and the vanilla 1/nil convention are accepted —
|
||||
// mirroring FrameXML's GameTooltip_ShowCompareItem.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/StatAccum.h"
|
||||
#include "item/TooltipItem.h"
|
||||
#include "tooltip/SetItemEvent.h"
|
||||
#include "ui/ColorData.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Tooltip::Compare {
|
||||
|
||||
namespace {
|
||||
|
||||
using Item::StatAccum::Accum;
|
||||
|
||||
// The engine's item-tooltip builder. __thiscall(self = tooltip object).
|
||||
// With headerFlag != 0 it skips its leading clear and emits the grey
|
||||
// "Currently Equipped" header; the item itself is looked up from itemID +
|
||||
// guid regardless. See OFF/FUN notes in Offsets.h.
|
||||
using BuildItemTooltip_t = void(__thiscall *)(void *self, uint32_t itemID, const void *guid,
|
||||
const void *guid2, int a4, int a5, int a6,
|
||||
int headerFlag, int a8, int a9);
|
||||
// Raw add-line. Color args are pointers to a 4-byte packed {b,g,r,a}.
|
||||
using AddLine_t = void(__thiscall *)(void *self, const char *left, const char *right,
|
||||
const void *leftColor, const void *rightColor, int wrap);
|
||||
using SetLineText_t = void(__thiscall *)(void *fontString, const char *text, int flag);
|
||||
using SetLineColor_t = void(__thiscall *)(void *fontString, const void *colorBGRA);
|
||||
|
||||
// inventoryType (ItemStats_C m_inventoryType) → equipment slot ID(s).
|
||||
// Slot IDs are the engine's canonical INVSLOT values (1 = Head … 19 =
|
||||
// Tabard). Two-slot types (finger, trinket, one-hand weapon) fill both
|
||||
// entries and return 2. Returns 0 for non-equippable types (bag, ammo,
|
||||
// quiver, non-equip). `out` must hold 2 ints.
|
||||
int SlotsForInvType(int invType, int *out) {
|
||||
switch (invType) {
|
||||
case 1: out[0] = 1; return 1; // HEAD
|
||||
case 2: out[0] = 2; return 1; // NECK
|
||||
case 3: out[0] = 3; return 1; // SHOULDER
|
||||
case 4: out[0] = 4; return 1; // BODY (shirt)
|
||||
case 5: case 20: out[0] = 5; return 1; // CHEST / ROBE
|
||||
case 6: out[0] = 6; return 1; // WAIST
|
||||
case 7: out[0] = 7; return 1; // LEGS
|
||||
case 8: out[0] = 8; return 1; // FEET
|
||||
case 9: out[0] = 9; return 1; // WRIST
|
||||
case 10: out[0] = 10; return 1; // HANDS
|
||||
case 11: out[0] = 11; out[1] = 12; return 2; // FINGER
|
||||
case 12: out[0] = 13; out[1] = 14; return 2; // TRINKET
|
||||
case 16: out[0] = 15; return 1; // CLOAK (back)
|
||||
case 13: out[0] = 16; out[1] = 17; return 2; // ONE-HAND WEAPON (main/off)
|
||||
case 17: out[0] = 16; return 1; // TWO-HAND WEAPON
|
||||
case 21: out[0] = 16; return 1; // WEAPONMAINHAND
|
||||
case 14: out[0] = 17; return 1; // SHIELD (off hand)
|
||||
case 22: out[0] = 17; return 1; // WEAPONOFFHAND
|
||||
case 23: out[0] = 17; return 1; // HOLDABLE (off hand)
|
||||
case 15: out[0] = 18; return 1; // RANGED
|
||||
case 25: out[0] = 18; return 1; // THROWN
|
||||
case 26: out[0] = 18; return 1; // RANGEDRIGHT (wands)
|
||||
case 28: out[0] = 18; return 1; // RELIC
|
||||
case 19: out[0] = 19; return 1; // TABARD
|
||||
default: return 0; // 0 non-equip, 18 bag, 24 ammo, 27 quiver
|
||||
}
|
||||
}
|
||||
|
||||
// English fallback labels, used when the client doesn't define the
|
||||
// modern `_G[key]` global-string (vanilla 1.12 largely doesn't). Phrasing
|
||||
// mirrors pfUI's eqcompare so the deltas read consistently on this
|
||||
// client. Percent stats keep a "%" suffix since the values are native
|
||||
// vanilla percentages.
|
||||
struct KeyLabel {
|
||||
const char *key;
|
||||
const char *label;
|
||||
};
|
||||
constexpr KeyLabel kFallbackLabels[] = {
|
||||
{"ITEM_MOD_MANA_SHORT", "Mana"},
|
||||
{"ITEM_MOD_HEALTH_SHORT", "Health"},
|
||||
{"ITEM_MOD_AGILITY_SHORT", "Agility"},
|
||||
{"ITEM_MOD_STRENGTH_SHORT", "Strength"},
|
||||
{"ITEM_MOD_INTELLECT_SHORT", "Intellect"},
|
||||
{"ITEM_MOD_SPIRIT_SHORT", "Spirit"},
|
||||
{"ITEM_MOD_STAMINA_SHORT", "Stamina"},
|
||||
{"RESISTANCE0_NAME", "Armor"},
|
||||
{"RESISTANCE1_NAME", "Holy Resistance"},
|
||||
{"RESISTANCE2_NAME", "Fire Resistance"},
|
||||
{"RESISTANCE3_NAME", "Nature Resistance"},
|
||||
{"RESISTANCE4_NAME", "Frost Resistance"},
|
||||
{"RESISTANCE5_NAME", "Shadow Resistance"},
|
||||
{"RESISTANCE6_NAME", "Arcane Resistance"},
|
||||
{"ITEM_MOD_ATTACK_POWER_SHORT", "Attack Power"},
|
||||
{"ITEM_MOD_RANGED_ATTACK_POWER_SHORT", "Ranged Attack Power"},
|
||||
{"ITEM_MOD_CRIT_MELEE_RATING", "Melee Crit %"},
|
||||
{"ITEM_MOD_CRIT_RANGED_RATING", "Ranged Crit %"},
|
||||
{"ITEM_MOD_HIT_MELEE_RATING", "Melee Hit %"},
|
||||
{"ITEM_MOD_HIT_RANGED_RATING", "Ranged Hit %"},
|
||||
{"ITEM_MOD_HIT_SPELL_RATING", "Spell Hit %"},
|
||||
{"ITEM_MOD_CRIT_SPELL_RATING", "Spell Crit %"},
|
||||
{"ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", "Spell Damage"},
|
||||
{"ITEM_MOD_SPELL_HEALING_DONE_SHORT", "Spell Healing"},
|
||||
{"ITEM_MOD_MANA_REGENERATION", "Mana Regen"},
|
||||
{"ITEM_MOD_DEFENSE_SKILL_RATING", "Defense"},
|
||||
{"ITEM_MOD_DODGE_RATING", "Dodge %"},
|
||||
{"ITEM_MOD_PARRY_RATING", "Parry %"},
|
||||
{"ITEM_MOD_BLOCK_RATING", "Block %"},
|
||||
{"ITEM_MOD_BLOCK_VALUE", "Block Value"},
|
||||
{"ITEM_MOD_DAMAGE_PER_SECOND_SHORT", "Damage Per Second"},
|
||||
{"ITEM_DELTA_DESCRIPTION",
|
||||
"If you replace this item, the following stat changes will occur:"},
|
||||
};
|
||||
|
||||
const char *FallbackLabel(const char *key) {
|
||||
for (const auto &e : kFallbackLabels)
|
||||
if (std::strcmp(e.key, key) == 0)
|
||||
return e.label;
|
||||
return key; // last resort — raw key beats crashing
|
||||
}
|
||||
|
||||
// Resolve a display label for `key` into `out`. Prefers the client's
|
||||
// `_G[key]` when it's a plain, non-empty string (respects localization /
|
||||
// server-defined labels), read via a direct globals-table access — no
|
||||
// pcall. Ignores format strings (a "%"-bearing global like "+%d Strength"
|
||||
// would render garbage), falling back to the built-in English label.
|
||||
void LabelFor(void *L, const char *key, char *out, int outSize) {
|
||||
const int saved = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
const char *g = (Game::Lua::Type(L, -1) == Game::Lua::TYPE_STRING)
|
||||
? Game::Lua::ToString(L, -1)
|
||||
: nullptr;
|
||||
if (g != nullptr && *g != '\0' && std::strchr(g, '%') == nullptr)
|
||||
std::snprintf(out, outSize, "%s", g);
|
||||
else
|
||||
std::snprintf(out, outSize, "%s", FallbackLabel(key));
|
||||
Game::Lua::SetTop(L, saved);
|
||||
}
|
||||
|
||||
// Delta colors — the canonical GlobalColor.dbc tags modern WoW uses for
|
||||
// item comparison, pulled from UI::ColorData (its packed argb is already
|
||||
// the {b,g,r,a} order the tooltip line-color setter wants).
|
||||
constexpr uint32_t kGain = UI::ColorData::ByTag("INCREASE_STAT_COLOR"); // green
|
||||
constexpr uint32_t kLoss = UI::ColorData::ByTag("DECREASE_STAT_COLOR"); // red
|
||||
constexpr uint32_t kHeader = UI::ColorData::ByTag("NORMAL_FONT_COLOR"); // header yellow
|
||||
|
||||
// self:AddLine equivalent — left-aligned colored line via the engine's
|
||||
// raw add-line. `color` must outlive the call (we pass its address).
|
||||
void AddColoredLine(void *self, const char *text, const uint32_t &color) {
|
||||
auto add = reinterpret_cast<AddLine_t>(Offsets::FUN_GAMETOOLTIP_ADD_LINE);
|
||||
add(self, text, nullptr, &color, nullptr, 0);
|
||||
}
|
||||
|
||||
// Appends the yellow ITEM_DELTA_DESCRIPTION header + one line per non-zero
|
||||
// stat and a DPS line. No-op when nothing differs (identical items /
|
||||
// self-compare add no delta section).
|
||||
void RenderDeltas(void *self, void *L, const Accum *acc, double dpsDelta) {
|
||||
const bool hasDps = (dpsDelta > 1e-4 || dpsDelta < -1e-4);
|
||||
bool hasAny = hasDps;
|
||||
for (int i = 0; !hasAny && i < Item::StatAccum::kCount; ++i)
|
||||
hasAny = (acc[i].value != 0);
|
||||
if (!hasAny)
|
||||
return;
|
||||
|
||||
char label[192];
|
||||
char line[256];
|
||||
|
||||
LabelFor(L, "ITEM_DELTA_DESCRIPTION", label, sizeof(label));
|
||||
AddColoredLine(self, label, kHeader);
|
||||
|
||||
if (hasDps) {
|
||||
LabelFor(L, "ITEM_MOD_DAMAGE_PER_SECOND_SHORT", label, sizeof(label));
|
||||
std::snprintf(line, sizeof(line), "%s%.1f %s", dpsDelta > 0 ? "+" : "",
|
||||
dpsDelta, label);
|
||||
AddColoredLine(self, line, dpsDelta > 0 ? kGain : kLoss);
|
||||
}
|
||||
|
||||
for (int i = 0; i < Item::StatAccum::kCount; ++i) {
|
||||
const long v = acc[i].value;
|
||||
if (v == 0)
|
||||
continue;
|
||||
LabelFor(L, acc[i].key, label, sizeof(label));
|
||||
std::snprintf(line, sizeof(line), "%s%ld %s", v > 0 ? "+" : "", v, label);
|
||||
AddColoredLine(self, line, v > 0 ? kGain : kLoss);
|
||||
}
|
||||
}
|
||||
|
||||
// --- "Currently Equipped" header ---------------------------------------
|
||||
//
|
||||
// The engine's builder can emit this header itself (FUN_0052B650 param_7),
|
||||
// but that path merges it into the item's name line and force-wraps it.
|
||||
// So we build the item cleanly and prepend the header the way vanilla
|
||||
// addons (pfUI eqcompare) do — shift every built line's text+color down
|
||||
// one slot and claim line 0 — but entirely in C via the engine's line
|
||||
// FontStrings, no Lua method calls.
|
||||
|
||||
const char *FSText(const void *fs) {
|
||||
if (fs == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(
|
||||
static_cast<const uint8_t *>(fs) + Offsets::OFF_FONTSTRING_TEXT);
|
||||
}
|
||||
|
||||
// Whether a line's FontString is actually shown. The tooltip clear hides
|
||||
// unused cells but keeps their (now stale) text, so this — not FSText —
|
||||
// is what decides whether a cell carries live content to shift.
|
||||
bool FSVisible(const void *fs) {
|
||||
if (fs == nullptr)
|
||||
return false;
|
||||
return *reinterpret_cast<const int *>(
|
||||
static_cast<const uint8_t *>(fs) + Offsets::OFF_FONTSTRING_VISIBLE) != 0;
|
||||
}
|
||||
|
||||
// Reads a FontString's current color into `*out` (4-byte {b,g,r,a}).
|
||||
// Returns false when the color storage isn't set, so the caller leaves
|
||||
// the destination's existing color alone.
|
||||
bool FSColor(const void *fs, uint32_t *out) {
|
||||
if (fs == nullptr)
|
||||
return false;
|
||||
auto *slot = *reinterpret_cast<const uint32_t *const *>(
|
||||
static_cast<const uint8_t *>(fs) + Offsets::OFF_FONTSTRING_COLOR_PTR);
|
||||
if (slot == nullptr)
|
||||
return false;
|
||||
*out = *slot;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FSSetText(void *fs, const char *text) {
|
||||
if (fs != nullptr)
|
||||
reinterpret_cast<SetLineText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(fs, text, 0);
|
||||
}
|
||||
|
||||
void FSSetColor(void *fs, const uint32_t &color) {
|
||||
if (fs != nullptr)
|
||||
reinterpret_cast<SetLineColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(fs, &color);
|
||||
}
|
||||
|
||||
using ShowHideFS_t = void(__fastcall *)(void *fs);
|
||||
|
||||
// Realize a FontString's shown state the way the engine's AddLine/clear
|
||||
// pair do: set the desired-shown flag, then call SHOW or HIDE so the
|
||||
// FontString gets positioned (or dropped from) the layout. Without this a
|
||||
// cell moved into a previously-hidden slot never anchors.
|
||||
void FSSetShown(void *fs, bool shown) {
|
||||
if (fs == nullptr)
|
||||
return;
|
||||
*reinterpret_cast<int *>(static_cast<uint8_t *>(fs) +
|
||||
Offsets::OFF_FONTSTRING_SHOWN_FLAG) = shown ? 1 : 0;
|
||||
reinterpret_cast<ShowHideFS_t>(
|
||||
shown ? Offsets::FUN_FONTSTRING_SHOW : Offsets::FUN_FONTSTRING_HIDE)(fs);
|
||||
}
|
||||
|
||||
// Copies `src`'s content (text + color) onto `dst`, replicating the shown
|
||||
// state so `dst` positions correctly; hides `dst` when `src` isn't a live
|
||||
// (shown) cell. Keyed on visibility, not text, so stale text the clear
|
||||
// left behind in a hidden cell isn't resurrected. One column of the shift.
|
||||
void CopyLineCell(void *dst, const void *src) {
|
||||
if (dst == nullptr)
|
||||
return;
|
||||
if (FSVisible(src)) {
|
||||
const char *text = FSText(src);
|
||||
FSSetText(dst, text != nullptr ? text : "");
|
||||
// Always set a color: source's if it has one, else white — so a
|
||||
// stale color left on `dst` by a previous delta line is reset
|
||||
// (lines the builder leaves at the default have no readable color).
|
||||
uint32_t color = UI::ColorData::ByTag("WHITE_FONT_COLOR");
|
||||
FSColor(src, &color);
|
||||
FSSetColor(dst, color);
|
||||
FSSetShown(dst, true);
|
||||
} else {
|
||||
FSSetText(dst, "");
|
||||
FSSetShown(dst, false);
|
||||
}
|
||||
}
|
||||
|
||||
void PrependCurrentlyEquipped(void *self, void *L) {
|
||||
auto *t = static_cast<uint8_t *>(self);
|
||||
int *numLines = reinterpret_cast<int *>(t + Offsets::OFF_GAMETOOLTIP_NUM_LINES);
|
||||
const int allocated =
|
||||
*reinterpret_cast<const int *>(t + Offsets::OFF_GAMETOOLTIP_NUM_LINES_ALLOC);
|
||||
const int n = *numLines;
|
||||
if (n <= 0 || n + 1 > allocated)
|
||||
return; // nothing built, or pool full (LinePool keeps 60, so rare)
|
||||
|
||||
auto **leftArr = *reinterpret_cast<void ***>(
|
||||
t + Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC + 8);
|
||||
auto **rightArr = *reinterpret_cast<void ***>(
|
||||
t + Offsets::OFF_GAMETOOLTIP_TEXTRIGHT_DESC + 8);
|
||||
auto *wrapArr = *reinterpret_cast<int **>(
|
||||
t + Offsets::OFF_GAMETOOLTIP_WRAPFLAG_DESC + 8);
|
||||
if (leftArr == nullptr || rightArr == nullptr || wrapArr == nullptr)
|
||||
return;
|
||||
|
||||
// Shift lines [0..n-1] down to [1..n]. High-to-low so each source is
|
||||
// read before the next iteration overwrites it.
|
||||
for (int i = n - 1; i >= 0; --i) {
|
||||
CopyLineCell(leftArr[i + 1], leftArr[i]);
|
||||
CopyLineCell(rightArr[i + 1], rightArr[i]);
|
||||
wrapArr[i + 1] = wrapArr[i];
|
||||
}
|
||||
|
||||
char header[128];
|
||||
LabelFor(L, "CURRENTLY_EQUIPPED", header, sizeof(header)); // native 1.12 string
|
||||
constexpr uint32_t grey = UI::ColorData::ByTag("GRAY_FONT_COLOR");
|
||||
FSSetText(leftArr[0], header);
|
||||
FSSetColor(leftArr[0], grey);
|
||||
FSSetShown(leftArr[0], true);
|
||||
FSSetText(rightArr[0], "");
|
||||
FSSetShown(rightArr[0], false);
|
||||
wrapArr[0] = 0;
|
||||
*numLines = n + 1;
|
||||
}
|
||||
|
||||
// Reads the live random-property (suffix) index off an equipped CGItem's
|
||||
// descriptor — the same index ItemRandomProperties.dbc / ApplyRandomSuffix
|
||||
// use. 0 for a plain item or a null descriptor.
|
||||
int EquippedSuffix(const uint8_t *item) {
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_DESCRIPTOR_RANDOM_PROPERTY));
|
||||
}
|
||||
|
||||
uint64_t EquippedGUID(const uint8_t *item) {
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetHyperlinkCompareItem(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: GameTooltip:SetHyperlinkCompareItem(\"itemLink\" [, offset, shiftButton, comparisonTooltip])");
|
||||
return 0;
|
||||
}
|
||||
const int argc = Game::Lua::GetTop(L);
|
||||
|
||||
auto bail = [&](int n) -> int {
|
||||
Game::Lua::SetTop(L, argc);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
return 1;
|
||||
};
|
||||
|
||||
// Compared ("hovered") item: prefer the explicit link (arg 2). When
|
||||
// it's absent, fall back to whatever the comparisonTooltip (arg 5) is
|
||||
// displaying — the builder stashes that tooltip's itemID/GUID, so we
|
||||
// recover the itemID directly and the suffix via the GUID's live
|
||||
// CGItem. This makes the method 1:1 with the retail call, which can be
|
||||
// driven purely tooltip-to-tooltip.
|
||||
int hoveredID = Item::Arg::ResolveItemID(L, 2);
|
||||
int hoveredSuffix = Item::StatAccum::ParseRandomSuffixFromLink(L, 2);
|
||||
if (hoveredID <= 0 && Game::Lua::Type(L, 5) == Game::Lua::TYPE_TABLE) {
|
||||
if (void *cmp = Game::Lua::ResolveObject(L, 5)) {
|
||||
uint64_t g = 0;
|
||||
hoveredID = Item::TooltipItem::CurrentID(cmp, &g);
|
||||
if (g != 0) {
|
||||
const uint8_t *cg = Item::Location::ResolveByGUID(g);
|
||||
if (cg != nullptr)
|
||||
hoveredSuffix = EquippedSuffix(cg);
|
||||
}
|
||||
}
|
||||
}
|
||||
const uint8_t *hoveredRec =
|
||||
hoveredID > 0 ? Item::StatAccum::FetchRecord(static_cast<uint32_t>(hoveredID)) : nullptr;
|
||||
if (hoveredRec == nullptr)
|
||||
return bail(0); // no link and no comparisonTooltip item, or not cached yet
|
||||
|
||||
const int invType = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
hoveredRec + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE));
|
||||
int slots[2] = {0, 0};
|
||||
const int nSlots = SlotsForInvType(invType, slots);
|
||||
if (nSlots == 0)
|
||||
return bail(0); // non-equippable
|
||||
|
||||
int offset = (Game::Lua::Type(L, 3) == Game::Lua::TYPE_NUMBER)
|
||||
? static_cast<int>(Game::Lua::ToNumber(L, 3))
|
||||
: 1;
|
||||
if (offset < 1)
|
||||
offset = 1;
|
||||
if (offset > nSlots)
|
||||
return bail(nSlots);
|
||||
const int slotID = slots[offset - 1];
|
||||
|
||||
// shiftButton: gates the stat-change breakdown. Deltas show by default
|
||||
// — for `true`, `1`, `nil`, or omitted — and are hidden ONLY for an
|
||||
// explicit `false` or `0`. Accepting both the boolean and the vanilla
|
||||
// 1/nil convention matches how callers (and FrameXML's
|
||||
// GameTooltip_ShowCompareItem) forward the modified-click state.
|
||||
bool showDeltas = true;
|
||||
const int shiftType = Game::Lua::Type(L, 4);
|
||||
if (shiftType == Game::Lua::TYPE_BOOLEAN)
|
||||
showDeltas = Game::Lua::ToBoolean(L, 4) != 0;
|
||||
else if (shiftType == Game::Lua::TYPE_NUMBER)
|
||||
showDeltas = Game::Lua::ToNumber(L, 4) != 0.0;
|
||||
|
||||
// What's equipped there? Nothing → no comparison; leave the tooltip
|
||||
// as the caller had it.
|
||||
const uint8_t *eqItem = Item::Location::ResolveEquipmentSlot(slotID);
|
||||
if (eqItem == nullptr)
|
||||
return bail(nSlots);
|
||||
const int equippedID = Item::ID::FromCGItem(eqItem);
|
||||
if (equippedID <= 0)
|
||||
return bail(nSlots);
|
||||
const int equippedSuffix = EquippedSuffix(eqItem);
|
||||
uint64_t equippedGUID = EquippedGUID(eqItem);
|
||||
|
||||
// The tooltip's underlying CFrameScriptObject — the `this` for the
|
||||
// engine's builder / add-line thiscalls.
|
||||
void *self = Game::Lua::ResolveObject(L, 1);
|
||||
if (self == nullptr)
|
||||
return bail(0);
|
||||
|
||||
// Build the equipped item into the tooltip via the engine's own
|
||||
// item-tooltip builder (the call SetInventoryItem/SetHyperlink use),
|
||||
// proven arg shape from FUN_005353b0/FUN_00535700 — all flags 0. This
|
||||
// clears + fills the item cleanly (no wrap). The grey "Currently
|
||||
// Equipped" header is prepended afterwards. Suppress OnTooltipSetItem for
|
||||
// this internal build so a handler doesn't run mid-way through the
|
||||
// line-shift below (it fires for the *hovered* item's real Set* instead).
|
||||
{
|
||||
Tooltip::SetItemEvent::Suppressor noEvent;
|
||||
reinterpret_cast<BuildItemTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_ITEM)(
|
||||
self, static_cast<uint32_t>(equippedID), &equippedGUID, &equippedGUID, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
PrependCurrentlyEquipped(self, L);
|
||||
|
||||
// Delta = hovered − equipped, then render (unless suppressed).
|
||||
if (showDeltas) {
|
||||
Accum acc[Item::StatAccum::kCount];
|
||||
Item::StatAccum::Init(acc);
|
||||
Item::StatAccum::AccumulateRecord(acc, hoveredRec, +1);
|
||||
Item::StatAccum::ApplyRandomSuffix(acc, hoveredSuffix, +1);
|
||||
const uint8_t *equippedRec =
|
||||
Item::StatAccum::FetchRecord(static_cast<uint32_t>(equippedID));
|
||||
if (equippedRec != nullptr) {
|
||||
Item::StatAccum::AccumulateRecord(acc, equippedRec, -1);
|
||||
Item::StatAccum::ApplyRandomSuffix(acc, equippedSuffix, -1);
|
||||
}
|
||||
const double dpsDelta = Item::StatAccum::ComputeDPS(hoveredRec) -
|
||||
(equippedRec ? Item::StatAccum::ComputeDPS(equippedRec) : 0.0);
|
||||
RenderDeltas(self, L, acc, dpsDelta);
|
||||
}
|
||||
return bail(nSlots);
|
||||
}
|
||||
|
||||
// `GameTooltip:IsEquippedItem()` → bool. True when the item the tooltip is
|
||||
// currently displaying is equipped in a character-pane slot. The displayed
|
||||
// item is resolved by Item::TooltipItem::CurrentID (which guards the stale
|
||||
// item-GUID so unit / spell / gameobject tooltips read 0 and never
|
||||
// false-positive). Backports the 3.3.5 GameTooltip method (registry
|
||||
// index 63); the equipment walk mirrors C_Item.IsEquippedItem's itemID
|
||||
// path.
|
||||
int __fastcall Script_IsEquippedItem(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:IsEquippedItem()");
|
||||
return 0;
|
||||
}
|
||||
void *self = Game::Lua::ResolveObject(L, 1);
|
||||
const int itemID = self != nullptr ? Item::TooltipItem::CurrentID(self, nullptr) : 0;
|
||||
|
||||
bool equipped = false;
|
||||
for (int slot = Offsets::EQUIPMENT_SLOT_FIRST;
|
||||
itemID > 0 && slot <= Offsets::EQUIPMENT_SLOT_LAST; ++slot) {
|
||||
const uint8_t *it = Item::Location::ResolveEquipmentSlot(slot);
|
||||
if (it != nullptr && Item::ID::FromCGItem(it) == itemID) {
|
||||
equipped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushBool(L, equipped);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetHyperlinkCompareItem", &Script_SetHyperlinkCompareItem},
|
||||
{"IsEquippedItem", &Script_IsEquippedItem},
|
||||
};
|
||||
|
||||
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])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Tooltip::Compare
|
||||
@@ -0,0 +1,222 @@
|
||||
// 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/>.
|
||||
|
||||
// Lift GameTooltip's 30-line cap (TODO #97) — pure C++, no addon Lua.
|
||||
//
|
||||
// Vanilla `GameTooltip:AddLine` (FUN_00530270) silently drops any line
|
||||
// past `numLinesAllocated`, and that count is just "how many contiguous
|
||||
// `<name>TextLeftN`/`TextRightN` FontStrings the FrameXML template
|
||||
// declared" — established once by the tooltip's XML-OnLoad region scan
|
||||
// (FUN_00529650, vtable slot 9). Stat-heavy tooltips (pfUI's eqcompare
|
||||
// block) lose their tail past line 30.
|
||||
//
|
||||
// We grow the pool the same way 3.3.5's AddLine grows on demand, but at
|
||||
// scan time instead of per line: co-hook the scan, and after the engine
|
||||
// has set up the template's 30 lines, create `TextLeft/Right 31..60` in
|
||||
// C++ (mirroring Script_CreateFontString + SetFontObject + SetPoint) and
|
||||
// append them to the tooltip's three line arrays. This runs once per
|
||||
// GameTooltip-family frame at creation (and per `/reload`), so it
|
||||
// auto-covers GameTooltip, ShoppingTooltip1/2, ItemRefTooltip,
|
||||
// WorldMapTooltip, … with no frame list and no `AddLine` hook.
|
||||
//
|
||||
// Everything the grow needs — parent frame, the previous line (for its
|
||||
// anchor target + font), and the array data pointers — comes straight
|
||||
// from the just-populated tooltip, so there's no `_G` name lookup. The
|
||||
// new lines are still *named* (`FUN_REGION_SET_NAME`) so addons that read
|
||||
// `_G[name.."TextLeft"..i]` keep working and a re-run of the scan
|
||||
// recounts them idempotently. See TODO #97 for the full RE trail.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Tooltip::LinePool {
|
||||
|
||||
namespace {
|
||||
|
||||
// Grow every GameTooltip-family frame to this many lines. pfUI's worst
|
||||
// case is ~32; 60 leaves headroom without an unbounded pool.
|
||||
constexpr uint32_t kMaxLines = 60;
|
||||
|
||||
// --- engine entry points (all __thiscall; see Offsets.h) -------------------
|
||||
using GetName_t = const char *(__thiscall *)(void *self);
|
||||
using PoolAlloc_t = void *(__thiscall *)(void *pool, int zeroInit, const char *tag, int line);
|
||||
using FontStringCtor_t = void *(__thiscall *)(void *mem, void *parent, int layer, int sublayer);
|
||||
using SetName_t = void(__thiscall *)(void *region, const char *name);
|
||||
using SetFont_t = void(__thiscall *)(void *fontHolder, void *fontObject);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint,
|
||||
float x, float y, int flag);
|
||||
using ReallocArray_t = void(__thiscall *)(void *descriptor, uint32_t newCount);
|
||||
|
||||
// The scan we co-hook: __thiscall(tooltip, node, flag). Hooked via the
|
||||
// standard __fastcall(ecx, edx, …) shim — __thiscall passes `this` in ECX
|
||||
// and the rest on the stack, so the trailing stack args line up and the
|
||||
// phantom `edx` is harmless.
|
||||
using Scan_t = void(__fastcall *)(void *tooltip, void *edx, void *node, int flag);
|
||||
Scan_t g_scanOriginal = nullptr;
|
||||
|
||||
uint8_t *DescData(uint8_t *tooltip, int descOffset) {
|
||||
return *reinterpret_cast<uint8_t **>(tooltip + descOffset + 8);
|
||||
}
|
||||
|
||||
// FUN_REGION_SET_POINT stores offsets in internal coordinates, so a pixel
|
||||
// offset must be scaled the way Script_SetPoint does before being passed —
|
||||
// otherwise a -2px gap lands ~1000px off. Returns 0 if the scale globals
|
||||
// aren't populated yet (degrades to a 0-offset anchor, not a wild one).
|
||||
float PixelToInternal(float pixels) {
|
||||
const float mul = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float divBase = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_DIV);
|
||||
const float denom = divBase * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT);
|
||||
if (denom == 0.0f)
|
||||
return 0.0f;
|
||||
return pixels * mul / denom;
|
||||
}
|
||||
|
||||
// Reads a FontString's current font object (holder+4). Used to copy the
|
||||
// previous line's font onto a new one so it renders identically.
|
||||
void *FontObjectOf(void *fontString) {
|
||||
if (fontString == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<void **>(reinterpret_cast<uint8_t *>(fontString) +
|
||||
Offsets::OFF_FONTSTRING_FONT_OBJECT);
|
||||
}
|
||||
|
||||
// Builds one CSimpleFontString: alloc → ctor(parent) → name → font (copied
|
||||
// from `fontSource`) → anchor. Mirrors Script_CreateFontString's core plus
|
||||
// the SetFontObject / SetPoint the Lua caller would do. Returns nullptr on
|
||||
// allocation failure.
|
||||
void *CreateLine(void *parent, const char *name, void *fontSource, int point,
|
||||
void *relRegion, int relPoint, float x, float y) {
|
||||
auto alloc = reinterpret_cast<PoolAlloc_t>(Offsets::FUN_REGION_POOL_ALLOC);
|
||||
void *mem = alloc(reinterpret_cast<void *>(Offsets::VAR_SIMPLEFONTSTRING_POOL), 0,
|
||||
reinterpret_cast<const char *>(Offsets::VAR_SIMPLEFONTSTRING_CLASS_TAG), -2);
|
||||
if (mem == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto ctor = reinterpret_cast<FontStringCtor_t>(Offsets::FUN_SIMPLEFONTSTRING_CTOR);
|
||||
void *fs = ctor(mem, parent, Offsets::DRAWLAYER_ARTWORK, 1);
|
||||
if (fs == nullptr)
|
||||
return nullptr;
|
||||
auto *fsBytes = reinterpret_cast<uint8_t *>(fs);
|
||||
|
||||
auto setName = reinterpret_cast<SetName_t>(Offsets::FUN_REGION_SET_NAME);
|
||||
setName(fs, name);
|
||||
|
||||
void *fontObject = FontObjectOf(fontSource);
|
||||
if (fontObject != nullptr) {
|
||||
auto setFont = reinterpret_cast<SetFont_t>(Offsets::FUN_FONTSTRING_SET_FONT);
|
||||
setFont(fsBytes + Offsets::OFF_FONTSTRING_FONT_HOLDER, fontObject);
|
||||
}
|
||||
|
||||
if (relRegion != nullptr) {
|
||||
auto setPoint = reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT);
|
||||
setPoint(fsBytes + Offsets::OFF_REGION_ANCHOR, point,
|
||||
reinterpret_cast<uint8_t *>(relRegion) + Offsets::OFF_REGION_ANCHOR,
|
||||
relPoint, PixelToInternal(x), PixelToInternal(y), 1);
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
|
||||
// Appends one (left, right) line pair to the tooltip's three parallel
|
||||
// arrays and bumps `numLinesAllocated` — the C++ equivalent of the
|
||||
// engine's own FUN_0061fe30. Reallocs each array via its {count,cap,data}
|
||||
// descriptor (the reallocator preserves `cap` old elements; we set the new
|
||||
// cap to match, as the scan does).
|
||||
void AppendLine(uint8_t *tooltip, void *left, void *right) {
|
||||
auto reallocPtr = reinterpret_cast<ReallocArray_t>(Offsets::FUN_TOOLTIP_REALLOC_PTR_ARRAY);
|
||||
auto reallocInt = reinterpret_cast<ReallocArray_t>(Offsets::FUN_TOOLTIP_REALLOC_INT_ARRAY);
|
||||
|
||||
const uint32_t index = *reinterpret_cast<uint32_t *>(
|
||||
tooltip + Offsets::OFF_GAMETOOLTIP_NUM_LINES_ALLOC);
|
||||
const uint32_t newCount = index + 1;
|
||||
|
||||
reallocPtr(tooltip + Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC, newCount);
|
||||
*reinterpret_cast<uint32_t *>(tooltip + Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC + 4) = newCount;
|
||||
reallocPtr(tooltip + Offsets::OFF_GAMETOOLTIP_TEXTRIGHT_DESC, newCount);
|
||||
*reinterpret_cast<uint32_t *>(tooltip + Offsets::OFF_GAMETOOLTIP_TEXTRIGHT_DESC + 4) = newCount;
|
||||
reallocInt(tooltip + Offsets::OFF_GAMETOOLTIP_WRAPFLAG_DESC, newCount);
|
||||
*reinterpret_cast<uint32_t *>(tooltip + Offsets::OFF_GAMETOOLTIP_WRAPFLAG_DESC + 4) = newCount;
|
||||
|
||||
reinterpret_cast<void **>(DescData(tooltip, Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC))[index] = left;
|
||||
reinterpret_cast<void **>(DescData(tooltip, Offsets::OFF_GAMETOOLTIP_TEXTRIGHT_DESC))[index] = right;
|
||||
reinterpret_cast<uint32_t *>(DescData(tooltip, Offsets::OFF_GAMETOOLTIP_WRAPFLAG_DESC))[index] = 0;
|
||||
|
||||
*reinterpret_cast<uint32_t *>(tooltip + Offsets::OFF_GAMETOOLTIP_NUM_LINES_ALLOC) = newCount;
|
||||
}
|
||||
|
||||
// Grows `tooltip`'s line pool up to kMaxLines, creating + appending the
|
||||
// missing `TextLeftN`/`TextRightN` pairs. No-op if the pool is empty
|
||||
// (scan didn't populate) or already at/above the target.
|
||||
void GrowPool(uint8_t *tooltip) {
|
||||
uint32_t count = *reinterpret_cast<uint32_t *>(
|
||||
tooltip + Offsets::OFF_GAMETOOLTIP_NUM_LINES_ALLOC);
|
||||
if (count == 0 || count >= kMaxLines)
|
||||
return;
|
||||
|
||||
auto vtable = *reinterpret_cast<void ***>(tooltip);
|
||||
auto getName = reinterpret_cast<GetName_t>(vtable[1]); // slot 1 = GetName
|
||||
const char *baseName = getName(tooltip);
|
||||
if (baseName == nullptr || baseName[0] == '\0')
|
||||
return;
|
||||
|
||||
for (uint32_t index = count; index < kMaxLines; ++index) {
|
||||
// Previous line supplies both the anchor target and the font.
|
||||
void *prevLeft = reinterpret_cast<void **>(
|
||||
DescData(tooltip, Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC))[index - 1];
|
||||
void *prevRight = reinterpret_cast<void **>(
|
||||
DescData(tooltip, Offsets::OFF_GAMETOOLTIP_TEXTRIGHT_DESC))[index - 1];
|
||||
if (prevLeft == nullptr || prevRight == nullptr)
|
||||
break;
|
||||
|
||||
char nameLeft[96];
|
||||
char nameRight[96];
|
||||
std::snprintf(nameLeft, sizeof nameLeft, "%sTextLeft%u", baseName, index + 1);
|
||||
std::snprintf(nameRight, sizeof nameRight, "%sTextRight%u", baseName, index + 1);
|
||||
|
||||
// Left: TOPLEFT anchored to the previous left line's BOTTOMLEFT at
|
||||
// (0, -2) — the template's exact chain. Right: RIGHT anchored to
|
||||
// this line's own left LEFT at (40, 0).
|
||||
void *left = CreateLine(tooltip, nameLeft, prevLeft, Offsets::FRAMEPOINT_TOPLEFT,
|
||||
prevLeft, Offsets::FRAMEPOINT_BOTTOMLEFT, 0.0f, -2.0f);
|
||||
if (left == nullptr)
|
||||
break;
|
||||
void *right = CreateLine(tooltip, nameRight, prevRight, Offsets::FRAMEPOINT_RIGHT,
|
||||
left, Offsets::FRAMEPOINT_LEFT, 40.0f, 0.0f);
|
||||
if (right == nullptr)
|
||||
break;
|
||||
|
||||
AppendLine(tooltip, left, right);
|
||||
}
|
||||
}
|
||||
|
||||
// Co-hook of the tooltip's XML-OnLoad region scan. Runs the original
|
||||
// (which counts the template's 30 lines and sizes the arrays), then grows
|
||||
// the pool. Guarded on the CGameTooltip vtable so we never touch a
|
||||
// non-tooltip object.
|
||||
void __fastcall Scan_h(void *tooltip, void *edx, void *node, int flag) {
|
||||
g_scanOriginal(tooltip, edx, node, flag);
|
||||
if (tooltip != nullptr &&
|
||||
*reinterpret_cast<uintptr_t *>(tooltip) == Offsets::VAR_GAMETOOLTIP_VTABLE)
|
||||
GrowPool(reinterpret_cast<uint8_t *>(tooltip));
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_GAMETOOLTIP_SETUP_LINES,
|
||||
reinterpret_cast<void *>(&Scan_h),
|
||||
reinterpret_cast<void **>(&g_scanOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Tooltip::LinePool
|
||||
@@ -0,0 +1,168 @@
|
||||
// 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/>.
|
||||
|
||||
// `GameTooltip:HookScript("OnTooltipSetItem", fn)` — backport of the modern
|
||||
// tooltip script that fires whenever a tooltip's item is set, so addons can
|
||||
// annotate item tooltips by hooking one script instead of wrapping every
|
||||
// Set* method (the pattern AtlasLoot resorts to in vanilla, rebuilding the
|
||||
// tooltip and clobbering its item linkage). Fully native — a real frame
|
||||
// script, settable via the standard SetScript / GetScript / HookScript.
|
||||
//
|
||||
// 1.12 already has the machinery for tooltip scripts, just not this one:
|
||||
//
|
||||
// - The CGGameTooltip vtable resolver FUN_GAMETOOLTIP_SCRIPT_RESOLVER maps a
|
||||
// script name to its handler slot on the tooltip (OnTooltipCleared etc.,
|
||||
// each an 8-byte {handler, context} pair). SetScript/GetScript/HookScript
|
||||
// all go through it. We co-hook it: for "OnTooltipSetItem" (a name the
|
||||
// engine doesn't know) we hand back a pointer to a C-side per-tooltip cell
|
||||
// — the tooltip object (alloc size 0x460) has no free 8-byte slot, and the
|
||||
// resolver's only contract is "return the address the script lives at", so
|
||||
// external storage works exactly like an in-object slot.
|
||||
//
|
||||
// - Every item setter (SetHyperlink item:, SetBagItem, SetInventoryItem,
|
||||
// SetMerchantItem, SetAuctionItem, …) funnels through the shared item
|
||||
// builder FUN_GAMETOOLTIP_BUILD_ITEM. We co-hook it and, after the build,
|
||||
// fire the handler via FUN_FRAME_RUN_SCRIPT (the same no-arg runner the
|
||||
// clear uses for OnTooltipCleared) with the tooltip as `self`.
|
||||
//
|
||||
// Suppressed during our own Tooltip::Compare equipped-item build (see
|
||||
// Suppressor), and guarded against a handler that re-enters the builder.
|
||||
|
||||
#include "SetItemEvent.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Tooltip::SetItemEvent {
|
||||
|
||||
namespace {
|
||||
|
||||
// Per-tooltip 8-byte handler slot {handler, context}. Tooltips are few and
|
||||
// permanent (GameTooltip, ItemRefTooltip, ShoppingTooltip1/2, AtlasLootTooltip,
|
||||
// WorldMapTooltip, plus the odd addon tooltip), so a small fixed table with a
|
||||
// linear scan is plenty and gives each cell a stable address to hand out.
|
||||
struct Cell {
|
||||
void *tooltip;
|
||||
uint32_t slot[2]; // [0] = handler, [1] = exec context (written by SetScript)
|
||||
};
|
||||
constexpr int kMaxTooltips = 32;
|
||||
Cell g_cells[kMaxTooltips];
|
||||
int g_cellCount = 0;
|
||||
|
||||
uint32_t *CellFor(void *tooltip, bool create) {
|
||||
for (int i = 0; i < g_cellCount; ++i)
|
||||
if (g_cells[i].tooltip == tooltip)
|
||||
return g_cells[i].slot;
|
||||
if (!create || g_cellCount >= kMaxTooltips)
|
||||
return nullptr;
|
||||
Cell &c = g_cells[g_cellCount++];
|
||||
c.tooltip = tooltip;
|
||||
c.slot[0] = 0;
|
||||
c.slot[1] = 0;
|
||||
return c.slot;
|
||||
}
|
||||
|
||||
bool EqualsIgnoreCase(const char *s, const char *literal) {
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
for (;; ++s, ++literal) {
|
||||
unsigned char a = static_cast<unsigned char>(*s);
|
||||
const unsigned char b = static_cast<unsigned char>(*literal);
|
||||
if (a >= 'A' && a <= 'Z')
|
||||
a = static_cast<unsigned char>(a + 32);
|
||||
if (a != b)
|
||||
return false;
|
||||
if (b == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr char kScriptNameLower[] = "ontooltipsetitem"; // matched case-insensitively
|
||||
|
||||
// --- Suppression / recursion state -------------------------------------
|
||||
int g_suppress = 0; // > 0 while our own compare build runs
|
||||
int g_firing = 0; // recursion guard around the handler fire
|
||||
|
||||
// --- Resolver co-hook (CGGameTooltip vtable script-name → slot) ---------
|
||||
using Resolver_t = int(__fastcall *)(void *self, void *edx, const char *name);
|
||||
Resolver_t g_resolverOriginal = nullptr;
|
||||
|
||||
int __fastcall Resolver_h(void *self, void *edx, const char *name) {
|
||||
const int engineSlot = g_resolverOriginal(self, edx, name);
|
||||
if (engineSlot != 0) // a base-frame or existing tooltip script — leave it
|
||||
return engineSlot;
|
||||
if (EqualsIgnoreCase(name, kScriptNameLower))
|
||||
return reinterpret_cast<int>(CellFor(self, /*create*/ true));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Item-builder co-hook (fires the handler after the item is set) -----
|
||||
using Build_t = void(__fastcall *)(void *self, void *edx, uint32_t itemID, const void *guid,
|
||||
const void *guid2, int a4, int a5, int a6, int headerFlag,
|
||||
int a8, int a9);
|
||||
Build_t g_buildOriginal = nullptr;
|
||||
|
||||
// The engine's real script invoker: binds the global `this` = frame and runs
|
||||
// the handler under its own protected lua_pcall. We call it directly rather
|
||||
// than the FUN_00702690 wrapper, whose exec-context mutation is unsafe from the
|
||||
// deep nested Lua->C->Lua stack our co-hook fires within — see Offsets.h.
|
||||
using InvokeScript_t = void(__fastcall *)(uint32_t handler, void *frame);
|
||||
|
||||
void __fastcall Build_h(void *self, void *edx, uint32_t itemID, const void *guid,
|
||||
const void *guid2, int a4, int a5, int a6, int headerFlag, int a8,
|
||||
int a9) {
|
||||
g_buildOriginal(self, edx, itemID, guid, guid2, a4, a5, a6, headerFlag, a8, a9);
|
||||
if (g_suppress > 0 || g_firing > 0)
|
||||
return;
|
||||
uint32_t *slot = CellFor(self, /*create*/ false);
|
||||
if (slot == nullptr || slot[0] == 0)
|
||||
return;
|
||||
|
||||
// We fire from inside the item builder, which is itself mid-execution of
|
||||
// the engine's Script_Set* (and, in the wild, nested under addon Set*
|
||||
// wrappers + several other DLLs' hooks). The invoker pushes onto the Lua
|
||||
// stack and does NOT restore the top to where the outer, still-running C
|
||||
// code left it — harmless from the engine's top-level dispatch, but from
|
||||
// here it shifts the stack and the outer code (and other DLLs' hooks) then
|
||||
// index garbage → deref → crash. Snapshot and restore the stack top around
|
||||
// the fire so the outer code sees exactly the stack it left.
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
++g_firing;
|
||||
reinterpret_cast<InvokeScript_t>(Offsets::FUN_FRAME_INVOKE_SCRIPT)(slot[0], self);
|
||||
--g_firing;
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _resolverHook{
|
||||
Offsets::FUN_GAMETOOLTIP_SCRIPT_RESOLVER,
|
||||
reinterpret_cast<void *>(&Resolver_h),
|
||||
reinterpret_cast<void **>(&g_resolverOriginal)};
|
||||
|
||||
static const Game::HookAutoRegister _buildHook{
|
||||
Offsets::FUN_GAMETOOLTIP_BUILD_ITEM,
|
||||
reinterpret_cast<void *>(&Build_h),
|
||||
reinterpret_cast<void **>(&g_buildOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
Suppressor::Suppressor() { ++g_suppress; }
|
||||
Suppressor::~Suppressor() {
|
||||
if (g_suppress > 0)
|
||||
--g_suppress;
|
||||
}
|
||||
|
||||
} // namespace Tooltip::SetItemEvent
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Tooltip::SetItemEvent {
|
||||
|
||||
// RAII guard that suppresses OnTooltipSetItem firing for the duration of an
|
||||
// engine item-build we invoke *internally* (Tooltip::Compare building the
|
||||
// equipped item into a shopping tooltip). Without it, a handler would run in
|
||||
// the middle of our line-shift / delta rendering and see a half-built tooltip.
|
||||
// Nestable (ref-counted).
|
||||
struct Suppressor {
|
||||
Suppressor();
|
||||
~Suppressor();
|
||||
Suppressor(const Suppressor &) = delete;
|
||||
Suppressor &operator=(const Suppressor &) = delete;
|
||||
};
|
||||
|
||||
} // namespace Tooltip::SetItemEvent
|
||||
@@ -242,4 +242,25 @@ constexpr Entry kColors[] = {
|
||||
|
||||
constexpr int kColorCount = sizeof(kColors) / sizeof(kColors[0]);
|
||||
|
||||
// Looks up a color by `baseTag` and returns it as a packed 32-bit value.
|
||||
// The DBC `argb` column decodes as `0xAARRGGBB` (see the byte-split note
|
||||
// above) — which is also exactly the little-endian {b,g,r,a} byte order
|
||||
// the engine's tooltip line-color setter (FUN_0077f750) reads, so the
|
||||
// returned value can be handed straight to a colored AddLine. Returns
|
||||
// `fallback` (default opaque white) when the tag isn't present. constexpr
|
||||
// so callers can bind named color constants at compile time.
|
||||
constexpr uint32_t ByTag(const char *tag, uint32_t fallback = 0xFFFFFFFFu) {
|
||||
for (const auto &e : kColors) {
|
||||
const char *a = e.baseTag;
|
||||
const char *b = tag;
|
||||
while (*a != '\0' && *a == *b) {
|
||||
++a;
|
||||
++b;
|
||||
}
|
||||
if (*a == '\0' && *b == '\0')
|
||||
return static_cast<uint32_t>(e.argb);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
} // namespace UI::ColorData
|
||||
|
||||
Reference in New Issue
Block a user