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
49
src/AcDream.Core/World/InteriorEntityIdAllocator.cs
Normal file
49
src/AcDream.Core/World/InteriorEntityIdAllocator.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
namespace AcDream.Core.World;
|
||||
|
||||
/// <summary>
|
||||
/// Packs a stable, collision-free id base for a landblock's dat-hydrated
|
||||
/// EnvCell-interior entities.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Why this exists (#190, 2026-07-09).</b> The id space is
|
||||
/// <c>0x4X XXXXX</c> — the top NIBBLE fixed at 4, distinguishing interior
|
||||
/// statics from live weenies (ids < 0x40000000), procedural scenery
|
||||
/// (0x80000000+, bit 31 set), and landblock stabs (0xC0000000+). Nothing
|
||||
/// decodes a landblock's X/Y back out of an entity id — every consumer
|
||||
/// (GameWindow.cs's <c>_isOutdoorMesh</c>/<c>_isLandblockStab</c>
|
||||
/// classification) reads only a THRESHOLD or a full-byte PREFIX — so the
|
||||
/// internal field widths below are free to change without touching any
|
||||
/// consumer.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// 28 remaining bits split X(8) / Y(8) / counter(12). X and Y stay at a
|
||||
/// full byte each to match AC's 0-255 landblock grid — shrinking either
|
||||
/// reintroduces #119's cross-landblock aliasing (two DIFFERENT landblocks
|
||||
/// sharing one id space). The prior split (X8/Y8/counter8, "0x40XXYY##")
|
||||
/// fixed #119 but left only 256 ids for ONE landblock's entire interior
|
||||
/// static population. The Town Network hub (205 cells, one landblock)
|
||||
/// already reached 248 before the #79/#93 A7.L1 light-carrier hydration
|
||||
/// fix, which pushed it to 277 — PAST the 8-bit boundary, silently
|
||||
/// aliasing into the NEXT landblock's reserved Y-byte (id 0x40000815 read
|
||||
/// back as landblock Y=0x08, not the true Y=0x07): the exact #119 bug,
|
||||
/// reincarnated by entity COUNT instead of a computation bug. Shrinking
|
||||
/// the fixed prefix from a full byte (0x40) to its top nibble (0x4_) frees
|
||||
/// 4 bits for the counter (8→12 bits, 256→4096 capacity) — ~15x headroom
|
||||
/// over the observed count.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class InteriorEntityIdAllocator
|
||||
{
|
||||
/// <summary>Per-landblock counter budget (12 bits). A landblock hydrating
|
||||
/// more interior entities than this would alias into the next Y-slot —
|
||||
/// exactly the #190 bug, just at a higher threshold. Callers should log
|
||||
/// loudly (never silently wrap) if this is ever exceeded.</summary>
|
||||
public const uint MaxCounter = 0xFFFu;
|
||||
|
||||
/// <summary>The first id in this landblock's reserved range (counter=0).
|
||||
/// Add a counter in <c>[0, MaxCounter]</c> to allocate a unique entity id
|
||||
/// within the landblock.</summary>
|
||||
public static uint Base(uint landblockX, uint landblockY)
|
||||
=> 0x40000000u | ((landblockX & 0xFFu) << 20) | ((landblockY & 0xFFu) << 12);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue