mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
unit: fire UPDATE_MOUSEOVER_UNIT on mouseover loss
Vanilla fires UPDATE_MOUSEOVER_UNIT only when a mouseover unit is gained;
moving the cursor off a unit clears the mouseover and signals nothing.
Retail fires for both gain and loss, so addons watching
UnitExists("mouseover") can react when it goes false. This restores the
loss fire.
Co-hooks the engine's mouseover set/clear chokepoint (FUN_00492890) --
the ONLY writer of the mouseover GUID globals, so every gain/loss/change
flows through it. Resolves whether the mouseover is a unit before and
after the original and fires the event on the unit->non-unit transition.
Gating on unit TYPE (not just "GUID present -> absent") is load-bearing:
the same GUID slot also holds gameobjects and items, which never fire a
gain, so a naive present->absent test spuriously fired when moving off a
herb node or chest (reported in testing). The unit|player object-mask
resolve fixes that; the !hasUnit half prevents a double-fire on any
->unit transition the engine already covered inline.
Change-gated target (per-frame would spam the gain event while
hovering), so it's a cool hook. Documented in API.md (loss-fire fix
subsection + TOC) and Events.md notes.
This commit is contained in:
+55
@@ -129,6 +129,7 @@ build instructions.
|
||||
- [`QUEST_REMOVED` event](#quest_removed-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
- [`UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)](#update_mouseover_unit-event-loss-fire-fix)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
|
||||
- [Expansion](#expansion)
|
||||
@@ -3123,6 +3124,60 @@ byte reads (cheap) and naturally handles the case where one packet
|
||||
carries multiple rep updates whose threshold crossings ripple AT_WAR
|
||||
state across unrelated slots.
|
||||
|
||||
### `UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)
|
||||
|
||||
`UPDATE_MOUSEOVER_UNIT` exists in the vanilla event table and fires when a
|
||||
mouseover unit is **gained**, but the 1.12 engine **never fires it on
|
||||
loss** — moving the cursor off a unit clears the mouseover and signals
|
||||
nothing. Modern WoW fires it for both gain and loss, so addons observing
|
||||
`UnitExists("mouseover")` can react when it becomes false. We restore the
|
||||
loss fire.
|
||||
|
||||
Payload is unchanged (none in vanilla — same as retail): the event
|
||||
carries no args; handlers read `UnitExists("mouseover")`, which is
|
||||
`nil`/false at fire time on the loss path.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "UPDATE_MOUSEOVER_UNIT" then
|
||||
if UnitExists("mouseover") then
|
||||
-- gained (or changed to) a mouseover unit
|
||||
else
|
||||
-- mouseover unit lost (the vanilla gap this fills)
|
||||
end
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
**Transitions and who fires them:**
|
||||
|
||||
| From | To | Fires? | Source |
|
||||
|------|----|:------:|--------|
|
||||
| unit | nothing | yes | ClassicAPI (silent in stock 1.12) |
|
||||
| unit | gameobject | yes | ClassicAPI (mouseover unit genuinely lost) |
|
||||
| gameobject | nothing | no | — (never fired a gain; not a unit) |
|
||||
| anything | unit | yes | engine's own inline fire (gain / unit→unit) |
|
||||
|
||||
**Implementation notes**
|
||||
|
||||
The mouseover GUID lives in two globals written **only** by the engine's
|
||||
mouseover set/clear chokepoint (`0x00492890`), so every gain / loss /
|
||||
change flows through it — on a unit gain it fires `UPDATE_MOUSEOVER_UNIT`
|
||||
itself, on loss it clears the GUID silently. We co-hook that function:
|
||||
resolve whether the mouseover is a *unit* before and after the original,
|
||||
and fire on the unit→non-unit transition. Gating on unit *type* (not just
|
||||
"GUID present → absent") is load-bearing — the same GUID slot also holds
|
||||
gameobjects and items, which never fire a gain, so the type check is what
|
||||
keeps moving off a herb node or chest from spuriously firing. The
|
||||
`!hasUnit` half prevents a double-fire on any →unit transition the engine
|
||||
already covered.
|
||||
|
||||
The target is change-gated by its callers (if it ran per-frame the gain
|
||||
event would spam while hovering), so it's a cool hook — no per-frame
|
||||
overhead.
|
||||
|
||||
### `UPDATE_SHAPESHIFT_FORM` event
|
||||
|
||||
Fires whenever the local player's shapeshift form changes — entering
|
||||
|
||||
@@ -435,6 +435,17 @@ and other DLLs claim for custom events.
|
||||
|
||||
## Notes on specific entries
|
||||
|
||||
- **`[336] UPDATE_MOUSEOVER_UNIT` now fires on loss too (ClassicAPI).**
|
||||
Stock 1.12 fires this only when a mouseover unit is *gained*; moving
|
||||
the cursor off a unit clears the mouseover and signals nothing.
|
||||
ClassicAPI co-hooks the engine's mouseover set/clear chokepoint
|
||||
(`0x00492890`, the sole writer of the mouseover GUID) and fires
|
||||
`UPDATE_MOUSEOVER_UNIT` on the unit→non-unit transition, matching
|
||||
retail. Gated on the *old* mouseover being a unit and the *new* one
|
||||
not, so moving off a gameobject / item (which never fired a gain)
|
||||
doesn't spuriously fire. No payload either way — handlers read
|
||||
`UnitExists("mouseover")`, false at fire time on loss. See
|
||||
[API.md](API.md#update_mouseover_unit-event-loss-fire-fix).
|
||||
- **Repeated `UNIT_*` entries at low indices** (e.g. four slots named
|
||||
`UNIT_DAMAGE` at 128–131, five `UNIT_STATS` at 144–148, seven
|
||||
`UNIT_RESISTANCES` at 149–155) — these are the per-stat /
|
||||
|
||||
@@ -554,6 +554,31 @@ enum Offsets {
|
||||
// input it's the right primitive.
|
||||
FUN_TOKEN_TO_GUID = 0x00515970,
|
||||
|
||||
// Mouseover-unit GUID globals — the `"mouseover"` token resolves to
|
||||
// these (see the token-dispatch list above). Written ONLY by
|
||||
// `FUN_SET_MOUSEOVER_UNIT`; zero when nothing is moused over. Used by
|
||||
// `Unit::Mouseover` to detect the loss transition.
|
||||
VAR_MOUSEOVER_GUID_LO = 0x00B4E2C8,
|
||||
VAR_MOUSEOVER_GUID_HI = 0x00B4E2CC,
|
||||
|
||||
// The engine's single mouseover set/clear chokepoint —
|
||||
// `__stdcall(guidLo, guidHi, arg3, arg4)` (early returns are all
|
||||
// `ret 0x10`, and the caller pushes 4 without cleaning). It is the
|
||||
// ONLY writer of `VAR_MOUSEOVER_GUID_*`, so every mouseover
|
||||
// gain/loss/change flows through it. On a unit gain it fires
|
||||
// `UPDATE_MOUSEOVER_UNIT` itself (via `FUN_FIRE_EVENT_NO_ARGS(0x150)`
|
||||
// in the friendly/hostile-unit tooltip case); on loss it clears the
|
||||
// GUID but fires nothing — the gap `Unit::Mouseover` closes.
|
||||
// Change-gated by its callers (if it ran per-frame the gain fire
|
||||
// would spam every frame while hovering, which vanilla doesn't), so
|
||||
// it's a cool hook target.
|
||||
FUN_SET_MOUSEOVER_UNIT = 0x00492890,
|
||||
|
||||
// Event ID of `UPDATE_MOUSEOVER_UNIT` (name-table slot
|
||||
// `0x00BE16D8` → `(0x16D8 - 0x1198) / 4`). Fired with no args via
|
||||
// `FUN_FIRE_EVENT_NO_ARGS`; Lua handlers read `UnitExists("mouseover")`.
|
||||
EVENT_UPDATE_MOUSEOVER_UNIT = 0x150,
|
||||
|
||||
// `SStrCmpI(a, b, n)` — Storm's case-insensitive memcmp-style
|
||||
// comparator. **`int __stdcall(const char *a, const char *b, int n)`**
|
||||
// — the function ends with `ret 0xc`, so the callee pops the
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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/>.
|
||||
|
||||
// UPDATE_MOUSEOVER_UNIT loss-fire polyfill.
|
||||
//
|
||||
// Vanilla fires `UPDATE_MOUSEOVER_UNIT` only when a mouseover is *gained*
|
||||
// (the engine's `FUN_SET_MOUSEOVER_UNIT` fires it inline in the
|
||||
// friendly/hostile-unit tooltip case). When the cursor leaves the unit,
|
||||
// the engine clears the mouseover GUID but fires nothing — so an addon
|
||||
// that reacted to `UnitExists("mouseover")` becoming true never gets told
|
||||
// when it becomes false. Retail fires the event for both gain and loss.
|
||||
//
|
||||
// We close the gap by co-hooking the single set/clear chokepoint (it is
|
||||
// the ONLY writer of the mouseover GUID globals, so every gain / loss /
|
||||
// change flows through it): resolve whether the mouseover was a *unit*
|
||||
// before the original, then after, and fire `UPDATE_MOUSEOVER_UNIT` when
|
||||
// it went from a unit to a non-unit.
|
||||
//
|
||||
// The unit-vs-non-unit test matters: the mouseover GUID also holds
|
||||
// gameobjects (and items), but the engine only fires the *gain* event for
|
||||
// units — the gameobject/item cases never fire it. So a naive
|
||||
// "GUID present -> absent" test spuriously fires when you move off a
|
||||
// gameobject that never fired a gain. Gating on "old was a unit, new
|
||||
// isn't" fixes that while still matching retail's other cases:
|
||||
//
|
||||
// * unit -> nothing : fire (retail fires)
|
||||
// * unit -> gameobject : fire (retail fires — mouseover unit lost)
|
||||
// * gameobject -> nothing: no (bug fixed; no gain ever fired)
|
||||
// * X -> unit : engine already fired inline; we don't (new is
|
||||
// a unit, so our `!newIsUnit` guard is false —
|
||||
// no double-fire on gain or unit->unit change)
|
||||
//
|
||||
// The event carries no arguments in vanilla (same as retail); handlers
|
||||
// read `UnitExists("mouseover")` — which is false at fire time on the loss
|
||||
// path, exactly matching retail semantics.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Mouseover {
|
||||
|
||||
namespace {
|
||||
|
||||
// `FUN_SET_MOUSEOVER_UNIT` is `__stdcall(guidLo, guidHi, arg3, arg4)` —
|
||||
// see Offsets.h. We don't use the args; they pass straight through.
|
||||
using SetMouseover_t = void(__stdcall *)(uint32_t guidLo, uint32_t guidHi,
|
||||
uint32_t arg3, uint32_t arg4);
|
||||
|
||||
using FireEventNoArgs_t = void(__fastcall *)(int eventID);
|
||||
|
||||
// `FUN_OBJECT_RESOLVE_BY_GUID(typeMask, debugName, guidLo, guidHi, prio)`
|
||||
// — keeps the object only if its type flags intersect `typeMask`, so the
|
||||
// unit|player mask returns non-null only for a live unit / player / pet /
|
||||
// MC'd creature (never a gameobject or item). Same primitive
|
||||
// `Unit::Flags::ResolveUnitOrPlayerByGuid` uses.
|
||||
using ResolveObjectByGuid_t = void *(__fastcall *)(int typeMask,
|
||||
const char *debugName,
|
||||
uint32_t guidLo,
|
||||
uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
// True iff the current mouseover GUID resolves to a unit (not a
|
||||
// gameobject / item / nothing).
|
||||
bool MouseoverIsUnit() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MOUSEOVER_GUID_LO));
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MOUSEOVER_GUID_HI));
|
||||
if ((lo | hi) == 0)
|
||||
return false;
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
return fn(kUnitOrPlayerMask, "UPDATE_MOUSEOVER_UNIT", lo, hi, 0) != nullptr;
|
||||
}
|
||||
|
||||
SetMouseover_t SetMouseover_o = nullptr;
|
||||
|
||||
void __stdcall SetMouseover_h(uint32_t guidLo, uint32_t guidHi,
|
||||
uint32_t arg3, uint32_t arg4) {
|
||||
const bool hadUnit = MouseoverIsUnit();
|
||||
SetMouseover_o(guidLo, guidHi, arg3, arg4);
|
||||
const bool hasUnit = MouseoverIsUnit();
|
||||
|
||||
// Only the unit-loss transition is missing from the engine. Any
|
||||
// ->unit transition (gain / unit->unit change) already fired inline
|
||||
// in the original, so `!hasUnit` prevents a double-fire there.
|
||||
if (hadUnit && !hasUnit) {
|
||||
auto fire = reinterpret_cast<FireEventNoArgs_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS));
|
||||
fire(Offsets::EVENT_UPDATE_MOUSEOVER_UNIT);
|
||||
}
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_SET_MOUSEOVER_UNIT,
|
||||
reinterpret_cast<void *>(&SetMouseover_h),
|
||||
reinterpret_cast<void **>(&SetMouseover_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::Mouseover
|
||||
Reference in New Issue
Block a user