fix(#190): interior entity id counter overflowed past its 8-bit budget, aliasing into the next landblock
Found while investigating #189 (missing fountain/candle particles):
reverting the A7.L1 light-carrier hydration fix (9ebb2060) made the
Town Network fountain's water-spray particle work again, which didn't
fit the earlier dat-truth finding that the fountain's own entity was
never touched by that fix. Traced with ACDREAM_DUMP_ENTITY: the
fountain's hydrated entity.Id shifted between reverted (0x400007F8)
and fixed (0x40000815) builds — a 29-id delta matching the extra
mesh-less light carriers the A7.L1 fix now keeps alive earlier in the
same landblock's hydration pass.
Root cause: GameWindow's interior-entity id scheme
(interiorIdBase + localCounter, "0x40XXYY##") reserves only 8 bits
(256 values) for a landblock's ENTIRE interior static population — a
residual explicitly flagged in the #119 fix's own comment ("counter
overflow past 0xFF still bleeds into the lbY byte"). The Town Network
hub (205 cells, one landblock) already sat at 248 before A7.L1; the
light fix pushed it to 277, past the boundary. 0x40000815 decodes as
landblock Y=0x08 — NOT this dungeon's true Y=0x07 — the exact #119
cross-landblock aliasing bug, reincarnated by entity count instead of
a computation bug. EntityScriptActivator keys particle-script
instances by entity.Id directly (no landblock-hint disambiguation
unlike the #119 batch cache), so the aliased id silently broke the
fountain's script tracking.
Fix: AcDream.Core.World.InteriorEntityIdAllocator widens the counter
8->12 bits (256->4096) by shrinking the fixed class prefix from a
full byte (0x40) to its top nibble (0x4_) — verified safe against
every entity.Id classification check in GameWindow (none decode X/Y
back out, they only check thresholds/prefixes). Added a loud
one-time [id-overflow] log if a landblock ever exceeds the new
budget, so this class of bug can never hide silently again.
Core 2675+2skip / App 741+2skip / UI 425 / Net 385 green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
f6b054b7d7
commit
e651cb6dd1
3 changed files with 165 additions and 7 deletions
|
|
@ -7115,9 +7115,9 @@ public sealed class GameWindow : IDisposable
|
|||
(lbY - _liveCenterY) * 192f,
|
||||
0f);
|
||||
|
||||
// Per-landblock id namespace: 0x40000000 | (lbX << 16) | (lbY << 8) | local_counter —
|
||||
// the same 0xNNXXYY## scheme scenery uses (0x80XXYY##). Distinct from scenery
|
||||
// (0x80000000+) and stabs (ids from LandblockLoader).
|
||||
// Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator
|
||||
// for the full bit layout + history. Distinct from scenery (0x80000000+) and
|
||||
// landblock stabs (0xC0000000+, ids from LandblockLoader).
|
||||
//
|
||||
// #119 ROOT-CAUSE FIX (2026-06-11): this used to be
|
||||
// `0x40000000 | (landblockId & 0x00FFFF00)`, which for landblock keys 0xXXYYFFFF
|
||||
|
|
@ -7127,12 +7127,20 @@ public sealed class GameWindow : IDisposable
|
|||
// staircase, both 0x40B3FF09). The Tier-1 classification cache then served one
|
||||
// entity's batches to the other (the cache hint at bucket-draw time was the
|
||||
// player's landblock, identical for both) — the session-sticky "broken stairs +
|
||||
// water barrel". Counter overflow past 0xFF still bleeds into the lbY byte (the
|
||||
// documented #53 scenery-namespace caveat); the cache's (EntityId, owner-derived
|
||||
// LandblockHint) tuple key disambiguates that residual case.
|
||||
// water barrel".
|
||||
//
|
||||
// #190 (2026-07-09): the fix above LEFT a documented residual — "counter overflow
|
||||
// past 0xFF still bleeds into the lbY byte." That residual manifested for real:
|
||||
// the Town Network hub (205 cells, one landblock) reached 277 interior entities
|
||||
// after the #79/#93 A7.L1 light-carrier hydration fix, aliasing into the NEXT
|
||||
// landblock's Y-byte (entity script/particle tracking is keyed on entity.Id
|
||||
// directly — EntityScriptActivator — with no landblock-hint disambiguation, so
|
||||
// the fountain's water-spray script silently stopped firing). Widened the
|
||||
// counter budget 8→12 bits (256→4096); see InteriorEntityIdAllocator's doc for
|
||||
// why this is safe (nothing decodes X/Y back out of an entity id).
|
||||
uint interiorLbX = (landblockId >> 24) & 0xFFu;
|
||||
uint interiorLbY = (landblockId >> 16) & 0xFFu;
|
||||
uint interiorIdBase = 0x40000000u | (interiorLbX << 16) | (interiorLbY << 8);
|
||||
uint interiorIdBase = AcDream.Core.World.InteriorEntityIdAllocator.Base(interiorLbX, interiorLbY);
|
||||
uint localCounter = 0;
|
||||
|
||||
uint firstCellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
||||
|
|
@ -7345,6 +7353,17 @@ public sealed class GameWindow : IDisposable
|
|||
var worldPos = stab.Frame.Origin + lbOffset;
|
||||
var worldRot = stab.Frame.Orientation;
|
||||
|
||||
// #190: never silently wrap past the counter budget — that's exactly how
|
||||
// this bug hid the first time (see the InteriorEntityIdAllocator doc
|
||||
// comment). Log once per landblock the moment it happens; the id below
|
||||
// still aliases into the next Y-slot when this fires (4096 is generous
|
||||
// but not infinite), but at least it's visible in launch.log instead of
|
||||
// silently breaking entity.Id-keyed systems (scripts, particles, shadows).
|
||||
if (localCounter == AcDream.Core.World.InteriorEntityIdAllocator.MaxCounter + 1)
|
||||
Console.WriteLine(
|
||||
$"[id-overflow] landblock 0x{landblockId:X8} exceeded {AcDream.Core.World.InteriorEntityIdAllocator.MaxCounter + 1} " +
|
||||
"interior entities — ids are now aliasing into the next landblock's reserved range (#190 class)");
|
||||
|
||||
var hydrated = new AcDream.Core.World.WorldEntity
|
||||
{
|
||||
Id = interiorIdBase + localCounter++,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue