Compare commits

...
6 Commits
Author SHA1 Message Date
Brues b5aed30a5d feat(chat): add GetCurrentChatEvent
Returns the event name and arguments of the CHAT_MSG_* event being fired,
read from the fire itself rather than the event/arg globals, which 1.12's
ChatFrame_OnEvent depends on and any earlier handler or hook can overwrite.
Nested chat events restore the outer payload when they return.
2026-10-23 03:05:09 -05:00
Brues 9755cd3e09 feat(gossip): fire DYNAMIC_GOSSIP_POI_UPDATED and document the POI API
Diff the gossip marker's x, y and expiry once per frame: each set
rewrites them and both clears (10 yards, 480 s) zero them. The map
rebuild that every change runs also runs on each map view change, so it
is not a usable trigger on its own. Add the two POI functions and the
event to README.md and docs/API.md.
2026-10-23 01:44:22 -05:00
Brues 604d87b9fc feat(gossip): add C_GossipInfo.GetPoiForUiMapID and GetPoiInfo
Read the gossip marker from static blip record 1, where the
SMSG_GOSSIP_POI handler stores it, and place it on a map with the world
map's own test (0x004A7360): same map id and inside 0..1. The packet
carries no POI id and the engine holds one marker, so its id is 1.
2026-10-23 01:37:53 -05:00
Brues 6b0a74ec4b fix(gossip): gate C_GossipInfo on the greeting-pending flag and walk visible options in SelectOptionByIndex
The gossip GUID is latched before the greeting resolves, so GUID != 0 alone
served a window that had not shown yet, with the previous NPC's text. Honor
the engine's pending flag (0xBBB670), the same one its option selector checks.

