mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
hearth: gate HEARTHSTONE_BOUND on in-world flag, not location diff
Turtle's server re-sends SMSG_BINDPOINTUPDATE on every map/zone transition with the unchanged bind location, behind a loading screen. The valid-flag gate only suppresses the initial post-login sync, so every map change fired HEARTHSTONE_BOUND. A player can't bind mid-load, so gate on the engine's "world is live" flag instead: VAR_IN_WORLD (0x00B4B424), set to 1 by the enter-world init FUN_004908C0 and back to 0 by the world-teardown FUN_00490A80 that runs at the start of every transition (verified via the enter/leave pair in Ghidra). The resync arrives with the flag 0 (suppressed); a real innkeeper bind always arrives with it 1 (fires) -- including a rebind at the same inn, unlike a location diff would. Add VAR_IN_WORLD to Offsets.h (the real pollable in-world boolean; 0x00B4E37C, once wrongly labeled this, is only instance grace-period state). Update the bind-handler offset comment and docs/API.md.
This commit is contained in:
+9
-3
@@ -2539,9 +2539,15 @@ it: vanilla renders durability UI for equipped gear only.
|
||||
### `HEARTHSTONE_BOUND` event
|
||||
|
||||
Fires (with no payload) every time the player binds their
|
||||
hearthstone at an innkeeper. Polyfills modern WoW's event of the
|
||||
same name — addons listen to `HEARTHSTONE_BOUND` and re-read
|
||||
`GetBindLocation()` to refresh whatever bind-location UI they show.
|
||||
hearthstone at an innkeeper — including a rebind at the same inn.
|
||||
Polyfills modern WoW's event of the same name — addons listen to
|
||||
`HEARTHSTONE_BOUND` and re-read `GetBindLocation()` to refresh
|
||||
whatever bind-location UI they show.
|
||||
|
||||
> Note: the server re-sends the bind packet on map/zone transitions
|
||||
> (behind the loading screen). Those resyncs are suppressed by gating
|
||||
> on the engine's in-world flag, so the event fires only for an actual
|
||||
> innkeeper bind, never on a map change.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
|
||||
+22
-7
@@ -3825,6 +3825,20 @@ enum Offsets {
|
||||
// `Script_IsInInstance`).
|
||||
VAR_CURRENT_MAP_ID = 0x00B4E378,
|
||||
|
||||
// "World is live" flag — 1 while the player is fully in the world, 0 during
|
||||
// any loading screen (initial login, zone/instance transition, logout to
|
||||
// glue). Set to 1 by the enter-world init `FUN_004908C0` (guarded
|
||||
// `if (==0){=1;...}`) and back to 0 by the world-teardown `FUN_00490A80`
|
||||
// (`if(!=0){=0;...}`) that runs at the start of every transition. Byte flag.
|
||||
// The clean pollable gate for "can this happen in the world right now" —
|
||||
// e.g. suppressing server resync packets that arrive behind a loading
|
||||
// screen. Verified via the enter/leave pair in Ghidra.
|
||||
//
|
||||
// (NOTE: 0x00B4E37C is NOT this flag — an earlier note wrongly claimed it
|
||||
// was. That address is instance grace-period state, read only by the
|
||||
// "recently left an instance" 90-second check in `FUN_00495c90`.)
|
||||
VAR_IN_WORLD = 0x00B4B424,
|
||||
|
||||
// Local player's current AreaTable.dbc area ID (u32 storage; only
|
||||
// the low 16 bits are used — the value is broadcast over the wire
|
||||
// as a u16 in `SMSG_PARTY_MEMBER_STATS`). Written by the zone-change
|
||||
@@ -4874,18 +4888,19 @@ enum Offsets {
|
||||
//
|
||||
// Single caller (the packet dispatcher's jump table for opcode
|
||||
// 0x155) — quiet hook target, no contention with other DLLs.
|
||||
// Hooked by `Player::HearthstoneBound` to fire `HEARTHSTONE_BOUND`
|
||||
// every time the user re-binds at an innkeeper. Gating off the
|
||||
// bind-valid flag's transition distinguishes "initial sync" (the
|
||||
// one we suppress) from "user rebound at an inn" (the one we
|
||||
// fire on — even when the area string stays the same, since the
|
||||
// bind ACTION is what fires the event).
|
||||
// Hooked by `Player::HearthstoneBound` to fire `HEARTHSTONE_BOUND`.
|
||||
// Two packets must be suppressed so the event reflects only a real
|
||||
// innkeeper bind: (1) the initial post-login/char-switch sync
|
||||
// (gated on VAR_BIND_POINT_VALID being 0 before the handler runs),
|
||||
// and (2) Turtle's per-map-transition resync, which re-sends the
|
||||
// packet behind a loading screen — gated on VAR_IN_WORLD (the
|
||||
// player can't bind mid-load). See `Player::HearthstoneBound`.
|
||||
FUN_BINDPOINT_UPDATE_HANDLER = 0x005ED3C0,
|
||||
// "Bind point is valid" flag. Set to 1 by the BINDPOINTUPDATE
|
||||
// handler after the packet has been fully parsed. Zeroed by
|
||||
// `FUN_005E2510` (the per-character-entry init routine).
|
||||
// Reading this BEFORE the handler runs distinguishes initial
|
||||
// sync (`== 0`) from a real re-bind (`== 1`).
|
||||
// sync (`== 0`) from a later packet (`== 1`).
|
||||
VAR_BIND_POINT_VALID = 0x00C4D4E0,
|
||||
|
||||
// SMSG_QUESTGIVER_QUEST_COMPLETE handler (opcode 0x191). Server
|
||||
|
||||
@@ -10,19 +10,27 @@
|
||||
|
||||
// `HEARTHSTONE_BOUND` event — polyfills modern WoW's event of the
|
||||
// same name. Fires every time the player binds their hearthstone at
|
||||
// an innkeeper, even when they re-bind at the SAME inn — matches
|
||||
// an innkeeper, even when they re-bind at the SAME inn — matching
|
||||
// modern semantics where the bind ACTION fires the event regardless
|
||||
// of whether the location string changes.
|
||||
// of whether the location changes.
|
||||
//
|
||||
// Hooks the BINDPOINTUPDATE packet handler at `FUN_005ED3C0` and
|
||||
// gates on `VAR_BIND_POINT_VALID` to suppress only the initial
|
||||
// post-login sync. The engine zeroes that flag during per-character
|
||||
// entry init (`FUN_005E2510`); the handler sets it to 1 after
|
||||
// parsing. If the flag is 0 before the original runs, this packet
|
||||
// is the initial sync — don't fire. Any subsequent invocation is
|
||||
// a real innkeeper rebind. Character switch is handled automatically
|
||||
// because the entry init re-zeroes the flag, so each character's
|
||||
// first update is also treated as sync.
|
||||
// Hooks the BINDPOINTUPDATE packet handler at `FUN_005ED3C0`. Two
|
||||
// non-bind packets must be suppressed:
|
||||
//
|
||||
// 1. Initial post-login / character-switch sync — gated on
|
||||
// `VAR_BIND_POINT_VALID`. The engine zeroes that flag during
|
||||
// per-character entry init (`FUN_005E2510`); the handler sets it
|
||||
// to 1 after parsing. Flag 0 before the original runs ⇒ initial
|
||||
// sync, don't fire.
|
||||
// 2. Map/zone-transition resync — Turtle's server re-sends this
|
||||
// packet on every map change with the (unchanged) bind location.
|
||||
// The valid flag is already 1 by then, so (1) doesn't catch it.
|
||||
// But those packets arrive behind a LOADING SCREEN, and the
|
||||
// player can't bind mid-load — so gate on `VAR_IN_WORLD` (the
|
||||
// engine's "world is live" flag, 0 during any loading screen).
|
||||
// A genuine rebind always arrives with the world live (flag 1),
|
||||
// so this suppresses the resync while still firing for a rebind
|
||||
// at the same innkeeper.
|
||||
//
|
||||
// Event has no payload — addons call `GetBindLocation()` to read
|
||||
// the new location, matching modern semantics.
|
||||
@@ -51,7 +59,14 @@ void __fastcall BindPointUpdateHandler_h(void *packetBuffer) {
|
||||
BindPointUpdateHandler_o(packetBuffer);
|
||||
|
||||
if (wasValid == 0)
|
||||
return; // initial post-login sync — not a user rebind
|
||||
return; // initial post-login / char-switch sync — not a user rebind
|
||||
|
||||
// Resync packets ride the enter-world burst behind a loading screen;
|
||||
// a real innkeeper bind only happens with the world live. The player
|
||||
// can't bind mid-load, so a not-in-world packet is always a resync.
|
||||
if (*reinterpret_cast<const volatile uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_IN_WORLD)) == 0)
|
||||
return;
|
||||
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
Event::Custom::Fire(slot, "");
|
||||
|
||||
Reference in New Issue
Block a user