// 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 . // Lift GameTooltip's line cap — 4.3.4's grow-on-demand, pure C++. // // Vanilla's add-line body (FUN_00530270) silently drops any line once // `numLines` reaches `numLinesAllocated`, and that count is just "how many // contiguous `TextLeftN`/`TextRightN` FontStrings the FrameXML // template declared" (30), counted once by the XML-OnLoad scan // (FUN_00529650). Stat-heavy tooltips (pfUI's eqcompare block) lose their // tail. // // 4.3.4's CGTooltip::AddLine (wowdecomp15595 Tooltip.cpp) grows the // pool inside the gate: when the line about to be added takes the last // slot, it first builds the next `TextLeft/Right` pair (alloc → ctor // → SetName → previous line's font → template anchor → Hide) and appends it // with CGTooltip::AddFontStrings. We co-hook the same gate and do the same, // appending through 1.12's own AddFontStrings body (FUN_00529940, the // `GameTooltip:AddFontStrings` method's core). FUN_00530270 is the one // gate every line goes through — the Lua AddLine/AddDoubleLine methods and // the native spell/item/unit builders all call it — so every tooltip grows // with no cap and no frame list. // // After every add the pool keeps one spare line, as 4.3.4's does; // Tooltip::CompareItem's "Currently Equipped" prepend relies on it. #include "Game.h" #include "Offsets.h" #include "frame/Region.h" #include #include namespace Tooltip::LinePool { namespace { // --- engine entry points (all __thiscall; see Offsets.h) ------------------- using GetName_t = const char *(__thiscall *)(void *self); 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 HideFontString_t = void(__fastcall *)(void *fs); using AddFontStrings_t = void(__thiscall *)(void *tooltip, void *left, void *right); // The gate we co-hook: __thiscall(tooltip, left, right, lColor*, rColor*, // wrap), `ret 0x14`. 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 AddLine_t = void(__fastcall *)(void *tooltip, void *edx, const char *left, const char *right, const void *leftColor, const void *rightColor, int wrap); AddLine_t g_addLineOriginal = nullptr; bool HasText(const char *text) { return text != nullptr && text[0] != '\0'; } void *LineAt(uint8_t *tooltip, int descOffset, uint32_t index) { return Game::Read(tooltip, descOffset + 8)[index]; } // Builds one hidden CSimpleFontString: alloc → ctor(parent) → name → font // (copied from `fontSource`) → anchor → hide. Mirrors 4.3.4's AddLine grow. // Returns nullptr on allocation failure. void *CreateLine(void *parent, const char *name, void *fontSource, int point, void *relRegion, int relPoint, float x, float y) { void *fs = Frame::Region::CreateFontString(parent, Offsets::DRAWLAYER_ARTWORK, 1); if (fs == nullptr) return nullptr; auto *fsBytes = reinterpret_cast(fs); reinterpret_cast(Offsets::FUN_REGION_SET_NAME)(fs, name); void *fontObject = Game::Read(fontSource, Offsets::OFF_FONTSTRING_FONT_OBJECT); if (fontObject != nullptr) reinterpret_cast(Offsets::FUN_FONTSTRING_SET_FONT)( fsBytes + Offsets::OFF_FONTSTRING_FONT_HOLDER, fontObject); reinterpret_cast(Offsets::FUN_REGION_SET_POINT)( fsBytes + Offsets::OFF_REGION_ANCHOR, point, reinterpret_cast(relRegion) + Offsets::OFF_REGION_ANCHOR, relPoint, Game::PixelToInternal(x), Game::PixelToInternal(y), 1); // The clear only hides lines below `numLines`, so a spare line has to // start hidden — the same `+0xC4 = 0` + hide the clear does. Game::Ref(fs, Offsets::OFF_FONTSTRING_SHOWN_FLAG) = 0; reinterpret_cast(Offsets::FUN_FONTSTRING_HIDE)(fs); return fs; } // Appends line `max + 1` to a tooltip whose pool holds `max` lines. The // previous line is the anchor target and the font source, as in the // template: left TOPLEFT → previous left BOTTOMLEFT at (0, line gap), right // RIGHT → its own left LEFT at (40, 0). False when allocation fails. bool GrowOne(uint8_t *tooltip, uint32_t max) { void *prevLeft = LineAt(tooltip, Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC, max - 1); void *prevRight = LineAt(tooltip, Offsets::OFF_GAMETOOLTIP_TEXTRIGHT_DESC, max - 1); if (prevLeft == nullptr || prevRight == nullptr) return false; auto vtable = *reinterpret_cast(tooltip); const char *baseName = reinterpret_cast(vtable[1])(tooltip); // slot 1 = GetName if (baseName == nullptr) baseName = ""; // the scan's own fallback char nameLeft[256]; char nameRight[256]; std::snprintf(nameLeft, sizeof nameLeft, "%sTextLeft%u", baseName, max + 1); std::snprintf(nameRight, sizeof nameRight, "%sTextRight%u", baseName, max + 1); void *left = CreateLine(tooltip, nameLeft, prevLeft, Offsets::FRAMEPOINT_TOPLEFT, prevLeft, Offsets::FRAMEPOINT_BOTTOMLEFT, 0.0f, Game::Read(Offsets::VAR_TOOLTIP_LINE_GAP)); if (left == nullptr) return false; void *right = CreateLine(tooltip, nameRight, prevRight, Offsets::FRAMEPOINT_RIGHT, left, Offsets::FRAMEPOINT_LEFT, 40.0f, 0.0f); if (right == nullptr) return false; reinterpret_cast(Offsets::FUN_GAMETOOLTIP_ADD_FONT_STRINGS)(tooltip, left, right); return true; } void __fastcall AddLine_h(void *tooltip, void *edx, const char *left, const char *right, const void *leftColor, const void *rightColor, int wrap) { // 4.3.4 skips the grow for an empty line and for a tooltip whose // template declared no lines; the original then no-ops on its own. if (HasText(left) || HasText(right)) { auto *t = static_cast(tooltip); const uint32_t numLines = Game::Read(t, Offsets::OFF_GAMETOOLTIP_NUM_LINES); uint32_t max = Game::Read(t, Offsets::OFF_GAMETOOLTIP_NUM_LINES_ALLOC); // A loop rather than 4.3.4's single `==`, so a pool a direct // `numLines` writer (the compare prepend) left with no spare still // recovers it. while (max != 0 && numLines + 1 >= max && GrowOne(t, max)) ++max; } g_addLineOriginal(tooltip, edx, left, right, leftColor, rightColor, wrap); } static const Game::HookAutoRegister _hookreg{ Offsets::FUN_GAMETOOLTIP_ADD_LINE, reinterpret_cast(&AddLine_h), reinterpret_cast(&g_addLineOriginal)}; } // namespace } // namespace Tooltip::LinePool