SelectOptionByIndex used orderIndex - 1 as the storage slot; it now skips
hidden slots the way GetOptions numbers them.
2026-10-23 01:30:06 -05:00
Brues 165ccf3e9c refactor(lua): share the engine's flag readers as Game::Lua::ToFlag and Game::StringToFlag
Twenty modules each declared their own typedef or wrapper for
FUN_LUA_TO_BOOLEAN_LOOSE (4.3.4's StringToBOOL(L, index, default)) or
FUN_XML_ATTR_TO_BOOL (StringToBOOL(const char *)). Route them all
through one helper each.

frame/Cooldown.cpp had a C++ copy of both that read unknown attribute
text as true; the engine's reader, which 4.3.4's XML loader uses, reads
it as false. SetReverse/SetDrawEdge now also truncate numbers like
every other 1.12 flag method.
2026-10-23 01:15:38 -05:00
Brues ce4f80518f fix(tooltip): match 4.3.4's SetSpellByID and AddSpellByID
Take (spellID [, isPet [, showSubtext]]), pass the spell's remaining
cooldown to the engine builder as 1.12's SetSpell does, and return 1
when the cooldown line shows, else nil. The rank is now off by default
(showSubtext turns it on), and a bad ID raises 4.3.4's
"Invalid spell ID in %s:SetSpellByID".

AddSpellByID rewrites the builder's "Next rank:" header through the
engine's add-line, so showSubtext works when appending, and leaves a
recipe's created-item tooltip untouched.
2026-10-23 01:07:00 -05:00
31 changed files with 650 additions and 270 deletions
+3 -2
View File
@@ -69,7 +69,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID` |
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
| [ClassColor](docs/API.md#classcolor) | `C_ClassColor.GetClassColor` |
@@ -103,7 +103,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
| [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`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests` |
| [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.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests` |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
@@ -275,6 +275,7 @@ when launching with `-console`), not as Lua functions. See the
| `COMBAT_LOG_EVENT` | `timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...` ([details](docs/API.md#combat_log_event_unfiltered--combat_log_event-events)) |
| `COMBAT_LOG_EVENT_UNFILTERED` | same as `COMBAT_LOG_EVENT` |
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
| `DYNAMIC_GOSSIP_POI_UPDATED` | *(none)* |
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
| `EQUIPMENT_SWAP_PENDING` | `setID` |
| `EQUIPMENT_SWAP_FINISHED` | `success, setID` |
+111 -26
View File
@@ -66,6 +66,7 @@ build instructions.
- [`GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only](#getsavedcharacterorderrealm--setsavedcharacterorderrealm-order--gluexml-only)
- [Chat](#chat)
- [`GetCurrentChatEvent()`](#getcurrentchatevent)
- [`GetCurrentChatGUID()`](#getcurrentchatguid)
- [Class](#class)
@@ -198,6 +199,7 @@ build instructions.
- [`UPDATE_INVENTORY_DURABILITY` event](#update_inventory_durability-event)
- [`PLAYER_AVG_ITEM_LEVEL_READY` event](#player_avg_item_level_ready-event)
- [`HEARTHSTONE_BOUND` event](#hearthstone_bound-event)
- [`DYNAMIC_GOSSIP_POI_UPDATED` event](#dynamic_gossip_poi_updated-event)
- [Player input-state events (`PLAYER_STARTED_MOVING` / `LOOKING` / `TURNING` + `STOPPED_*`)](#player-input-state-events)
- [`GLOBAL_MOUSE_DOWN` / `GLOBAL_MOUSE_UP` events](#global_mouse_down--global_mouse_up-events)
- [`AUCTION_MULTISELL_START` / `AUCTION_MULTISELL_UPDATE` / `AUCTION_MULTISELL_FAILURE` events](#c_auctionhousepostitemitemlocation-duration-quantity-numstacks-bid-buyout)
@@ -376,8 +378,8 @@ build instructions.
- [GameTooltip](#gametooltip)
- [`GameTooltip:AddTexture(texture)`](#gametooltipaddtexturetexture)
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
- [`GameTooltip:AddSpellByID(spellID)`](#gametooltipaddspellbyidspellid)
- [`GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])`](#gametooltipsetspellbyidspellid--ispet--showsubtext)
- [`GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])`](#gametooltipaddspellbyidspellid--ispet--showsubtext)
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
- [`GameTooltip:SetInventoryItemByID(itemID)`](#gametooltipsetinventoryitembyiditemid)
- [`GameTooltip:SetHyperlink(link)`](#gametooltipsethyperlinklink)
@@ -429,6 +431,8 @@ build instructions.
- [`C_GossipInfo.SelectAvailableQuest(questID)`](#c_gossipinfoselectavailablequestquestid)
- [`C_GossipInfo.SelectActiveQuest(questID)`](#c_gossipinfoselectactivequestquestid)
- [`C_GossipInfo.CloseGossip()`](#c_gossipinfoclosegossip)
- [`C_GossipInfo.GetPoiForUiMapID(uiMapID)`](#c_gossipinfogetpoiforuimapiduimapid)
- [`C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)`](#c_gossipinfogetpoiinfouimapid-gossippoiid)
- [`GetNumGossipAvailableQuests()` / `GetNumGossipActiveQuests()`](#getnumgossipavailablequests--getnumgossipactivequests)
- [Hooks](#hooks)
@@ -1934,6 +1938,40 @@ end
## Chat
### `GetCurrentChatEvent()`
Returns the name and the arguments of the `CHAT_MSG_*` event that is being
handled now. It returns nothing when you call it outside a chat event.
This is a ClassicAPI extension.
```lua
event, arg1, arg2, ..., arg12 = GetCurrentChatEvent()
```
The values are the values that the event gave to its handlers, in the same
order: the event name, `arg1` to `arg10`, then the line ID (`arg11`) and the
sender GUID (`arg12`) (see
[`CHAT_MSG_*` arguments](#chat_msg_-arguments-lineid-arg11-and-guid-arg12)).
Use it when the `event` and `arg1` to `arg12` globals are not reliable. Other
handlers and hooks can change these globals before your code reads them. This
function always gives the values of the event that is in progress.
```lua
local orig = ChatFrame_OnEvent
function ChatFrame_OnEvent(event)
local name, text, sender = GetCurrentChatEvent()
if name then
-- name, text and sender are the real values, even if a hook changed arg1/arg2
end
return orig(event)
end
```
If a chat event handler causes another chat event, the function gives the
inner event while that handler runs. When the inner handler returns, it gives
the outer event again.
### `GetCurrentChatGUID()`
Returns the sender's GUID for the `CHAT_MSG_*` event that is being handled
@@ -4518,6 +4556,12 @@ Character switch is handled automatically: the character-entry init
re-zeroes the flag, so each new character's first update is also
treated as sync and suppressed.
### `DYNAMIC_GOSSIP_POI_UPDATED` event
Fires (with no payload) when a gossip point of interest is added to the map or
removed from it. When it fires, read the marker again with
[`C_GossipInfo.GetPoiForUiMapID`](#c_gossipinfogetpoiforuimapiduimapid).
### Player input-state events
Three pairs of zero-payload events fired from a single per-frame
@@ -8644,44 +8688,51 @@ The call adds no texture and returns `false` in these cases:
12x12. Do not resize or move a texture after you add it: the text of the
line does not move with it.
### `GameTooltip:SetSpellByID(spellID)`
### `GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])`
Renders a spell tooltip for any `spellID`, including spells the player has
not learned. The stock 1.12 `GameTooltip:SetSpell(slot, bookType)` only works
for entries in the player's spellbook (or pet book). `SetSpellByID` bypasses
the spellbook indirection and calls WoW's internal tooltip builder directly.
Shows the tooltip of a spell. The spell can be any spell ID, also a spell
that the player did not learn.
| Argument | Type | Description |
|---|---|---|
| `spellID` | number | The ID of the spell. |
| `isPet` | boolean | If `true`, the cost, range, and cooldown are those of the pet. The default is `false`. |
| `showSubtext` | boolean | If `true`, the rank shows to the right of the spell name. The default is `false`. |
Returns `1` when the tooltip shows a cooldown that counts down. Otherwise,
returns `nil`. The tooltip shows the remaining cooldown only for a spell
with a cooldown of 1 minute or more.
If `spellID` is not a number, or is less than 0, the function raises an
error. If no spell has this ID, the tooltip clears and hides.
```lua
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetSpellByID(133) -- Fireball
GameTooltip:SetSpellByID(133) -- Fireball
GameTooltip:SetSpellByID(133, false, true) -- Fireball, with its rank
GameTooltip:Show()
```
### `GameTooltip:AddSpellByID(spellID)`
### `GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])`
The append counterpart to [`SetSpellByID`](#gametooltipsetspellbyidspellid):
adds a spell's full tooltip (name, cast time, cost, range, description) to
the *current* tooltip **without clearing** the existing lines. Works for any
`spellID`, learned or not. Lets you compose tooltips — e.g. a header line
plus a spell block.
Adds the tooltip of a spell below the lines that the tooltip already shows.
The tooltip does not clear first. The arguments and the return value are the
same as for [`SetSpellByID`](#gametooltipsetspellbyidspellid--ispet--showsubtext).
Use this function to make one tooltip from more parts, for example a header
line and a spell.
```lua
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:AddLine("Rank 3 grants:")
GameTooltip:AddSpellByID(133) -- Fireball, appended below the header
GameTooltip:AddSpellByID(133) -- Fireball, below the header
GameTooltip:Show()
```
Unlike `SetSpellByID`, this does **not** update what
[`GetSpell`](#gametooltipgetspell) reports — the appended spell isn't the
tooltip's "primary" spell, so `GetSpell` keeps reflecting whatever `SetX`
call (if any) built the base tooltip. Silent no-op for `spellID <= 0` or an
unknown spell.
> The engine's tooltip builder only exposes "append" via its internal talent
> "next rank" preview, which emits a `Next rank:` header instead of the
> spell name; we overwrite that header line with the real name so the
> appended block reads normally.
This function does not change the value of
[`GetSpell`](#gametooltipgetspell). `GetSpell` continues to give the spell
of the tooltip before the added lines. If no spell has this ID, the function
adds no lines.
### `GameTooltip:GetItem()`
@@ -9003,7 +9054,7 @@ GameTooltip:SetHyperlink("quest:783")
| `\|H<payload>\|h[text]\|h` | The tooltip for the payload, as in the rows below |
| `item:` | The item tooltip |
| `enchant:<spellID>` | The spell tooltip, for an Enchanting formula or a trade-skill recipe (such as a [`GetTradeSkillRecipeLink`](#gettradeskillrecipelinkindex) link) |
| `spell:<spellID>` | The spell tooltip, the same as [`SetSpellByID`](#gametooltipsetspellbyidspellid) |
| `spell:<spellID>` | The spell tooltip of [`SetSpellByID`](#gametooltipsetspellbyidspellid--ispet--showsubtext), with the rank and without the cooldown |
| `quest:<questID>[:level]` | The quest title, then the objectives text. Tokens such as `$N` are expanded, as in the quest log. |
Other link types (for example `achievement:` and `talent:`) raise the error
@@ -9772,6 +9823,40 @@ Picks an in-progress quest by `questID`. Same shape as
Closes the gossip window. Direct passthrough to the engine's
`CloseGossip`. Returns nothing.
### `C_GossipInfo.GetPoiForUiMapID(uiMapID)`
Returns the ID of the gossip point of interest on the map `uiMapID`, or nil.
Some gossip options add this marker to the map, for example the directions
that a city guard gives. There is only one marker at a time, and its ID is
always `1`.
The function returns nil when there is no marker, or when the marker is not
inside the map. The marker goes away when the player comes within 10 yards of
it, or 8 minutes after it was added.
### `C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)`
Returns a table that describes the gossip point of interest on the map
`uiMapID`, or nil. It returns nil in the same conditions as
`GetPoiForUiMapID`, and when `gossipPoiID` is not the ID that
`GetPoiForUiMapID` returns.
| Field | Type | Description |
|----------------|---------|-------------|
| `name` | string | The name of the location, for example `"Stormwind Bank"`. |
| `textureIndex` | number | The icon index. It is the same index that `GetMapLandmarkInfo` returns for this marker. |
| `position` | vector2 | The position on the map, from 0 to 1 on each axis. |
| `inBattleMap` | boolean | Always `false`. |
```lua
local mapID = C_Map.GetBestMapForUnit("player")
local poiID = C_GossipInfo.GetPoiForUiMapID(mapID)
if poiID then
local info = C_GossipInfo.GetPoiInfo(mapID, poiID)
print(info.name, info.position:GetXY())
end
```
### `GetNumGossipAvailableQuests()` / `GetNumGossipActiveQuests()`
Returns the number of quests that `GetGossipAvailableQuests()` /
+12
View File
@@ -40,6 +40,11 @@ double InternalToPixel(float internal) {
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
}
bool StringToFlag(const char *text) {
using StringToFlag_t = int(__fastcall *)(const char *text);
return reinterpret_cast<StringToFlag_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0;
}
namespace Lua {
// Each entry binds a typed function pointer in `Game::Lua::` to the
// corresponding raw VA in `Offsets`. The X-macro keeps the column-aligned
@@ -131,6 +136,13 @@ void PushObject(void *L, void *object) {
RawGetI(L, REGISTRY_INDEX, ObjectLuaRef(object));
}
bool ToFlag(void *L, int index, bool defaultValue) {
// Returns a full int in EAX on every path (Ghidra's `bool` is wrong).
using ToFlag_t = int(__fastcall *)(void *L, int index, int defaultValue);
return reinterpret_cast<ToFlag_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, index,
defaultValue ? 1 : 0) != 0;
}
const char *ObjectDisplayName(void *object) {
using GetName_t = const char *(__thiscall *)(void *);
const void *vtable = Game::Read<const void *>(object, 0);
+13
View File
@@ -104,6 +104,12 @@ using LoadScriptFunctions_t = void(__fastcall *)();
// registering its 109 glue functions. See FUN_LOAD_GLUE_SCRIPT_FUNCTIONS.
using LoadGlueScriptFunctions_t = void(__stdcall *)();
// The engine's text-to-flag reader (FUN_XML_ATTR_TO_BOOL, 4.3.4's
// `StringToBOOL(const char *)`), which XML boolean attributes go through:
// true for text starting 1-9/T/Y/t/y or equal to "on"/"enabled" (any case),
// false for anything else, including null.
bool StringToFlag(const char *text);
// --- API documentation descriptors -----------------------------------------
//
// Every Lua registration (see the `Register*` functions in `Game::Lua`) can
@@ -312,6 +318,13 @@ extern const lua_pushboolean_t PushBoolean;
// engine's `lua_pushboolean` ABI wants `int` (any non-zero = true), so
// this just funnels through the raw binding with a single conversion.
inline void PushBool(void *L, bool b) { PushBoolean(L, static_cast<int>(b)); }
// The engine's script-flag argument reader (FUN_LUA_TO_BOOLEAN_LOOSE, 4.3.4's
// `StringToBOOL(L, index, default)`), which every engine method taking an
// optional flag uses: nil is false, a boolean is itself, a number is its
// truncated int (so 0.5 is false), a string reads as StringToFlag except that
// text it does not recognize is `defaultValue`, and any other type is
// `defaultValue`.
bool ToFlag(void *L, int index, bool defaultValue = false);
extern const lua_pushstring_t PushString;
extern const lua_pushlstring_t PushLString;
extern const lua_pushvalue_t PushValue;
+49 -5
View File
@@ -189,8 +189,16 @@ enum Offsets {
// lookups (e.g. GameTooltip:GetOwner) subtract this back out.
OFF_FRAME_LAYOUT_SUBOBJECT = 0x24,
// Inner spell-tooltip builder, called from Script_GameTooltip_SetSpell at 0x00532E92.
// __thiscall(spellID, 0, 0, isPet, 0, 0, 0); we always pass isPet=0.
// Inner spell-tooltip builder (CGTooltip::BuildSpell), called from
// Script_GameTooltip_SetSpell at 0x00532E92. `uint __thiscall(spellID,
// compact, cooldownMs, pet, showRank, talent, rebuild)`, RET 0x1C — the
// first six of 4.3.4's CGTooltip::SetSpell(spellId, brief, cooldown,
// isPet, showSubtext, …, talent, …, append, …) in order. `rebuild`
// skips the clear and the spellID stash (+0x39C) and, unlike 4.3.4's
// append, always heads the block with "\n" TOOLTIP_TALENT_NEXT_RANK
// instead of the name line (talent or not). A TRADESPELL record builds
// its created item's tooltip instead. Returns 1 when it added the
// cooldown line (cooldownMs != 0, recovery >= 60s) or a chance-to line.
FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP = 0x0052E610,
// Existing GameTooltip method-table entries we dispatch to from
@@ -3783,9 +3791,12 @@ enum Offsets {
// The two default tooltip line colors, packed {b,g,r,a} like every
// FUN_GAMETOOLTIP_ADD_LINE color: text 0xFFFFFFFF (static init 0x00528E90)
// and title 0xFFFFD200 (static init 0x00528E50) — 4.3.4's
// s_tooltipTextColor / s_tooltipTitleColor.
// s_tooltipTextColor / s_tooltipTitleColor. The gray 0xFF808080 is the
// right column of the spell builder's name line (benilla-re
// s_tooltipGray, verified) — 4.3.4's s_tooltipGrayColor.
VAR_TOOLTIP_TEXT_COLOR = 0x00C0CF60,
VAR_TOOLTIP_TITLE_COLOR = 0x00C0D3E8,
VAR_TOOLTIP_GRAY_COLOR = 0x00C0D3C4,
FUN_SCRIPT_FRAME_SHOW = 0x00775750,
FUN_SCRIPT_FRAME_HIDE = 0x00775810,
FUN_SCRIPT_FRAME_SETMINRESIZE = 0x00776020,
@@ -10376,6 +10387,37 @@ enum Offsets {
// the previous NPC's stale arrays/text after close.
VAR_GOSSIP_NPC_GUID = 0x00BC3F58,
// u32, 1 while the greeting is unresolved. The greeting builder
// 0x004E2010 (its only writer) sets it on an NPC-text cache miss or
// textID 0 and clears it once the text resolves, right before
// GOSSIP_SHOW; the option selector 0x004E2320 refuses while it is
// set. The GUID above is latched earlier, so GUID != 0 alone counts
// a window that has not shown yet — with the previous NPC's text
// still in VAR_GOSSIP_GREETING_TEXT. Verified via xrefs (1 read, 2
// writes) and the 0x004E2010 decompile.
VAR_GOSSIP_TEXT_PENDING = 0x00BBB670,
// --- Gossip point of interest (SMSG_GOSSIP_POI, opcode 0x224) ------
// The handler 0x004E2840 stores the marker in static blip record 1
// (records 0x74 apart from 0x00CEA760: 0 = selected quest, 1 = gossip,
// 2 = corpse) through the setter 0x006DAC10, the record's only writer.
// There is one marker at a time; a new packet overwrites it. It is
// live while its position is nonzero: 0x006D93A0 zeroes x/y when the
// player comes within 10 yards or 480 s after it was set. The world
// map's landmark builder 0x004A67A0 reads it the same way and projects
// it with 0x004A7360, which drops it unless the displayed map's row
// has the record's map id and the point lands inside 0..1. Verified
// via the 0x006DAC10 / 0x004A67A0 / 0x004A7360 decompiles.
VAR_GOSSIP_POI_BLIP = 0x00CEA7D4,
VAR_GOSSIP_POI_NAME = 0x00CEA6E0, // char[0x40], slot 1 of the names at 0x00CEA6A0
// u32, wall-clock seconds (0x00429580) + 480 at each set; both clears
// zero it with x/y (0x006D93FB, 0x006D9A3A). 0 = no marker.
VAR_GOSSIP_POI_EXPIRY = 0x00CEAAA4,
OFF_STATIC_BLIP_ICON = 0x08, // u32, POI icon index (the packet's icon)
OFF_STATIC_BLIP_X = 0x10, // float, world X (north)
OFF_STATIC_BLIP_Y = 0x14, // float, world Y (west)
OFF_STATIC_BLIP_MAP = 0x1C, // i32, Map.dbc id: the minimap's map when set
// --- Quest-greeting state (SMSG_QUESTGIVER_QUEST_LIST) -------------
// Vanilla's SECOND questgiver path: NPCs without a gossip menu send
// SMSG_QUESTGIVER_QUEST_LIST, whose handler (FUN_005dbb10) fills a
@@ -11214,7 +11256,7 @@ enum Offsets {
GXSHADER_TYPE_PIXEL = 1,
GXSHADER_CODE_D3D = 2,
GXRS_PIXEL_SHADER = 0x3F, // pointer-form selector (0x40 = vertex shader); doc only — the engine's draw binds it
FUN_LUA_TO_BOOLEAN_LOOSE = 0x006F1C10, // __fastcall(L, idx, default): nil→0, boolean, number→int, "1/t/y…"→1, "0/f/n…"→0, "enabled"→1, "disabled"→0, other→default
FUN_LUA_TO_BOOLEAN_LOOSE = 0x006F1C10, // __fastcall(L, idx, default): nil→0, boolean, number→int, "1/t/y…"→1, "0/f/n…"→0, "enabled"→1, "disabled"→0, other→default. Called through Game::Lua::ToFlag (4.3.4 StringToBOOL(L, idx, default))
// --- Texture memory accounting: the zero-dimension hang -------------------
// FUN_GX_TEXTURE_MIP_BYTES totals a texture's bytes across its mip chain:
@@ -11885,7 +11927,9 @@ enum Offsets {
// (0x00779FC6), then reads its own attributes: "letters" (0x0077A046),
// "autoFocus" (0x0077A0A5), "multiLine", "numeric", "historyLines".
// Booleans go through FUN_XML_ATTR_TO_BOOL - `int __fastcall(const char *)`,
// 1 for text starting 1/t/y - as for autoFocus at 0x0077A0BC.
// 4.3.4's StringToBOOL(const char *): 1 for text starting 1-9/T/Y or equal
// to on/enabled, else 0 (null too) - as for autoFocus at 0x0077A0BC.
// Called through Game::StringToFlag.
FUN_XML_EDITBOX_LOAD = 0x00779FB0,
FUN_XML_ATTR_TO_BOOL = 0x006F1B30,
// Selection-highlight regions — three consecutive CSimpleTexture pointers
+2 -6
View File
@@ -471,14 +471,10 @@ namespace {
Group *Self(void *L) { return static_cast<Group *>(Resolve(L, 1, Group::s_class)); }
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
bool LooseBool(void *L, int idx, int dflt) {
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, dflt) != 0;
}
int __fastcall Script_Play(void *L) {
if (Group *g = Self(L))
g->Play(LooseBool(L, 2, 0));
g->Play(Game::Lua::ToFlag(L, 2));
return 0;
}
int __fastcall Script_Pause(void *L) {
@@ -620,7 +616,7 @@ int __fastcall Script_SetIgnoreFramerateThrottle(void *L) {
Group *g = Self(L);
if (g == nullptr)
return 0;
if (LooseBool(L, 2, 0))
if (Game::Lua::ToFlag(L, 2))
g->flags |= GROUP_IGNORE_THROTTLE;
else
g->flags &= ~GROUP_IGNORE_THROTTLE;
+4 -8
View File
@@ -63,7 +63,6 @@ using KeyDispatch_t = int(__fastcall *)(void *mgr, void *edx, const char *key, i
using CommandExecute_t = int(__fastcall *)(void *mgr, void *edx, const char *cmd, int isDown);
using GetCommandAction_t = const char *(__thiscall *)(void *mgr, const char *key);
using ScriptFn_t = int(__fastcall *)(void *L);
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
KeyDispatch_t g_keyDispatchOriginal = nullptr;
CommandExecute_t g_commandExecuteOriginal = nullptr;
@@ -260,9 +259,6 @@ int __fastcall CommandExecute_h(void *mgr, void *edx, const char *command, int i
// --- Lua ----------------------------------------------------------------------
bool LooseBool(void *L, int idx) {
return reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, 0) != 0;
}
const char *OptString(void *L, int idx) {
return Game::Lua::IsString(L, idx) ? Game::Lua::ToString(L, idx) : nullptr;
@@ -321,7 +317,7 @@ int __fastcall Script_SetOverrideBinding(void *L) {
Game::Lua::Error(L, "Usage: SetOverrideBinding(owner, isPriority, \"KEY\"[, \"COMMAND\"])");
return 0;
}
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
return 0;
}
@@ -332,7 +328,7 @@ int SetOverrideCommand(void *L, const char *prefix, const char *usage) {
return 0;
}
const std::string command = std::string(prefix) + Game::Lua::ToString(L, 4);
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
return 0;
}
@@ -358,7 +354,7 @@ int __fastcall Script_SetOverrideBindingClick(void *L) {
return 0;
}
const std::string command = ClickCommand(Game::Lua::ToString(L, 4), OptString(L, 5));
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
return 0;
}
@@ -376,7 +372,7 @@ int __fastcall Script_ClearOverrideBindings(void *L) {
// GetBindingAction(key [, checkOverride]): 4.3.4's GetCommandAction checks the
// override first when asked; the saved binding is the engine's own answer.
int __fastcall Script_GetBindingAction(void *L) {
if (Game::Lua::IsString(L, 1) && LooseBool(L, 2)) {
if (Game::Lua::IsString(L, 1) && Game::Lua::ToFlag(L, 2)) {
if (const char *command = ActiveOverride(Game::Lua::ToString(L, 1))) {
Game::Lua::PushString(L, command);
return 1;
+93 -2
View File
@@ -11,8 +11,9 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them, and
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler.
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them,
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler,
// and `GetCurrentChatEvent()`, which reads the whole payload back.
//
// 4.3.4's SendChatEvent (ChatFrame.cpp, 0x008E82E0) fires every chat event
// with "%s%s%s%s%s%s%d%d%s%d%d%s%u%b": the ten arguments 1.12 already passes,
@@ -33,6 +34,13 @@
// `GetCurrentChatGUID()` returns that same sender, as nil where arg12 is "",
// so code written for the function and code reading arg12 always agree.
//
// `GetCurrentChatEvent()` (a ClassicAPI extension) returns the event name and
// the arguments exactly as fired. 1.12's ChatFrame_OnEvent reads the `event`
// and `arg1..` globals, which any handler or hook running before it may
// overwrite; the payload read here is the one this fire passed. It lives only
// for the fire, and a chat event fired from another's handler restores the
// outer payload when it returns.
//
// The sender comes from a SenderScope. Chat::Dispatch opens one around the
// chat-message dispatch with the GUID it picks for that chat type; the text
// emote builder is co-hooked below because it fires without passing its GUID
@@ -70,6 +78,33 @@ using TextEmote_t = void(__fastcall *)(const uint64_t *senderGuid, int emote, co
TextEmote_t g_textEmoteOriginal = nullptr;
// The chat event being fired, as pushed. `argc` is 12 for the stock format and
// 10 when another loader's format was fired unextended.
struct Payload {
int eventId;
const char *text, *sender, *language, *channelString, *target, *flags;
int zoneChannelID, channelNumber;
const char *channelName;
int channelUnk;
uint32_t lineID;
const char *guid;
int argc;
};
const Payload *g_payload = nullptr;
// Points g_payload at a fire for its duration, restoring the outer one after.
class PayloadScope {
public:
explicit PayloadScope(const Payload &p) : prev_(g_payload) { g_payload = &p; }
~PayloadScope() { g_payload = prev_; }
PayloadScope(const PayloadScope &) = delete;
PayloadScope &operator=(const PayloadScope &) = delete;
private:
const Payload *prev_;
};
// 1.12's chat tags are "AFK", "DND" and "GM"; 4.3.4's IsStaffChatFlag also
// matches "DEV", which this client never sends.
bool IsStaffFlag(const char *flags) { return flags != nullptr && std::strcmp(flags, "GM") == 0; }
@@ -88,7 +123,10 @@ void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char
text = Chat::RaidMarkers::ForChatEvent(text, markerBuf, sizeof markerBuf);
// Another loader passing its own format: the arguments may not be ours to
// extend, so fire exactly what was asked.
Payload payload{eventId, text, sender, language, channelString, target, flags,
zoneChannelID, channelNumber, channelName, channelUnk, 0, "", 10};
if (fmt != reinterpret_cast<const char *>(Offsets::VAR_STR_CHAT_EVENT_FORMAT)) {
PayloadScope scope(payload);
fire(eventId, fmt, text, sender, language, channelString, target, flags, zoneChannelID,
channelNumber, channelName, channelUnk);
return;
@@ -97,6 +135,10 @@ void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char
char guid[Guid::STRING_SIZE] = "";
if (g_sender != 0)
Guid::FormatAsString(g_sender, guid, sizeof guid);
payload.lineID = lineID;
payload.guid = guid;
payload.argc = 12;
PayloadScope scope(payload);
fire(eventId, "%s%s%s%s%s%s%d%d%s%d%d%s", text, sender, language, channelString, target, flags,
zoneChannelID, channelNumber, channelName, channelUnk, static_cast<int>(lineID), guid);
}
@@ -115,6 +157,53 @@ int __fastcall Script_GetCurrentChatGUID(void *L) {
return 1;
}
// event name, arg1..argN — the same values the handler received.
int __fastcall Script_GetCurrentChatEvent(void *L) {
using namespace Game::Lua;
const Payload *p = g_payload;
if (p == nullptr || CheckStack(L, p->argc + 1) == 0)
return 0;
const auto *entry = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) +
p->eventId * Offsets::EVENT_ENTRY_STRIDE;
PushString(L, Game::Read<const char *>(entry, Offsets::OFF_EVENT_ENTRY_NAME));
// A null string pushes nil, as it does through the engine's dispatcher.
PushString(L, p->text);
PushString(L, p->sender);
PushString(L, p->language);
PushString(L, p->channelString);
PushString(L, p->target);
PushString(L, p->flags);
PushNumber(L, p->zoneChannelID);
PushNumber(L, p->channelNumber);
PushString(L, p->channelName);
PushNumber(L, p->channelUnk);
if (p->argc == 12) {
PushNumber(L, p->lineID);
PushString(L, p->guid);
}
return p->argc + 1;
}
const Game::Doc::Field kChatEventRets[] = {
Game::Doc::Req("event", "string", "The CHAT_MSG_* event name."),
Game::Doc::Opt("text", "string"),
Game::Doc::Opt("playerName", "string"),
Game::Doc::Opt("languageName", "string"),
Game::Doc::Opt("channelName", "string"),
Game::Doc::Opt("playerName2", "string"),
Game::Doc::Opt("specialFlags", "string"),
Game::Doc::Opt("zoneChannelID", "number"),
Game::Doc::Opt("channelIndex", "number"),
Game::Doc::Opt("channelBaseName", "string"),
Game::Doc::Opt("unused", "number"),
Game::Doc::Opt("lineID", "number"),
Game::Doc::Opt("guid", "string"),
};
const Game::Doc::Function kGetCurrentChatEvent{
"Returns the name and arguments of the CHAT_MSG_* event being handled now, or nothing "
"outside one.",
{}, kChatEventRets, "ChatGlobals", true};
// Once per process; a patched site no longer reaches FUN_FIRE_EVENT, so a
// second pass would refuse anyway.
bool g_applied = false;
@@ -139,6 +228,8 @@ void Apply() {
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID", &Script_GetCurrentChatGUID);
Game::Lua::RegisterGlobalFunction("GetCurrentChatEvent", &Script_GetCurrentChatEvent,
&kGetCurrentChatEvent);
Apply();
}
+3 -8
View File
@@ -82,17 +82,12 @@ constexpr int kMaxRetention = 0x7FFFFFFF / 1000;
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
using Tokenize_t = void(__stdcall *)(char **cursor, char *out, unsigned outSize,
const char *delims, int *outQuoted);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int dflt);
int StrCmpI(const char *a, const char *b) {
return reinterpret_cast<SStrCmpI_t>(static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I))(
a, b, kStormMaxStr);
}
bool ArgFlag(void *L, int idx) {
return reinterpret_cast<LooseBool_t>(
static_cast<uintptr_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE))(L, idx, 0) != 0;
}
bool Complete(uint32_t matched) {
return (matched & AFFILIATION_MASK) && (matched & REACTION_MASK) && (matched & CONTROL_MASK) &&
@@ -269,7 +264,7 @@ int __fastcall Script_CombatLogSetRetentionTime(void *L) {
}
int __fastcall Script_CombatLogGetNumEntries(void *L) {
const bool all = ArgFlag(L, 1);
const bool all = Game::Lua::ToFlag(L, 1);
int count = 0;
for (const Entry &e : g_entries) {
if (Matches(e, all))
@@ -286,7 +281,7 @@ int __fastcall Script_CombatLogSetCurrentEntry(void *L) {
}
g_current = -1;
int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
const bool all = ArgFlag(L, 2);
const bool all = Game::Lua::ToFlag(L, 2);
if (index <= 0) {
// 0 is the newest entry, -1 the one before it.
for (int i = Count() - 1; i >= 0; --i) {
@@ -320,7 +315,7 @@ int __fastcall Script_CombatLogAdvanceEntry(void *L) {
return 0;
}
int count = static_cast<int>(Game::Lua::ToNumber(L, 1));
const bool all = ArgFlag(L, 2);
const bool all = Game::Lua::ToFlag(L, 2);
if (count != 0) {
// The walk starts at the current entry, which counts when it matches.
int result = -1;
+1 -2
View File
@@ -86,7 +86,6 @@ const Game::HookAutoRegister _countHook{Offsets::FUN_EDITBOX_COUNT_CHARS, reinte
using Load_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status);
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
using AttrToBool_t = int(__fastcall *)(const char *text);
Load_t g_origEditBoxLoad = nullptr;
// `self` is the loader sub-object inside the edit box.
@@ -99,7 +98,7 @@ uintptr_t __fastcall EditBoxLoad_h(void *self, void * /*edx*/, const void *node,
if (value == nullptr || *value == '\0')
return result;
void *editBox = static_cast<uint8_t *>(self) - Offsets::OFF_XML_LOADER_SUBOBJECT;
if (reinterpret_cast<AttrToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value))
if (Game::StringToFlag(value))
g_counting.insert(editBox);
else
g_counting.erase(editBox);
+1 -2
View File
@@ -76,7 +76,6 @@ using Notify_t = void(__fastcall *)(uint32_t slot0, int sendToServer, int quiet)
using Usable_t = uint32_t(__fastcall *)(uint32_t slot0, int *outNoMana);
using Texture_t = const char *(__fastcall *)(uint32_t slot0);
using Script_t = int(__fastcall *)(void *L);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
// `__stdcall(a, packet)`, RET 8 — declared `__fastcall` with the two
// register slots passed through untouched, which is the same frame.
using ActionButtons_t = int(__fastcall *)(void *ecx, void *edx, void *a, void *packet);
@@ -193,7 +192,7 @@ int __fastcall UseAction_h(void *L) {
if (slot < 0 || slot >= static_cast<int>(kSlots))
return g_origUseAction(L);
const uint32_t slot0 = static_cast<uint32_t>(slot);
if (reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0) != 0) {
if (Game::Lua::ToFlag(L, 2)) {
if (const uint32_t id = CursorSetID()) {
PlaceSet(slot0, id);
return 0;
+2 -6
View File
@@ -34,16 +34,12 @@ namespace FontString::IndentedWordWrap {
namespace {
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
using SetStyleFlags_t = void(__fastcall *)(void *fontString, void *edx, uint32_t flags);
using UpdateObjects_t = void(__fastcall *)(void *font);
using ReadTextType_t = int(__fastcall *)(void *L);
constexpr uint32_t kIndented = Offsets::FONT_STYLE_INDENTED_WORD_WRAP;
bool ReadIndent(void *L) {
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0;
}
int PushIndented(void *L, uint32_t styleFlags) {
if (styleFlags & kIndented)
@@ -93,7 +89,7 @@ void *HtmlFont(void *L) {
template <void *(*Font)(void *)> int __fastcall Script_SetFontIndented(void *L) {
void *font = Font(L);
if (font != nullptr)
SetFontIndented(font, ReadIndent(L));
SetFontIndented(font, Game::Lua::ToFlag(L, 2, true));
return 0;
}
@@ -115,7 +111,7 @@ int __fastcall Script_SetFontStringIndented(void *L) {
void *fontString = Game::Lua::ResolveFontString(L);
if (fontString == nullptr)
return 0;
const bool indented = ReadIndent(L);
const bool indented = Game::Lua::ToFlag(L, 2, true);
Game::Ref<uint32_t>(fontString, Offsets::OFF_FONTSTRING_SETTABLE_STYLE) &= ~kIndented;
const uint32_t flags = Game::Read<uint32_t>(fontString, Offsets::OFF_FONTSTRING_FLAGS);
reinterpret_cast<SetStyleFlags_t>(Offsets::FUN_FONTSTRING_SET_STYLE_FLAGS)(
+1 -2
View File
@@ -32,7 +32,6 @@ namespace FontString::WordWrap {
namespace {
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
constexpr uint32_t kNoWrap = Offsets::FONT_STYLE_NO_WORD_WRAP;
@@ -40,7 +39,7 @@ int __fastcall Script_SetWordWrap(void *L) {
void *fontString = Game::Lua::ResolveFontString(L);
if (fontString == nullptr)
return 0;
const bool wrap = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0;
const bool wrap = Game::Lua::ToFlag(L, 2, true);
Game::Ref<uint32_t>(fontString, Offsets::OFF_FONTSTRING_SETTABLE_STYLE) &= ~kNoWrap;
const uint32_t flags = Game::Read<uint32_t>(fontString, Offsets::OFF_FONTSTRING_FLAGS);
const uint32_t wanted = wrap ? flags & ~kNoWrap : flags | kNoWrap;
+1 -2
View File
@@ -1100,7 +1100,6 @@ int __fastcall Script_GetAttribute(void *L) {
// inherited templates are loaded first, so the frame's own values win.
// Problems are the engine's load warnings, with 4.3.4's texts.
void LoadXmlAttributes(void *L, void *frame, const void *attributes, void *status) {
using ToBool_t = int(__fastcall *)(const char *);
using ToFloat_t = float(__stdcall *)(const char *);
const int top = Game::Lua::GetTop(L);
@@ -1138,7 +1137,7 @@ void LoadXmlAttributes(void *L, void *frame, const void *attributes, void *statu
if (isNil)
Game::Lua::PushNil(L);
else if (Ascii::EqualCI(type, "boolean"))
Game::Lua::PushBool(L, reinterpret_cast<ToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value) != 0);
Game::Lua::PushBool(L, Game::StringToFlag(value));
else if (Ascii::EqualCI(type, "number"))
Game::Lua::PushNumber(L, reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(value));
else
+4 -44
View File
@@ -655,8 +655,6 @@ void __fastcall OnFrameSizeChanged(void *layout, void *, const float *oldRect) {
ResetVerts(FrameOfLayout(layout));
}
bool StringToBool(const char *s);
// The stock CooldownFrameTemplate: a Model template (Cooldown.xml, 1.12).
bool IsStockTemplate(const void *node) {
if (!Xml::TagIs(node, "Model"))
@@ -691,9 +689,9 @@ uintptr_t __fastcall LoadXML(void *layout, void *, const void *node, void *statu
return result;
Fields &f = F(FrameOfLayout(layout));
if (const char *reverse = Xml::Attr(node, "reverse"))
f.reverse = StringToBool(reverse);
f.reverse = Game::StringToFlag(reverse);
if (const char *drawEdge = Xml::Attr(node, "drawEdge"))
f.drawEdge = StringToBool(drawEdge);
f.drawEdge = Game::StringToFlag(drawEdge);
return result;
}
@@ -743,44 +741,6 @@ void *__fastcall CreateCooldown(void *parent) {
void *ResolveCooldown(void *L) { return Game::Lua::ResolveTypedObject(L, 1, TypeIdVar()); }
// 4.3.4's StringToBOOL on a string (FrameXML/LoadXML.cpp 0x004EF0C0): the
// first letter (0/F/N false, 1-9/T/Y true), then off/disabled/on/enabled,
// and true for anything else.
bool StringToBool(const char *s) {
switch (*s) {
case '0': case 'F': case 'N': case 'f': case 'n':
return false;
case '1': case '2': case '3': case '4': case '5':
case '6': case '7': case '8': case '9':
case 'T': case 'Y': case 't': case 'y':
return true;
default:
break;
}
if (_stricmp(s, "off") == 0 || _stricmp(s, "disabled") == 0)
return false;
return true;
}
// StringToBOOL(L, index, 1): nil is false, a boolean is itself, a number is
// != 0, a string as above, and anything else the default.
bool ArgToBool(void *L, int index) {
switch (Game::Lua::Type(L, index)) {
case Game::Lua::TYPE_NIL:
return false;
case Game::Lua::TYPE_BOOLEAN:
return Game::Lua::ToBoolean(L, index) != 0;
case Game::Lua::TYPE_NUMBER:
return Game::Lua::ToNumber(L, index) != 0.0;
case Game::Lua::TYPE_STRING: {
const char *s = Game::Lua::ToString(L, index);
return s == nullptr || StringToBool(s);
}
default:
return true;
}
}
// OFFSET (4.3.4): 0x009FFBD0
int __fastcall Script_SetCooldown(void *L) {
void *frame = ResolveCooldown(L);
@@ -802,7 +762,7 @@ int __fastcall Script_SetReverse(void *L) {
void *frame = ResolveCooldown(L);
if (frame == nullptr)
return 0;
F(frame).reverse = ArgToBool(L, 2);
F(frame).reverse = Game::Lua::ToFlag(L, 2, true);
return 0;
}
@@ -821,7 +781,7 @@ int __fastcall Script_SetDrawEdge(void *L) {
if (frame == nullptr)
return 0;
Fields &f = F(frame);
const uint32_t drawEdge = ArgToBool(L, 2);
const uint32_t drawEdge = Game::Lua::ToFlag(L, 2, true);
if (drawEdge != f.drawEdge) {
f.drawEdge = drawEdge;
NotifyLayerChanged(frame, 3);
+2 -3
View File
@@ -36,7 +36,6 @@ namespace {
using SaveFrame_t = void(__fastcall *)(void *frame, void *file);
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
SaveFrame_t g_origSaveFrame = nullptr;
@@ -58,9 +57,9 @@ int __fastcall Script_SetDontSavePosition(void *L) {
void *frame = Game::Lua::ResolveFrame(L);
if (frame == nullptr)
return 0;
const int on = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1);
const bool on = Game::Lua::ToFlag(L, 2, true);
reinterpret_cast<SetFrameFlag_t>(Offsets::FUN_FRAME_SET_FRAME_FLAG)(
frame, nullptr, Offsets::FRAME_FLAG_DONT_SAVE_POSITION, on != 0);
frame, nullptr, Offsets::FRAME_FLAG_DONT_SAVE_POSITION, on);
return 0;
}
+1 -2
View File
@@ -56,7 +56,6 @@ namespace {
// other things.
std::unordered_set<void *> g_disabled;
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
bool OnRun(void *frame, uint32_t *slotPtr, const char * /*fmt*/, void * /*varargs*/) {
if (g_disabled.empty() || g_disabled.count(frame) == 0)
@@ -71,7 +70,7 @@ int __fastcall Script_SetHyperlinksEnabled(void *L) {
void *frame = Game::Lua::ResolveFrame(L);
if (frame == nullptr)
return 0;
if (reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0)
if (Game::Lua::ToFlag(L, 2, true))
g_disabled.erase(frame);
else
g_disabled.insert(frame);
+2 -3
View File
@@ -47,7 +47,6 @@ namespace {
using KeySlot_t = int(__fastcall *)(void *frame, void *edx, void *event);
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
KeySlot_t g_origFrameOnChar = nullptr;
KeySlot_t g_origFrameOnKeyDown = nullptr;
@@ -86,9 +85,9 @@ int __fastcall Script_SetPropagateKeyboardInput(void *L) {
void *frame = Game::Lua::ResolveFrame(L);
if (frame == nullptr)
return 0;
const int on = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0);
const bool on = Game::Lua::ToFlag(L, 2);
reinterpret_cast<SetFrameFlag_t>(Offsets::FUN_FRAME_SET_FRAME_FLAG)(
frame, nullptr, Offsets::FRAME_FLAG_PROPAGATE_KEYBOARD, on != 0);
frame, nullptr, Offsets::FRAME_FLAG_PROPAGATE_KEYBOARD, on);
return 0;
}
+2 -4
View File
@@ -48,8 +48,6 @@ namespace {
using OnEnter_t = void(__fastcall *)(void *button, void *edx);
using OnLeave_t = void(__fastcall *)(void *button, void *edx, int removed);
using FireScript_t = void(__fastcall *)(void *frame, void *edx, int *slot);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
using AttrToBool_t = int(__fastcall *)(const char *text);
std::unordered_set<const void *> g_enabled;
@@ -99,7 +97,7 @@ void OnFrameNode(void *object, const void *node, void * /*status*/) {
const char *value = Xml::Attr(node, "motionScriptsWhileDisabled");
if (value == nullptr || *value == '\0' || !Game::Lua::IsA(object, Offsets::VAR_BUTTON_LUA_TYPE_ID))
return;
Set(object, reinterpret_cast<AttrToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value) != 0);
Set(object, Game::StringToFlag(value));
}
const Xml::FrameLoad::AutoSubscribe _xmlSub{&OnFrameNode};
@@ -115,7 +113,7 @@ int __fastcall Script_SetMotionScriptsWhileDisabled(void *L) {
void *button = ResolveButton(L);
if (button == nullptr)
return 0;
Set(button, reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0) != 0);
Set(button, Game::Lua::ToFlag(L, 2));
return 0;
}
+25 -9
View File
@@ -30,7 +30,8 @@
// block in Offsets.h). Retail keeps the two APIs disjoint, but for
// backporting it's far more useful for `C_GossipInfo` to serve
// whichever questgiver session is actually live:
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0) → gossip arrays
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0
// and greeting not pending) → gossip arrays
// - greeting session live (VAR_QUESTGIVER_GUID != 0
// and == VAR_GREETING_NPC_GUID) → greeting arrays
// - neither → empty / ""
@@ -68,10 +69,15 @@ uint64_t ReadU64(uintptr_t va) {
return *reinterpret_cast<const uint64_t *>(va);
}
// A gossip session is live from SMSG_GOSSIP_MESSAGE until the close
// worker (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED.
// A gossip session is live from GOSSIP_SHOW until the close worker
// (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED. The GUID is
// latched on SMSG_GOSSIP_MESSAGE, before the greeting resolves, so the
// engine's own pending flag (what its option selector checks) keeps the
// not-yet-shown window — and the previous NPC's text — out.
bool GossipSessionActive() {
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0;
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0 &&
*reinterpret_cast<const uint32_t *>(
Offsets::VAR_GOSSIP_TEXT_PENDING) == 0;
}
// A quest-greeting session is live while the quest-session GUID is set
@@ -427,18 +433,28 @@ int __fastcall Script_C_GossipInfo_SelectOption(void *L) {
// `C_GossipInfo.SelectOptionByIndex(orderIndex)` — `orderIndex` is
// 1-based in display order (matches what `GetOptions()` puts on each
// entry). The engine helper takes the 0-based array slot.
// entry). Slots with a negative index are hidden and skipped, so the
// display position is walked back to the 0-based array slot the engine
// helper takes, the same way `GetOptions()` numbered it.
int __fastcall Script_C_GossipInfo_SelectOptionByIndex(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L,
"Usage: C_GossipInfo.SelectOptionByIndex(orderIndex)");
return 0;
}
const int slot0Based = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
if (slot0Based < 0 || slot0Based >= Offsets::GOSSIP_OPTIONS_MAX ||
!GossipSessionActive())
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (!GossipSessionActive())
return 0;
EngineSelectOption(slot0Based, nullptr);
int orderIndex = 0;
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
if (OptionIndex(OptionEntry(slot)) < 0)
continue;
if (++orderIndex == target) {
EngineSelectOption(slot, nullptr);
return 0;
}
}
return 0;
}
+185
View File
@@ -0,0 +1,185 @@
// 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/>.
// `C_GossipInfo` point of interest — the map marker a gossip option such as
// a city guard's directions places (SMSG_GOSSIP_POI):
//
// GetPoiForUiMapID(uiMapID) -> gossipPoiID or nil
// GetPoiInfo(uiMapID, gossipPoiID) -> GossipPoiInfo or nil
//
// The engine keeps the marker in static blip record 1 (see
// `VAR_GOSSIP_POI_BLIP` in Offsets.h): one marker at a time, live while its
// position is nonzero. Both functions read that record and apply the world
// map's own placement test (0x004A7360): the map's WorldMapArea row must
// carry the marker's map id and the projected point must land inside 0..1.
//
// The packet carries no POI id, and there is only ever one marker, so its
// `gossipPoiID` is the engine's slot number, 1.
//
// `DYNAMIC_GOSSIP_POI_UPDATED` fires when the marker is set or cleared. Every
// change runs the world map rebuild 0x004A67A0 (the setter after a packet, the
// 480 s expiry and the 10-yard clear in 0x006D93A0), but that rebuild also
// runs for every map view change, so the event comes from a once-per-frame
// diff of the record instead: x, y and the expiry, which each set rewrites
// and each clear zeroes.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "map/Area.h"
#include "map/Vector2.h"
#include "tick/WorldTick.h"
#include <cstdint>
namespace Gossip::Poi {
namespace {
constexpr int kGossipPoiID = 1;
template <typename T> T BlipField(uintptr_t off) {
return *reinterpret_cast<const T *>(Offsets::VAR_GOSSIP_POI_BLIP + off);
}
const Game::Doc::Event kPoiUpdatedDoc{
"GossipInfo", "Fires when a gossip point of interest is added to or removed from the map.",
{}};
const Event::Custom::AutoReserve _evtPoiUpdated{"DYNAMIC_GOSSIP_POI_UPDATED",
&kPoiUpdatedDoc};
struct Snapshot {
uint32_t x, y, expiry;
};
// The record as of the last fire. DLL-lived like the record itself, so a
// /reload with a marker up does not fire a phantom change.
Snapshot g_last{};
void OnTick() {
const Snapshot now{BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_X),
BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_Y),
*reinterpret_cast<const uint32_t *>(Offsets::VAR_GOSSIP_POI_EXPIRY)};
if (now.x == g_last.x && now.y == g_last.y && now.expiry == g_last.expiry)
return;
g_last = now;
Event::Custom::Fire(_evtPoiUpdated.Slot(), "");
}
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
// Projects the live marker onto `uiMapID`. False when there is no marker or
// it isn't on that map.
bool ProjectOnMap(int uiMapID, double *outPx, double *outPy) {
const float x = BlipField<float>(Offsets::OFF_STATIC_BLIP_X);
const float y = BlipField<float>(Offsets::OFF_STATIC_BLIP_Y);
if (x == 0.0f && y == 0.0f)
return false;
const int row = Map::Area::RowForUiMapID(uiMapID);
int rowMapID = 0;
return row > 0 &&
Map::Area::RowRect(row, nullptr, nullptr, nullptr, nullptr, &rowMapID) &&
rowMapID == BlipField<int32_t>(Offsets::OFF_STATIC_BLIP_MAP) &&
Map::Area::PercentInRow(row, x, y, outPx, outPy) &&
*outPx >= 0.0 && *outPx <= 1.0 && *outPy >= 0.0 && *outPy <= 1.0;
}
// `C_GossipInfo.GetPoiForUiMapID(uiMapID)` — the id of the gossip marker
// shown on that map, or nil.
int __fastcall Script_GetPoiForUiMapID(void *L) {
const int uiMapID =
Game::Lua::IsNumber(L, 1) ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
double px = 0.0, py = 0.0;
const bool have = uiMapID != 0 && ProjectOnMap(uiMapID, &px, &py);
Game::Lua::SetTop(L, 0);
if (!have)
Game::Lua::PushNil(L);
else
Game::Lua::PushNumber(L, static_cast<double>(kGossipPoiID));
return 1;
}
// `C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)` — the marker's name, icon
// index and 0..1 position on that map, or nil.
int __fastcall Script_GetPoiInfo(void *L) {
const bool ok = Game::Lua::IsNumber(L, 1) && Game::Lua::IsNumber(L, 2);
const int uiMapID = ok ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
const int poiID = ok ? static_cast<int>(Game::Lua::ToNumber(L, 2)) : 0;
double px = 0.0, py = 0.0;
const bool have = poiID == kGossipPoiID && ProjectOnMap(uiMapID, &px, &py);
Game::Lua::SetTop(L, 0);
if (!have) {
Game::Lua::PushNil(L);
return 1;
}
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(
L, "name", reinterpret_cast<const char *>(Offsets::VAR_GOSSIP_POI_NAME));
Game::Lua::SetFieldNumber(
L, "textureIndex",
static_cast<double>(BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_ICON)));
Game::Lua::PushString(L, "position");
Map::PushVector2D(L, px, py);
Game::Lua::SetTable(L, -3);
Game::Lua::SetFieldBool(L, "inBattleMap", false);
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kForMapArgs[] = {
Game::Doc::Req("uiMapID", "number"),
};
const Game::Doc::Field kForMapRets[] = {
Game::Doc::Opt("gossipPoiID", "number"),
};
const Game::Doc::Function kGetPoiForUiMapID{
"Returns the id of the gossip point of interest shown on the map, if any.",
kForMapArgs, kForMapRets};
const Game::Doc::Field kPoiInfoFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("textureIndex", "number", "The icon's index in the map POI icon texture."),
Game::Doc::Req("position", "vector2", "The point on the map, from 0 to 1 on each axis."),
Game::Doc::Req("inBattleMap", "bool"),
};
const Game::Doc::Structure kGossipPoiInfo{
"GossipPoiInfo", "GossipInfo", kPoiInfoFields,
"A point of interest that a gossip option marked on the map."};
const Game::Doc::Field kInfoArgs[] = {
Game::Doc::Req("uiMapID", "number"),
Game::Doc::Req("gossipPoiID", "number"),
};
const Game::Doc::Field kInfoRets[] = {
Game::Doc::Opt("gossipPoiInfo", "GossipPoiInfo"),
};
const Game::Doc::Function kGetPoiInfo{
"Returns the gossip point of interest as it appears on the map.", kInfoArgs,
kInfoRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_GossipInfo", "GetPoiForUiMapID",
&Script_GetPoiForUiMapID, &kGetPoiForUiMapID);
Game::Lua::RegisterTableFunction("C_GossipInfo", "GetPoiInfo", &Script_GetPoiInfo,
&kGetPoiInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Gossip::Poi
+2 -4
View File
@@ -46,7 +46,6 @@ namespace Item::Count {
namespace {
using CountByID_t = int(__thiscall *)(const void *invMgr, int itemID, uint32_t flags);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
int CountByID(uintptr_t fn, int itemID, uint32_t flags) {
const uint8_t *invMgr = Unit::Identity::PlayerInventoryManager();
@@ -86,9 +85,8 @@ int CountUses(int itemID, int total) {
}
int __fastcall Script_C_Item_GetItemCount(void *L) {
const auto looseBool = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE);
const bool includeBank = looseBool(L, 2, 0) != 0;
const bool includeUses = looseBool(L, 3, 0) != 0;
const bool includeBank = Game::Lua::ToFlag(L, 2);
const bool includeUses = Game::Lua::ToFlag(L, 3);
const uint32_t flags = includeBank ? Offsets::INVMGR_SEARCH_BANK : 0;
const Item::Arg::Resolved arg = Item::Arg::Resolve(L, 1);
+7 -2
View File
@@ -98,12 +98,17 @@ const Game::Doc::Function kGetSpellCooldown{
bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
bool *enabled) {
int pet = 0;
Spell::Lookup::FindSpellbookSlot(spellID, &pet);
return Query(spellID, pet != 0, startMs, durationMs, enabled);
}
bool Query(int spellID, bool pet, uint32_t *startMs, uint32_t *durationMs,
bool *enabled) {
if (spellID <= 0 || Spell::Lookup::RecordForID(spellID) == nullptr)
return false;
auto fn = reinterpret_cast<QueryCooldown_t>(
static_cast<uintptr_t>(Offsets::FUN_SPELL_QUERY_COOLDOWN));
int pet = 0;
Spell::Lookup::FindSpellbookSlot(spellID, &pet);
uint32_t duration = 0, start = 0, enable = 0;
fn(spellID, pet, &duration, &start, &enable);
*startMs = start;
+6
View File
@@ -28,4 +28,10 @@ namespace Spell::Cooldown {
bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
bool *enabled);
// Same, read from the history the caller names (`pet` = the pet's) — the
// shape 4.3.4's `Spell_C_GetSpellCooldown(spellId, isPet, …)` takes, for
// callers that are handed the history instead of deriving it.
bool Query(int spellID, bool pet, uint32_t *startMs, uint32_t *durationMs,
bool *enabled);
} // namespace Spell::Cooldown
+103 -91
View File
@@ -13,20 +13,106 @@
#include "Game.h"
#include "Offsets.h"
#include "spell/Cooldown.h"
#include "spell/Lookup.h"
#include "spell/Tooltip.h"
#include "time/Clock.h"
#include "tooltip/Lines.h"
#include <cstdint>
namespace Spell::Tooltip {
// Spell.dbc record offsets used by GetSpell. Kept local rather than
// pulled from Spell::Info — that module keeps these as `static
// constexpr` privates and exposing them just for two readers here
// would force a header change.
namespace {
using BuildSpellTooltip_t = void(__thiscall *)(void *thisObj, int spellID, int arg2, int arg3,
int isPet, int showRank, int arg6, int arg7);
// CGTooltip::BuildSpell — see FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP.
using BuildSpell_t = uint32_t(__thiscall *)(void *tooltip, int spellID, int compact,
uint32_t cooldownMs, int pet, int showRank,
const void *talent, int rebuild);
uint32_t BuildSpell(void *tooltip, int spellID, uint32_t cooldownMs, bool pet, bool showRank,
bool rebuild) {
return reinterpret_cast<BuildSpell_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP)(
tooltip, spellID, 0, cooldownMs, pet ? 1 : 0, showRank ? 1 : 0, nullptr, rebuild ? 1 : 0);
}
// The cooldown argument 4.3.4's Script_SetSpellByID computes: the time left
// when the spell's cooldown is enabled, running and started, else 0.
uint32_t CooldownRemainingMs(int spellID, bool pet) {
uint32_t start = 0, duration = 0;
bool enabled = false;
if (!Spell::Cooldown::Query(spellID, pet, &start, &duration, &enabled) || !enabled ||
duration == 0 || start == 0)
return 0;
return Time::Clock::Remaining(start + duration);
}
// The builder's append mode heads the block with its talent "Next rank:"
// line, where 4.3.4's append (no talent) adds the ordinary name line. Rewrite
// that line in place with the engine's own add-line — the same
// `AddDoubleLine(name, subtext, text, gray)` the builder's non-append path
// makes — by pointing the line cursor back at it for the one call.
void RewriteAppendHeader(void *tooltip, int headerLine, int spellID, bool showSubtext) {
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
if (record == nullptr ||
(Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
Offsets::SPELL_ATTR_TRADESPELL) != 0)
return; // a recipe appends its created item's tooltip, which has no header
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
const char *name = Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
if (name == nullptr || name[0] == '\0')
return;
const char *subtext =
showSubtext ? Game::Read<const char *>(record, Offsets::OFF_SPELL_RECORD_RANK + locale * 4)
: nullptr;
const uint32_t textColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_TEXT_COLOR);
const uint32_t grayColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_GRAY_COLOR);
int *numLines = Game::Ptr<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
const int after = *numLines;
*numLines = headerLine;
::Tooltip::Lines::AddDouble(tooltip, name, subtext, textColor, grayColor);
*numLines = after;
}
// 4.3.4's Script_SetSpellByID / Script_AddSpellByID (Tooltip_Scripts.cpp
// 0x004733C0 / 0x00473530), which differ only in `append`: (spellID
// [, isPet [, showSubtext]]), the spell's cooldown passed to the builder,
// 1 when it added the cooldown line (FrameXML's refresh cue), else nil.
// Both raise the same message, SetSpellByID's.
int SetOrAddSpellByID(void *L, bool append) {
void *tooltip = Game::Lua::ResolveTooltip(L);
if (tooltip == nullptr)
return 0;
int spellID = 0;
if (!Game::Lua::IsNumber(L, 2) ||
(spellID = static_cast<int>(Game::Lua::ToNumber(L, 2))) < 0) {
Game::Lua::Error(L, "Invalid spell ID in %s:SetSpellByID",
Game::Lua::ObjectDisplayName(tooltip));
return 0;
}
const bool isPet = Game::Lua::ToFlag(L, 3);
const bool showSubtext = Game::Lua::ToFlag(L, 4);
const int before = Game::Read<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
const uint32_t result = BuildSpell(tooltip, spellID, CooldownRemainingMs(spellID, isPet),
isPet, showSubtext, append);
if (append && Game::Read<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES) > before)
RewriteAppendHeader(tooltip, before, spellID, showSubtext);
if (result != 0)
Game::Lua::PushNumber(L, 1.0);
else
Game::Lua::PushNil(L);
return 1;
}
int __fastcall Script_GameTooltipSetSpellByID(void *L) { return SetOrAddSpellByID(L, false); }
int __fastcall Script_GameTooltipAddSpellByID(void *L) { return SetOrAddSpellByID(L, true); }
} // namespace
void ShowByID(void *L, int spellID) {
if (spellID <= 0)
@@ -34,88 +120,7 @@ void ShowByID(void *L, int spellID) {
void *tooltipObj = Game::Lua::ResolveTooltip(L);
if (tooltipObj == nullptr)
return;
auto BuildSpellTooltip =
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, /*showRank=*/1, 0, 0);
}
static int __fastcall Script_GameTooltipSetSpellByID(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GameTooltipSetSpellByID(self, spellID)");
return 0;
}
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: GameTooltipSetSpellByID(self, spellID)");
return 0;
}
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 2));
ShowByID(L, spellID);
return 0;
}
// `GameTooltip:AddSpellByID(spellID)` — APPENDS a spell's tooltip to the
// current one instead of clearing+rebuilding (the `SetSpellByID` behavior).
// The natural pair to `SetSpellByID`; lets callers compose tooltips like
// `AddLine(talentName) + AddSpellByID(rankSpellID)`.
//
// `BuildSpellTooltip`'s last arg (`param_7`) gates the clear: `0` calls the
// per-tooltip Clear (`FUN_00530050`) first — that's `SetSpellByID`; non-zero
// SKIPS the clear and appends. The append path is the engine's talent
// "next rank" preview, so it (a) emits a `"\nNext rank:"` header line
// instead of the spell name, and (b) does NOT stash the displayed spellID at
// `+0x39C` (so `GetSpell` keeps reflecting the base tooltip — correct for an
// append). We fix (a) by overwriting that header line — which lands at index
// `numLines`-before-the-build (0-based) — with the real spell name via the
// line pool's left-text FontString. Verified against `FUN_0052E610`.
void AppendByID(void *L, int spellID) {
if (spellID <= 0)
return;
void *tooltipObj = Game::Lua::ResolveTooltip(L);
if (tooltipObj == nullptr)
return;
auto *tt = static_cast<uint8_t *>(tooltipObj);
const int before = Game::Read<int>(
tt, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
auto BuildSpellTooltip =
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, 0, 0, /*param_7 (append)=*/1);
const int after = Game::Read<int>(
tt, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
if (after <= before)
return; // nothing appended (bad spellID / missing Spell.dbc record)
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
if (record == nullptr)
return;
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
const char *name = Game::Read<const char *>(
record, Offsets::OFF_SPELL_NAMES + locale * 4);
if (name == nullptr || name[0] == '\0')
return;
// Left-text FontString array: descriptor at +0x324, data ptr at +0x8.
auto **textLeft = Game::Read<void **>(
tt, Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC + 8);
if (textLeft == nullptr)
return;
void *fs = textLeft[before];
if (fs == nullptr)
return;
using SetText_t = void(__thiscall *)(void *fs, const char *text, int flag);
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(fs, name, 0);
}
static int __fastcall Script_GameTooltipAddSpellByID(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE ||
!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: GameTooltip:AddSpellByID(spellID)");
return 0;
}
AppendByID(L, static_cast<int>(Game::Lua::ToNumber(L, 2)));
return 0;
BuildSpell(tooltipObj, spellID, 0, false, /*showRank=*/true, false);
}
// `GameTooltip:GetSpell()` → (name, rank, spellID) for whichever spell
@@ -196,13 +201,20 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
// --- Documentation ----------------------------------------------------------
static const Game::Doc::Field kSpellIDArg[] = {
static const Game::Doc::Field kSpellByIDArgs[] = {
Game::Doc::Req("spellID", "number", "Any spell ID, learned or not."),
Game::Doc::Opt("isPet", "boolean", "false", "Read cost, range and cooldown as the pet's spell."),
Game::Doc::Opt("showSubtext", "boolean", "false", "Show the spell's rank to the right of its name."),
};
static const Game::Doc::Field kSpellByIDRets[] = {
Game::Doc::Opt("hasCooldown", "number", nullptr,
"1 when the tooltip shows a cooldown that is counting down, else nil."),
};
static const Game::Doc::Function kSetSpellByID{
"Fills the tooltip with a spell's tooltip.", kSpellIDArg, {}};
"Fills the tooltip with a spell's tooltip.", kSpellByIDArgs, kSpellByIDRets};
static const Game::Doc::Function kAddSpellByID{
"Appends a spell's tooltip to what the tooltip already shows.", kSpellIDArg, {}};
"Appends a spell's tooltip to what the tooltip already shows.", kSpellByIDArgs,
kSpellByIDRets};
static const Game::Doc::Field kGetSpellRets[] = {
Game::Doc::Opt("name", "string", nullptr, "Nil when the tooltip shows no spell."),
+8 -19
View File
@@ -15,26 +15,15 @@
namespace Spell::Tooltip {
// Resolves the `GameTooltip` self at Lua stack[1] (via the
// engine's `FrameScript_PushObject`/`FrameScript_GetObject` chain)
// and dispatches the engine's spell-tooltip builder for `spellID`,
// with the spell's rank shown in the top-right corner of the title
// line (matching `SetAction`/`SetPetAction`; native `SetSpell` omits it).
// Silent no-op if `spellID <= 0` or stack[1] isn't a frame object.
// Resolves the `GameTooltip` self at Lua stack[1] and runs the engine's
// spell-tooltip builder for `spellID` with the rank shown in the top-right
// corner of the name line (as `SetAction` shows it) and no cooldown line.
// Silent no-op if `spellID <= 0` or stack[1] isn't a tooltip.
//
// Used by `GameTooltip:SetSpellByID` directly, and by
// `GameTooltip:SetTalentByID` as a fallback for cross-class talents
// (where the engine's per-player TalentEntry tables don't apply,
// so we render the rank-1 spell tooltip instead).
// Used by `GameTooltip:SetTalentByID` as a fallback for cross-class talents
// (where the engine's per-player TalentEntry tables don't apply, so we
// render the rank-1 spell tooltip instead) and by `spell:` hyperlinks.
// `GameTooltip:SetSpellByID` has its own 4.3.4-shaped arguments.
void ShowByID(void *L, int spellID);
// Like `ShowByID` but APPENDS the spell's tooltip to the current one
// (no clear), and overwrites the engine's talent "next rank" header with
// the real spell name. Backs `GameTooltip:AddSpellByID`. Silent no-op if
// `spellID <= 0`, stack[1] isn't a frame object, or the build added no
// lines. Does not stash the displayed spellID (so `GetSpell` still
// reflects the base tooltip). Useful for cross-class talent tooltips
// (`AddLine(talentName) + AppendByID(rankSpellID)`).
void AppendByID(void *L, int spellID);
} // namespace Spell::Tooltip
+2 -6
View File
@@ -264,17 +264,13 @@ const State *Find(void *bar) {
void *ResolveBar(void *L) { return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_STATUSBAR_LUA_TYPE_ID); }
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
bool ArgBool(void *L, int idx) {
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, 0) != 0;
}
int __fastcall Script_SetReverseFill(void *L) {
void *bar = ResolveBar(L);
if (bar == nullptr)
return 0;
const bool reverse = ArgBool(L, 2);
const bool reverse = Game::Lua::ToFlag(L, 2);
Modify(bar, [reverse](State &s) { s.reverseFill = reverse; });
return 0;
}
@@ -292,7 +288,7 @@ int __fastcall Script_SetRotatesTexture(void *L) {
void *bar = ResolveBar(L);
if (bar == nullptr)
return 0;
const bool rotates = ArgBool(L, 2);
const bool rotates = Game::Lua::ToFlag(L, 2);
Modify(bar, [rotates](State &s) { s.rotatesTexture = rotates; });
// 4.3.4 sets the full coordinates at once, so a bar with no value yet
// shows its image turned, or not, before the first layout. Full
+1 -4
View File
@@ -67,7 +67,6 @@ namespace {
using Cycle_t = void(__fastcall *)(int reverse, int mode);
using Valid_t = int(__fastcall *)(void *player, void *cand, int mode);
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
using SetSelection_t = void(__stdcall *)(uint32_t guidLo, uint32_t guidHi);
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
@@ -110,9 +109,7 @@ const Game::HookAutoRegister _validHook{Offsets::FUN_TARGET_CANDIDATE_VALID,
reinterpret_cast<void **>(&g_validOriginal)};
void Cycle(void *L, int mode) {
const int reverse =
reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 1, 0);
reinterpret_cast<Cycle_t>(Offsets::FUN_TARGET_NEAREST_CYCLE)(reverse, mode);
reinterpret_cast<Cycle_t>(Offsets::FUN_TARGET_NEAREST_CYCLE)(Game::Lua::ToFlag(L, 1), mode);
}
void SetSelection(uint64_t guid) {
+1 -3
View File
@@ -66,7 +66,6 @@ using ShaderCreate_t = void(__fastcall *)(int type, void **outSlot, const char *
using ArrayGrow_t = void(__thiscall *)(void *desc, uint32_t newCap);
using CompilePs_t = void(__thiscall *)(void *device, void *shader);
using SetShader_t = void(__thiscall *)(void *region, void *shader);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
using ScriptFn_t = int(__fastcall *)(void *L);
using GetUiPixelShader_t = void *(__fastcall *)(int index);
@@ -380,8 +379,7 @@ int __fastcall Script_SetDesaturated(void *L) {
Game::Lua::PushNil(L);
return 1;
}
const int flag = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1);
if (!Apply(region, flag != 0 ? kFull : 0)) {
if (!Apply(region, Game::Lua::ToFlag(L, 2, true) ? kFull : 0)) {
Game::Lua::PushNil(L);
return 1;
}
+2 -3
View File
@@ -57,15 +57,14 @@ namespace {
using OnSpriteRightClick_t = int(__stdcall *)(uint32_t guidLo, uint32_t guidHi);
using TokenToGuid_t = uint64_t(__fastcall *)(const char *token);
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
uint64_t ResolveGuid(void *L, const char *token) {
if (_stricmp(token, "mouseover") == 0)
return Game::Read<uint64_t>(Offsets::VAR_MOUSEOVER_GUID_LO);
if (Unit::TokenResolve::IsUnitToken(token))
return reinterpret_cast<TokenToGuid_t>(Offsets::FUN_TOKEN_TO_GUID)(token);
const int exact = reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0);
return Unit::Identity::FindGuidByName(token, Offsets::TYPEMASK_OBJECT, exact != 0);
return Unit::Identity::FindGuidByName(token, Offsets::TYPEMASK_OBJECT,
Game::Lua::ToFlag(L, 2));
}
bool Interact(void *L) {
+1 -2
View File
@@ -64,11 +64,10 @@ Kind KindOf(const char *type) {
}
void Push(void *L, Kind kind, const char *text) {
using ToBool_t = int(__fastcall *)(const char *);
using ToFloat_t = float(__stdcall *)(const char *);
switch (kind) {
case Kind::Boolean:
Game::Lua::PushBool(L, reinterpret_cast<ToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0);
Game::Lua::PushBool(L, Game::StringToFlag(text));
break;
case Kind::Number:
Game::Lua::PushNumber(L, reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(text));