event: remove the mis-timed EnsureCapacity table-grow valve

EnsureCapacity was meant to grow the event table when the engine's ~200
low NULL gaps ran short of our ~46 reservations, by reallocating the
entry buffer via Storm and swapping the base/count globals. But it was
called from LoadScriptFunctions_h, which runs BEFORE RebuildEventTable
inside FUN_0048FBF0 -- so it always operated on the previous cycle's
still-populated table, tripped its own "abort if any chain is live"
guard, and never actually grew anything. Mis-timed dead code doing risky
buffer surgery on the shared event table.

Claim-NULL into the engine's low gaps is the sole mechanism now; it
always suffices, and TryClaim logs once if it somehow doesn't. Drop the
function, its Storm alloc/free helpers, and the now-inaccurate offset
comments.
This commit is contained in:
Brues
2026-07-23 21:06:36 -05:00
parent af01a4b408
commit 538beda499
4 changed files with 19 additions and 139 deletions
-7
View File
@@ -60,13 +60,6 @@ static bool __fastcall FrameScript_Initialize_h() {
static void __fastcall LoadScriptFunctions_h() {
LoadScriptFunctions_o();
Game::RunModuleRegistrations();
// Now that every module has reserved its events (incl. the lazily-
// reserved TTS ones), and before any frame has registered, ensure the
// event table has enough NULL slots for our reservations — growing it
// on-demand if a crowded install has drained the shared gap pool. No-op
// in the common case. Must run here: reservations are complete and all
// chains are still empty (a prerequisite for the safe buffer move).
Event::Custom::EnsureCapacity();
// Permit `Event::Custom::TryClaim` to actually write to the event
// table from here on. Earlier writes (during the engine's own
// boot-time `RegisterEvent` flurry, plus SuperWoWhook/etc.) can
+11 -12
View File
@@ -3988,8 +3988,9 @@ enum Offsets {
VAR_EVENT_TABLE_BASE_PTR = 0x00CEEF68,
VAR_EVENT_TABLE_COUNT = 0x00CEEF64,
// The FrameScript_EventObject is a `{cap@+0, count@+4, data@+8}` at
// 0x00CEEF60, so COUNT/BASE above are its +4/+8. The +0 capacity mirror
// is updated alongside them when `EnsureCapacity` grows the table.
// 0x00CEEF60, so COUNT/BASE above are its +4/+8. (CAP is unused now —
// an EnsureCapacity grow-the-table valve that wrote it was removed;
// see the events "claim-NULL is the sole mechanism" note in CLAUDE.md.)
VAR_EVENT_TABLE_CAP = 0x00CEEF60,
EVENT_ENTRY_STRIDE = 0x10,
OFF_EVENT_ENTRY_NAME = 0x00,
@@ -4034,11 +4035,9 @@ enum Offsets {
// Event-table allocator — `__thiscall(EventObject *this, uint32 count)`.
// Called by `RebuildEventTable` to (re)size the entry array. SuperWoW and
// nampower co-hook it to destructively clamp the FrameXML count (`> 200`;
// glue is smaller) to 700. We deliberately DON'T hook it: a destructive
// clamp can't be beaten in-chain (it overwrites whatever ran before it,
// and hook order isn't controllable). Instead `Event::Custom::EnsureCapacity`
// reallocates the entry buffer directly via Storm + swaps the base/count
// globals, bypassing this clamp entirely. Kept for reference / RE trail.
// glue is smaller) to 700. We deliberately DON'T hook it and don't resize
// the table at all — `Event::Custom` only claims NULL slots from the ~200
// low gaps the engine leaves, which always suffices. Kept for reference.
FUN_FRAMESCRIPT_SET_EVENT_COUNT = 0x007053B0,
// `FireEvent` — the engine's event dispatcher. 149 callers in the
@@ -4254,11 +4253,11 @@ enum Offsets {
// from `SMemAlloc` (which it requires).
FUN_STORM_SSTRDUP = 0x0064A620,
// Storm allocator/free — the same pair the engine uses for the event
// table (see RebuildEventTable / SetEventCount). Both `__stdcall`, 4 args
// (`ret 0x10`). `Event::Custom::EnsureCapacity` uses them to reallocate
// the event table's entry buffer directly, bypassing SetEventCount's
// clamp. SMemAlloc's flag bit 8 (`& 8`) zero-fills the block.
// Storm allocator/free. Both `__stdcall`, 4 args (`ret 0x10`).
// SMemAlloc's flag bit 8 (`& 8`) zero-fills the block. (Duplicates of
// FUN_STORM_SMEM_ALLOC / FUN_STORM_SMEM_FREE above, same addresses;
// currently unused — the EnsureCapacity buffer realloc that used them
// was removed. Left as reference.)
// void* __stdcall SMemAlloc(uint size, const char *file, int line, int flags)
// char __stdcall SMemFree (void *ptr, const char *file, int line, int flags)
FUN_STORM_SMEMALLOC = 0x006462E0,
+8 -101
View File
@@ -49,9 +49,10 @@ bool g_writesEnabled = false;
// One-shot latch so a genuinely full event table (no NULL slot left to
// claim) is logged once per session instead of silently dropping the
// event — the failure mode that would otherwise rot unnoticed. Re-armed on
// `/reload` (the table is rebuilt fresh). `Event::Capacity` raises the
// FrameXML table to >=700 to keep this from ever triggering, but the log is
// the backstop if that hook is ever absent or the pool is somehow exhausted.
// `/reload` (the table is rebuilt fresh). In practice this never triggers:
// SuperWoW/nampower clamp the FrameXML table to 700 and the engine leaves
// ~200 NULL gaps in the low range, dwarfing our ~46 reservations. The log is
// purely a backstop if some future install somehow drains the gap pool.
bool g_warnedTableFull = false;
// Engine's SStrDup at `FUN_STORM_SSTRDUP` — uses `SMemAlloc` internally
@@ -68,22 +69,6 @@ char *SStrDup(const char *s) {
return fn(s, __FILE__, __LINE__);
}
// Storm allocator, matching what the engine uses for the event table itself
// (so the engine's reload teardown frees our buffer the same way). Both are
// `__stdcall`, 4 args (`ret 0x10`). SMemAlloc's flag bit 8 zero-fills.
void *SMemAlloc(unsigned size) {
using SMemAlloc_t = void *(__stdcall *)(unsigned size, const char *file,
int line, int flags);
auto fn = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEMALLOC);
return fn(size, __FILE__, __LINE__, 8 /* zero-fill */);
}
void SMemFree(void *ptr) {
using SMemFree_t = int(__stdcall *)(void *ptr, const char *file, int line,
int flags);
auto fn = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEMFREE);
fn(ptr, __FILE__, __LINE__, 0);
}
// Walk the engine's event table from the LOW end looking for a NULL-name
// slot. The bulk-init at `0x0051AB30` leaves ~200 NULL gaps scattered
// through the 548-slot input names array (it only writes ~350 of the
@@ -95,10 +80,10 @@ void SMemFree(void *ptr) {
// to 700), and other claim-based DLLs (VanillaMinimapTracking) also walk
// from the tail — so the top is the contested end. Claiming from the low
// engine gaps keeps us entirely out of their way and independent of their
// clamp / hook order. `EnsureCapacity` (run once at load) guarantees enough
// NULL slots exist down here before we ever get here; the skip-occupied
// invariant keeps this safe against future engine additions regardless of
// direction.
// clamp / hook order. The engine's ~200 low NULL gaps dwarf our ~46
// reservations, so a free slot is essentially always available; the
// skip-occupied invariant keeps this safe against future engine additions
// regardless of direction.
int TryClaim(const char *eventName) {
if (!g_writesEnabled)
return -1;
@@ -181,84 +166,6 @@ void RetryClaims() {
void EnableWrites() { g_writesEnabled = true; }
void EnsureCapacity() {
// Read the live table fresh — never cache; another DLL may have resized
// it (SuperWoW/nampower clamp to 700, another extender may have grown it).
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
if (base == nullptr || count <= 0 || g_reservedCount <= 0)
return;
// Count the NULL-name slots available to claim.
int nulls = 0;
for (int i = 0; i < count; ++i)
if (*reinterpret_cast<const char *const *>(
base + i * Offsets::EVENT_ENTRY_STRIDE +
Offsets::OFF_EVENT_ENTRY_NAME) == nullptr)
++nulls;
// Enough room already (the common case — the engine's low gaps are huge).
// If another DLL already enlarged the table, we simply see its NULLs here
// and don't grow. Cooperative, no coordination needed.
if (nulls >= g_reservedCount)
return;
// We're short — expand. But growing moves the buffer, which invalidates
// the entries' self-referential list anchors, so it's ONLY safe while
// every chain is empty (no frame registered yet). We run before the first
// RegisterEvent, so that holds — but VERIFY it defensively: a populated
// chain means someone registered early, and growing would corrupt their
// subscriptions. If so, abort and let claiming use whatever NULLs exist
// (the one-shot log fires if that's not enough).
for (int i = 0; i < count; ++i) {
const uint32_t head = *reinterpret_cast<const uint32_t *>(
base + i * Offsets::EVENT_ENTRY_STRIDE + Offsets::OFF_EVENT_ENTRY_HEAD);
const bool empty = (head == 0) || (head & 1); // 0 / low-bit sentinel
if (!empty) {
Debug::Log::Printf(
"[event] %d NULL slots < %d reserved, but chains are live — "
"skipping grow to avoid corrupting registrations",
nulls, g_reservedCount);
return;
}
}
// Grow by the deficit plus a margin (so we don't claim the very last
// slots and starve other claim-based DLLs). Pool-minimal.
constexpr int kMargin = 16;
const int newCount = count + (g_reservedCount - nulls) + kMargin;
auto *newBuf = static_cast<uint8_t *>(
SMemAlloc(static_cast<unsigned>(newCount) * Offsets::EVENT_ENTRY_STRIDE));
if (newBuf == nullptr)
return; // alloc failed — keep the old table; claiming logs if short
// Rebuild every entry at its NEW address: copy the name pointer (strings
// are shared, NOT duplicated), and re-init the anchor as an empty sentinel
// pointing at its own new location. Safe because all chains are empty.
for (int i = 0; i < newCount; ++i) {
auto *e = reinterpret_cast<uint32_t *>(
newBuf + i * Offsets::EVENT_ENTRY_STRIDE);
if (i < count) // new entries stay zero-filled (null name)
e[0] = *reinterpret_cast<const uint32_t *>(
base + i * Offsets::EVENT_ENTRY_STRIDE);
const uint32_t anchor = static_cast<uint32_t>(
reinterpret_cast<uintptr_t>(&e[2])); // entry+0x08
e[2] = anchor; // self-referential list anchor
e[3] = anchor | 1; // empty-chain sentinel head (+0x0C)
}
// Publish base BEFORE count, so the global never advertises more entries
// than the buffer holds. Then free the old buffer (buffer only — the names
// live on in newBuf).
*reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR) = newBuf;
*reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_CAP) = newCount;
*reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT) = newCount;
SMemFree(base);
Debug::Log::Printf("[event] grew table %d -> %d (%d NULL < %d reserved)",
count, newCount, nulls, g_reservedCount);
}
void PrepareForReload() {
for (int i = 0; i < g_reservedCount; ++i)
g_reserved[i].slot = -1;
-19
View File
@@ -124,23 +124,4 @@ void EnableWrites();
// slots point into the old layout and need to re-claim.
void PrepareForReload();
// On-demand event-table expansion. Called once from `LoadScriptFunctions_h`
// AFTER module registration (so every reservation, incl. the lazily-reserved
// TTS ones, is counted) and BEFORE any frame registers (so all chains are
// empty and a buffer move is safe). If the live table has fewer NULL slots
// than we've reserved, it grows the table by the deficit — by reallocating
// the buffer with the engine's own Storm allocator and swapping the base/
// count globals, NOT by routing a size through `SetEventCount`. That
// bypasses SuperWoW/nampower's destructive `= 700` clamp entirely, so it's
// independent of DLL load / hook order.
//
// Normally a no-op: the engine's low NULL gaps (~200) dwarf our reservation
// count, so claim-NULL alone suffices and this never grows. It's a
// last-resort durability valve for the case where many claim-based DLLs have
// drained the shared gap pool. Defensive throughout: reads the table fresh,
// verifies all chains are empty before touching anchors (aborts otherwise),
// frees only the old buffer (never the shared name strings), and publishes
// base before count.
void EnsureCapacity();
} // namespace Event::Custom