bag: gate BAG_UPDATE_DELAYED on PLAYER_ENTERING_WORLD, not VAR_IN_WORLD

The in-world gate added in 5d785ea polled 0x00B4E37C, believed to be an
"in world" flag. It isn't: verified in the binary it's instance
grace-period state, written from a packet handler (FUN_0049e6c0) and
read only by the "recently left an instance" 90-second window check
(FUN_00495c90). It never reliably goes non-zero from being in the world,
so OnWorldTick's early-return held g_pending forever and
BAG_UPDATE_DELAYED stopped firing entirely. There is no clean pollable
in-world boolean at these offsets (0x00B4E378 is the map ID; map 0 is
valid).

Gate on the correct signal instead: PLAYER_ENTERING_WORLD, already owned
by Event::EnteringWorld. Expose a one-way HasEnteredWorld() latch flipped
the first time PEW fires (loading screen done, world live) and stays true
across /reload; UpdateDelayed holds g_pending until then, then emits one
DELAYED covering the settled login inventory. Correct the mislabeled
0x00B4E37C note in Offsets.h so the trap isn't repeated.
This commit is contained in:
Brues
2026-07-30 20:13:05 -05:00
parent 8361ae01eb
commit f91ffd3189
4 changed files with 54 additions and 12 deletions
-7
View File
@@ -3825,13 +3825,6 @@ enum Offsets {
// `Script_IsInInstance`).
VAR_CURRENT_MAP_ID = 0x00B4E378,
// "In world" flag — non-zero when the player is in the world, zero while
// in a loading / transitioning state (initial-login load screen, zone /
// instance transitions). Sibling global to VAR_CURRENT_MAP_ID; used to
// gate work that shouldn't run behind a loading screen.
VAR_IN_WORLD = 0x00B4E37C,
// 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
+7 -5
View File
@@ -58,6 +58,7 @@
#include "Offsets.h"
#include "bag/UpdateDelayed.h"
#include "event/Custom.h"
#include "event/EnteringWorld.h"
#include "tick/WorldTick.h"
namespace Bag::UpdateDelayed {
@@ -132,11 +133,12 @@ void OnWorldTick() {
// slot (setting g_pending) while still behind the loading screen; the
// per-frame world tick runs there too, so firing BAG_UPDATE_DELAYED would
// make addons rescan bags repeatedly mid-load for no real change. Hold the
// pending flag — do NOT clear it — until the engine's in-world flag is set;
// the first in-world frame then emits a single DELAYED covering the settled
// login inventory (matching retail's one BAG_UPDATE_DELAYED at login). A
// transient clear during a later zone transition behaves the same way.
if (*reinterpret_cast<const volatile uint32_t *>(Offsets::VAR_IN_WORLD) == 0)
// pending flag — do NOT clear it — until PLAYER_ENTERING_WORLD has fired
// (the loading screen is done); the first in-world frame then emits a single
// DELAYED covering the settled login inventory (matching retail's one
// BAG_UPDATE_DELAYED at login). There is no reliable pollable in-world flag
// in the binary — see Event::EnteringWorld::HasEnteredWorld.
if (!Event::EnteringWorld::HasEnteredWorld())
return;
g_pending = false;
const int slot = Event::Custom::Lookup(kEventName);
+11
View File
@@ -38,12 +38,19 @@
#include "Game.h"
#include "event/Custom.h"
#include "event/EnteringWorld.h"
#include "event/SignalHook.h"
namespace Event::EnteringWorld {
namespace {
// One-way latch: set the first time PEW fires (see HasEnteredWorld). Stays true
// across /reload — the DLL isn't reloaded, and once the world is live it stays
// live for the session. Consumed by modules that must idle behind the initial-
// login loading screen (e.g. Bag::UpdateDelayed).
bool g_enteredWorld = false;
// Set on glue-screen boot; a pending latch means the next enter-world is a
// fresh character login (from the login/char-select screen).
bool g_atGlue = false;
@@ -67,6 +74,8 @@ bool Intercept(int eventID) {
if (pew < 0 || eventID != pew)
return false;
g_enteredWorld = true; // the loading screen is done; the world is live
const bool isInitialLogin = g_atGlue;
g_atGlue = false;
const bool isReloadingUi = !isInitialLogin && g_scriptReloaded;
@@ -106,4 +115,6 @@ const Game::GlueModuleAutoRegister _glue{&OnGlueInit};
} // namespace
bool HasEnteredWorld() { return g_enteredWorld; }
} // namespace Event::EnteringWorld
+36
View File
@@ -0,0 +1,36 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// `Event::EnteringWorld` owns the single interception of PLAYER_ENTERING_WORLD
// (see EnteringWorld.cpp). It also exposes the one durable fact other modules
// need from that signal: whether the player has finished at least one
// enter-world transition this session — i.e. the initial-login loading screen
// is done and the world is live.
//
// This is the reliable "in world" gate. There is NO clean pollable in-world
// boolean in the 1.12 binary: 0x00B4E378 is the current map ID (map 0 is
// valid) and 0x00B4E37C is instance grace-period state written from a packet
// handler, not a per-frame flag. PLAYER_ENTERING_WORLD, which fires once the
// loading screen completes and the world is ready, is the correct edge.
namespace Event::EnteringWorld {
// True once PLAYER_ENTERING_WORLD has fired at least once this session (initial
// login, /reload, or a zone/instance transition — whichever comes first).
// One-way latch: false only during the initial-login loading screen, before the
// world is live. Process-global, so it stays true across /reload.
bool HasEnteredWorld();
} // namespace Event::EnteringWorld