Skip to content

DRLG: fix nine bugs closing the gap to 1.10f#225

Open
ResurrectedTrader wants to merge 1 commit into
ThePhrozenKeep:masterfrom
ResurrectedTrader:master
Open

DRLG: fix nine bugs closing the gap to 1.10f#225
ResurrectedTrader wants to merge 1 commit into
ThePhrozenKeep:masterfrom
ResurrectedTrader:master

Conversation

@ResurrectedTrader

@ResurrectedTrader ResurrectedTrader commented Jul 5, 2026

Copy link
Copy Markdown

DRLG generator: nine bugs closing the gap to retail D2Common (1.10f + 1.14d)

Nine independent bugs in D2Common/src/Drlg/ where the ported C++
diverges from the retail x86 binary. All nine were found by driving
MOO's DRLG generator against live retail game code running under
d2mapapi
and diffing the
output per-level, then reading the retail decompile in IDA to
confirm each divergence and match the fix to the original bytes. Fixes
1-7 were nailed against 1.10f D2Common.dll; fixes 8-9 are MOO
reconstruction bugs cross-checked against both 1.10f
D2Common.dll and 1.14d Game.exe (they land the last residual
diffs on the 1.14d side and are byte-for-byte the same logic in both
retail versions, so they need no version gate).

With all nine fixes in place, MOO's DRLG output is byte-perfect
against both targets
across a 5-seed sweep
{42, 1234567, 1053646565, 987654321, 111222333}:

  • 1.10f code + 1.10f data (no UBERS) vs. 1.10f d2mapapi: 132/132
    levels PERFECT every seed.
  • 1.14d code + 1.14d data (with UBERS) vs. 1.14d d2mapapi: 136/136
    levels PERFECT every seed - including the four Pandemonium-event
    "uber" levels 133-136 (Matron's Den, Forgotten Sands, Furnace of
    Pain, Uber Tristram).

Before these fixes the sweep showed dozens of CELL_ONLY diffs and a
handful of PRESET_DIFF residuals.

Fixes 1-6 land Acts I-IV parity. Fix 7 lands Act V outdoor parity - it
was masked earlier because a separate consumer-side setup issue
(missing UBERS Levels.bin records) had DUNGEON_AllocAct(ACT_V, ...)
NULL-deref before Act V DRLG code ran, so the crash-once-reachable X-scan
loop bug in DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits was never
exercised. Fixes 8-9 close the residual CELL_ONLY diffs that survived
1-7 on the 1.14d side. The related consumer-side UBERS build fix (so
the uber levels 133-136 generate at all instead of crashing) is
described in the closing note.

Every fix below cites the specific retail address(es) - 1.10f, and for
fixes 8-9 the 1.14d address too - so you can open the binary in your
own tools and check the pseudocode.


Methodology

Same shape for every bug:

  1. Reproduce. A clientless test compiles MOO's DRLG generator and,
    for each level (seed x difficulty x area), asks live 1.10f
    D2Common.dll through d2mapapi for the same input. We diff:

    • mapRows - subtile collision (per-cell int)
    • npcs / objects - preset unit sets {id, x, y}
    • levelOrigin / width / height - level bounding box
      Classification per level: PERFECT / CELL_ONLY / PRESET_DIFF /
      STRUCTURAL. Any non-PERFECT is a bug on MOO's side (d2mapapi is
      running the reference DLL, not our code).
  2. Bisect. Env-gated fprintf traces inside MOO plus a
    MinHook-based hook layer inside d2mapapi log the same call sites
    on the 1.10f side (DRLGACTIVATE_*, DRLGROOM_AllocRoomEx,
    SEED_RollRandomNumber, SpawnHardcodedPresetUnits,
    AddPresetUnitToDrlgMap, ...). Both traces dump pLevel->pSeed,
    pDrlgRoom->pSeed, wRoomsInList[], fRoomStatus and the file
    positions of every parsed .ds1 unit. First divergence between the
    two logs pinpoints the offending function.

  3. Decompile. Once the function is known, IDA Hex-Rays on the
    1.10f address gives the pseudocode.

  4. Patch and re-sweep. Fix, rebuild, re-run the sweep. Escalate the
    sweep size (5 seeds -> 100 -> 1,000) once a fix survives the smaller
    pool.

Test hardness after this PR:

  • 5-seed sweep {42, 1234567, 1053646565, 987654321, 111222333} x
    103 levels = 515/515 PERFECT.
  • 100-seed sweep (deterministic set from C#'s System.Random(42).Next())
    x 103 levels = 10,300/10,300 PERFECT, zero anomalies file.
  • 1,000-seed sweep currently running; no divergence observed in
    first ~500 seeds.

Fix 1 - DRLGACTIVATE_RoomExIdentifyRealStatus: transition to ROOMSTATUS_COUNT was silently a no-op

File: source/D2Common/src/Drlg/DrlgActivate.cpp
1.10f address: 0x6FD73790

Symptom

Rooms whose visibility ref-counts had drained back to zero kept their
old fRoomStatus. RoomExStatusUnset_Untile's
fRoomStatus == ROOMSTATUS_COUNT check therefore never passed,
FreeRoom never fired during propagation, and pTileGrid on
neighbouring rooms stayed alive throughout initial DRLG generation.
That in turn made GetLinkedTileData find pre-existing tiles on
already-processed rooms and take an early-return that 1.10f never takes

  • visible as CELL_ONLY divergences on Act I wilderness levels.

Root cause

The port routed the transition through
DRLGACTIVATE_UpdateRoomExStatusImpl, which is guarded by
if (fRoomStatus > nStatus) (lower = higher priority).
ROOMSTATUS_COUNT = 4 is the highest value in the enum, so X > 4 is
never true - the update path is dead for exactly the transition it's
meant to perform.

Old code

DRLGACTIVATE_UpdateRoomExStatusImpl(pDrlgRoom, nFirstStatusWithRefCount);

Fix

DRLGACTIVATE_RoomExStatusUnlink(pDrlgRoom);
if (nFirstStatusWithRefCount < ROOMSTATUS_COUNT)
{
    DRLGACTIVATE_RoomExStatuslink(&pDrlgRoom->pLevel->pDrlg->tStatusRoomsLists[nFirstStatusWithRefCount], pDrlgRoom);
}
pDrlgRoom->fRoomStatus = nFirstStatusWithRefCount;

Evidence (1.10f 0x6FD73790)

void __fastcall DRLGACTIVATE_RoomExIdentifyRealStatus(struct D2DrlgRoomStrc *pDrlgRoom)
{
    fRoomStatus = pDrlgRoom->fRoomStatus;
    if ( fRoomStatus >= 4u || !pDrlgRoom->wRoomsInList[fRoomStatus] )
    {
        // ... scan wRoomsInList[] for first non-zero slot -> v2 (v3 = slot index)
        if ( fRoomStatus != v2 )
        {
            pStatusPrev = pDrlgRoom->pStatusPrev;
            if ( pStatusPrev )
            {
                pStatusNext = pDrlgRoom->pStatusNext;
                if ( pStatusNext )
                {
                    pStatusPrev->pStatusNext = pStatusNext;
                    pDrlgRoom->pStatusNext->pStatusPrev = pDrlgRoom->pStatusPrev;
                    pDrlgRoom->pStatusPrev = nullptr;
                    pDrlgRoom->pStatusNext = nullptr;
                }
            }
            if ( v2 < 4u )                                          // v2 < ROOMSTATUS_COUNT
            {
                v6 = &pDrlgRoom->pLevel->pDrlg->tStatusRoomsLists[v3];
                pDrlgRoom->pStatusNext = v6;
                pDrlgRoom->pStatusPrev = v6->pStatusPrev;
                v6->pStatusPrev->pStatusNext = pDrlgRoom;
                v6->pStatusPrev = pDrlgRoom;
                pDrlgRoom->fRoomStatus = v2;
            }
        }
        pDrlgRoom->fRoomStatus = v2;                                // unconditional
    }
}

Note the unconditional pDrlgRoom->fRoomStatus = v2; at the tail -
even the v2 == COUNT path (no new list to link into) still updates
the field. That is exactly what MOO was missing.


Fix 2 - DRLGACTIVATE_Update: for loop that never executed

File: source/D2Common/src/Drlg/DrlgActivate.cpp
1.10f address: 0x6FD73F20

Symptom

The client-visibility "sweep" that walks the ClientOutOfSight list
and initialises out-of-sight rooms never ran a single iteration.

Root cause

Textbook copy-paste of the loop-head pointer into the loop-continuation
predicate: both bounds are the same pointer, so the body never runs.
The 1.10f binary uses a proper do { ... } while (p != head && ...)
that starts at the head and terminates on the first return-to-head.

Old code

D2DrlgRoomStrc* pCurRoomEx;
for (pCurRoomEx = pDrlgRoomExListHead;
     pCurRoomEx != pDrlgRoomExListHead;               // always false
     pCurRoomEx = pCurRoomEx->pStatusNext)
{
    if (pCurRoomEx != &pDrlg->tStatusRoomsLists[ROOMSTATUS_CLIENT_OUT_OF_SIGHT])
    {
        DRLGACTIVATE_RoomEx_EnsureHasRoom(pCurRoomEx, false);
    }
    if (pDrlg->nRoomsInitSinceLastUpdate != 0)
    {
        break;
    }
}

Fix

D2DrlgRoomStrc* pCurRoomEx = pDrlgRoomExListHead;
do
{
    if (pCurRoomEx != &pDrlg->tStatusRoomsLists[ROOMSTATUS_CLIENT_OUT_OF_SIGHT])
    {
        DRLGACTIVATE_RoomEx_EnsureHasRoom(pCurRoomEx, false);
    }
    pCurRoomEx = pCurRoomEx->pStatusNext;
} while (pCurRoomEx != pDrlgRoomExListHead
         && pDrlg->nRoomsInitSinceLastUpdate == 0);

Evidence (1.10f 0x6FD73F20)

void __stdcall DRLGACTIVATE_Update(struct D2DrlgStrc *pDrlg)
{
    // ...
    v4 = pDrlg->pDrlgRoom;
    if ( v4 )
    {
        v5 = pDrlg->pDrlgRoom;
        do
        {
            if ( (v5->dwFlags & 0x100000) == 0
              && v5 != &pDrlg->tStatusRoomsLists[2]                  // != CLIENT_OUT_OF_SIGHT list head
              && !v5->pRoom )
            {
                // ... allocate + init room, bump counters ...
            }
            v5 = v5->pStatusNext;
        }
        while ( v5 != v4 && !pDrlg->nRoomsInitSinceLastUpdate );      // <-- do/while
        pDrlg->pDrlgRoom = v5;
    }
}

Straightforward
do { body; p = p->pStatusNext; } while (p != head && !nRoomsInitSinceLastUpdate);.


Fix 3 - DRLGOUTPLACE_PlaceAct1245OutdoorBorders: bHasDirection was sticky across vertex iterations

File: source/D2Common/src/Drlg/DrlgOutPlace.cpp
1.10f address: 0x6FD80ED7 (inside 0x6FD80E10)

Symptom

Once one vertex in the border chain OR'd bHasDirection = true into
tLvlPrestPackedInfo.nPackedValue, every subsequent iteration
inherited that flag regardless of its own vertex direction. On levels
whose border chain contains a non-endpoint vertex with nDirection == 1 (L6 Black Marsh being the canonical case), later segment cells
wrote the sticky bHasDirection flag and the river spawn refused to
place - visible as STRUCTURAL on L6.

Root cause

The port initialised tLvlPrestPackedInfo once outside the
do/while loop
. 1.10f recomputes the equivalent
(v60 = 2*(v49 != 0) + 1) at the top of every iteration.

Old code

D2DrlgOutdoorPackedGrid2InfoStrc tLvlPrestPackedInfo{ 0 };
tLvlPrestPackedInfo.nUnkb00 = true;
tLvlPrestPackedInfo.bHasDirection = pDrlgVertex->nDirection != 0;

do
{
    // ... loop body, no re-init here ...
} while (pDrlgVertex != pLevel->pOutdoors->pVertex);

Fix

D2DrlgOutdoorPackedGrid2InfoStrc tLvlPrestPackedInfo{ 0 };

do
{
    tLvlPrestPackedInfo.nPackedValue = 0;
    tLvlPrestPackedInfo.nUnkb00 = true;
    tLvlPrestPackedInfo.bHasDirection = pDrlgVertex->nDirection != 0;

    // ... rest of the loop body ...
} while (pDrlgVertex != pLevel->pOutdoors->pVertex);

Evidence (1.10f 0x6FD80ED7)

v60 is the D2DrlgOutdoorPackedGrid2InfoStrc union local
(tLvlPrestPackedInfo, a bit-packed 32-bit value: bit 0 = nUnkb00,
bit 1 = bHasDirection, ...). Hex-Rays does not resolve the bit-packed
local so it prints the bit assignments as raw arithmetic - the
annotations below decode each expression back to its named field.
v49 is the loop-local copy of pDrlgVertex->nDirection.

void __fastcall DRLGOUTPLACE_PlaceAct1245OutdoorBorders(struct D2DrlgLevelStrc *pLevel)
{
    // ...
    for ( i = v5; ; v5 = i )
    {
        DRLGVER_GetCoordDiff(pVertex, &v52, &v51);
        // ...
        v49       = *(_BYTE *)(pVertex + 8);          // pDrlgVertex->nDirection
        nLevelType = v53->nLevelType;
        v60       = 2 * (v49 != 0) + 1;               // <-- per-iteration re-init:
                                                      //     nPackedValue  = 0
                                                      //     nUnkb00       = 1
                                                      //     bHasDirection = (v49 != 0)
        switch ( nLevelType ) { /* ... */ }
        // ... body ...
        if ( (struct D2DrlgVertexStrc *)pVertex == v56->pVertex )
            break;
    }
}

v60 = 2 * (v49 != 0) + 1 is (bHasDirection << 1) | nUnkb00 with
nUnkb00 = 1 - i.e. the whole packed value is re-seeded per iteration
from the current vertex's nDirection. That is exactly what MOO's
post-fix
tLvlPrestPackedInfo.nPackedValue = 0; nUnkb00 = true; bHasDirection = pDrlgVertex->nDirection != 0;
does.


Fix 4 - DRLGOUTPLACE_PlaceAct1245OutdoorBorders: bHasDirection was set true even when both vertices had nDirection == 0

File: source/D2Common/src/Drlg/DrlgOutPlace.cpp
1.10f address: 0x6FD81167 (inside 0x6FD80E10)

Symptom

Segments in wilderness borders whose current and next vertex both
had nDirection == 0 were writing bHasDirection = 1 into the packed
grid, where retail writes bHasDirection = 0. That perturbed
downstream AlterGridFlag calls on those cells, shifting a handful of
segment LvlPrestIds and causing per-seed CELL_ONLY diffs on
LEVEL_MOOMOOFARM and other non-{BLOODMOOR, COLDPLAINS,
BURIALGROUNDS} Act I wilderness levels.

BLOODMOOR / COLDPLAINS / BURIALGROUNDS run the pre-loop that sets
pDrlgVertex->nDirection = 1 on select vertices, so at least one of
each vertex pair has a non-zero direction. Other wilderness levels
skip that pre-loop, so pairs with two adjacent zero-direction vertices
exist - which is exactly where the bug fires.

Root cause

The port's else-branch (when pDrlgVertex->nDirection == 0) fell back
to pNextVertex->nDirection for the effective nDirection, but then
set tLvlPrestPackedInfo.bHasDirection = true unconditionally -
even if pNextVertex->nDirection was also zero. Retail only sets that
bit when the effective (short-circuited) direction is non-zero, so
when both vertices are nDirection == 0 the bit stays clear.

Old code

nDirection = pDrlgVertex->nDirection;
if (nDirection)
{
    tLvlPrestPackedInfo.bHasDirection = true;
}
else
{
    nDirection = pNextVertex->nDirection;
    tLvlPrestPackedInfo.bHasDirection = true;   // <-- unconditional
}

Fix

nDirection = pDrlgVertex->nDirection;
if (nDirection == 0)
{
    nDirection = pNextVertex->nDirection;
}
if (nDirection)
{
    tLvlPrestPackedInfo.bHasDirection = true;   // <-- conditional on effective != 0
}

switch (pLevel->nLevelType)
{
case LVLTYPE_ACT1_WILDERNESS:
    v41 = nDirection == 0;
    break;
// ...
}

Evidence (1.10f 0x6FD81167)

v49 is pDrlgVertex->nDirection at the top of the loop and is
rewritten to pNextVertex->nDirection by the || short-circuit here
if it was zero. v60 is tLvlPrestPackedInfo.nPackedValue;
LOBYTE(v38) = v60 | 2 sets bit 1 of that packed value
(bHasDirection = true). Crucially, that |= 2 sits inside the
if — it fires only when the short-circuit produced a non-zero
effective direction. That's the retail behaviour MOO's else branch
was missing.

// ... continuation of DRLGOUTPLACE_PlaceAct1245OutdoorBorders, tail block ...
pVertex = (int)i;                                                 // pNextVertex
if ( v49 || (v49 = *((_BYTE *)i + 8)) != 0 )                      // <-- short-circuit:
{                                                                 //   v49 stays pDrlgVertex->nDirection if != 0,
    v38 = v60;                                                    //   else v49 = pNextVertex->nDirection.
    LOBYTE(v38) = v60 | 2;                                        //   bHasDirection = true, ONLY inside the if
    v60 = v38;                                                    //   (bit 1 of nPackedValue)
}
switch ( v53->nLevelType )
{
    case 2:                                                       // LVLTYPE_ACT1_WILDERNESS
        v40 = v49 == 0;                                           // reads *effective* v49
        break;
    case 0x10:  v40 = 2; break;                                   // ACT2_DESERT
    case 0x1B:  v40 = 3; break;                                   // ACT4_MESA
    case 0x1F:  v40 = sub_6FD84100(v53); break;                   // ACT5_BARRICADE
    default:    v40 = -1; break;
}

1.14d's Game.exe PlaceAct1245OutdoorBorders @ 0x675B60 has the same
v59 || (v59 = pNextVertex->nDirection) short-circuit followed by a
gated v46 |= 2u, so the two retail versions agree - no version
divergence to switch on.


Fix 5 - DRLGOUTWILD_SpawnRiver: bridge row selection had the wrong modulus

File: source/D2Common/src/Drlg/DrlgOutWild.cpp
1.10f address: 0x6FD85258

Symptom

For outdoor levels with OUTDOOR_RIVER | OUTDOOR_BRIDGE, MOO tested a
different (and generally wrong) set of grid rows for a valid
bridge-crossing spawn cell than 1.10f. On some seeds this shifts the
crossing by a row; on others it fails to find a valid cell at all -
measurable as CELL_ONLY / STRUCTURAL on Act I wilderness levels
that host bridges.

Root cause

MOO's modulus was gridHeight - 1 with no + 1 offset. The reference
formula is (nRand + i) % (gridHeight - 2) + 1, which produces a
completely different set of visited rows.

Old code

int nRand = SEED_RollLimitedRandomNumber(&pLevel->pSeed, pLevel->pOutdoors->nGridHeight - 2);

for (int i = 0; i < pLevel->pOutdoors->nGridHeight - 2; ++i)
{
    int nY = (nRand + i) % (pLevel->pOutdoors->nGridHeight - 1);
    // ...
}

Fix

const int nRowRange = pLevel->pOutdoors->nGridHeight - 2;
int nRand = SEED_RollLimitedRandomNumber(&pLevel->pSeed, nRowRange);

for (int i = 0; i < nRowRange; ++i)
{
    int nY = (nRand + i) % nRowRange + 1;
    // ...
}

Evidence (1.10f 0x6FD85258, inside DRLGOUTWILD_SpawnRiver @ 0x6FD850B0)

void __fastcall DRLGOUTWILD_SpawnRiver(struct D2DrlgLevelStrc *pLevel, int nX)
{
    // ...
    v24 = pLevel->pOutdoors;
    v12 = v24->nGridHeight - 2;                                    // <-- modulus
    // ... seed roll into v21 = SEED_RollLimitedRandomNumber(&pLevel->pSeed, v12)
    v22 = 0;
    if ( v12 > 0 )
    {
        while ( 1 )
        {
            v15 = nX - 1;
            v16 = (int)(v21 + v22) % v12 + 1;                      // <-- (nRand + i) % (nGridHeight-2) + 1
            // ... TestGridCellSpawnValid checks at (v15, v16) and (nX+2, v16) ...
            if ( ++v22 >= v12 )
                return;
        }
    }
}

(nRand + i) % (nGridHeight - 2) + 1 - modulus is nGridHeight - 2,
offset by + 1.


Fix 6 - DRLG_OUTDOORS_GenerateDirtPath: right-border column of the 9x9 floor grid was never written

File: source/D2Common/src/Drlg/DrlgOutdoors.cpp
1.10f address: 0x6FD7F240 (inside 0x6FD7EFE0)

Symptom

On Cold Plains and other Act I outdoor rooms, the right-border column
(x = pDrlgCoord.nWidth) of the floor grid stayed empty - two
floor-tile cells short of the reference. Directly visible as
CELL_ONLY diffs concentrated at the room's right edge.

Root cause

The port's loop was nX <= nWidth, one iteration short of 1.10f's
loop, which breaks when v14 > pDrlgCoord.nWidth + 1 - i.e. keeps
going through nX == nWidth, one column past what MOO covered.

Old code

for (int nX = 1; nX <= pDrlgRoom->pDrlgCoord.nWidth; ++nX)

Fix

for (int nX = 1; nX <= pDrlgRoom->pDrlgCoord.nWidth + 1; ++nX)

Evidence (1.10f 0x6FD7F240, tail of DRLG_OUTDOORS_GenerateDirtPath @ 0x6FD7EFE0)

void __fastcall DRLG_OUTDOORS_GenerateDirtPath(struct D2DrlgLevelStrc *pLevel,
                                               struct D2DrlgRoomStrc *pDrlgRoom)
{
    // ...
    v23 = &pDrlgRoom->pDrlgCoord;
    if ( p_pDrlgCoord->nWidth + 1 >= 1 )
    {
        v20 = 2;                                                   // starts at column 2 (1-based origin + 1)
        while ( 1 )
        {
            // ... body reads (v20-2, v13-1)..(v20, v13+1) neighbours ...
            v20 = v14 + 1;                                         // advance
            if ( v14 > v23->nWidth + 1 )                           // <-- break when v14 > nWidth + 1
                break;
            p_pDrlgCoord = v23;
        }
    }
}

The break at 0x6FD7F240 is
if (v14 > pDrlgCoord.nWidth + 1) break;, so the last iteration that
runs the body has v14 == pDrlgCoord.nWidth + 1, which corresponds to
writing at column pDrlgCoord.nWidth. MOO's
nX <= pDrlgCoord.nWidth bound is one iteration short.


Fix 7 - DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits: X-scan loops used height instead of width

File: source/D2Common/src/Drlg/DrlgOutSiege.cpp
1.10f address: 0x6FD84580

Symptom

Act V wilderness levels (LEVEL_BLOODYFOOTHILLS L110,
LEVEL_FRIGIDHIGHLANDS L111) intermittently crashed with either
EXCEPTION_ACCESS_VIOLATION or STATUS_HEAP_CORRUPTION (0xC0000374)
partway through Act V classify. The failure was seed-sensitive but not
seed-deterministic - across 8 sequential runs of the same seed 987654321,
5 crashed at various levels between L109 and L132 (three at heap-corrupt
detection during a later alloc, one at direct AV) and 3 returned 132/132
PERFECT. Same MOO code path, same input; only the transient heap layout
differed.

Root cause

The first two loops in PlaceBarricadeEntrancesAndExits are X-scans:
they walk cells along the top row (nY = 0) then along the second-to-
last row (nY = nGridHeight - 2), looking for the first bLvlLink cell
to spawn the top / bottom barricade preset on. The port bounded these
loops on pOutdoors->nGridHeight while passing the induction variable
as the X coord. Retail 1.10f bounds them on pOutdoors->nGridWidth.

Because Act V wilderness levels are always taller than they are wide
(the reproducing shape was nGridWidth = 8, nGridHeight = 20), the
induction variable ran to 19 - twelve cells past the row - and computed
&pOutdoors->pGrid[2].pCellsFlags[nX + pCellsRowOffsets[nY]] with nX = 8..19. pCellsRowOffsets[nY] still returned valid offsets, but the
resulting flat index landed anywhere from just past the row into
unmapped territory. The two Y-scan loops immediately after (left / right
columns) were correct - they iterate the Y coord and are already bound
by nGridHeight.

The wild write's target was determined by transient heap state (ASLR

  • prior CRT / logger / JSON allocations), so the same seed sometimes
    overwrote a benign heap block (silent, test still passed), sometimes
    corrupted a block header (heap manager caught it at the next
    alloc / free as STATUS_HEAP_CORRUPTION), and sometimes hit an
    unmapped page (immediate AV). None of the outcomes were about MOO's
    DRLG decisions differing between runs; the DRLG path itself was fully
    deterministic.

Why this wasn't caught earlier

DUNGEON_AllocAct(ACT_V, ...) used to NULL-deref inside
sub_6FD823C0(gAct5UbersDrlgLink, ...) because Levels.bin in the
build being tested lacked the 1.11+ UBERS records that the D2COMMON
build was compiled to reference. Every historical sweep aborted at Act
V setup and only classified 103 levels/seed - Acts I-IV - so
PlaceBarricadeEntrancesAndExits was never reached. Unblocking Act V
comparison (see the header note) is what made this seven-year-old
loop-bound typo observable.

Old code

void __fastcall DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(D2DrlgLevelStrc* pLevel)
{
    for (int i = 0; i < pLevel->pOutdoors->nGridHeight; ++i)   // <-- nGridHeight
    {
        if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i, 0).bLvlLink)
        {
            DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, i, 0,
                LVLPREST_ACT5_BARRICADE_EXIT_32X16,
                2 * (pLevel->nLevelId == LEVEL_BLOODYFOOTHILLS) - 1, 0);
            break;
        }
    }

    for (int i = 0; i < pLevel->pOutdoors->nGridHeight; ++i)   // <-- nGridHeight
    {
        if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i,
                pLevel->pOutdoors->nGridHeight - 2).bLvlLink)
        {
            DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, i,
                pLevel->pOutdoors->nGridHeight - 2,
                LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16,
                2 * (pLevel->nLevelId == LEVEL_BLOODYFOOTHILLS) - 1, 0);
            break;
        }
    }

    // Y-scan loops (unchanged) follow ...
}

Fix

for (int i = 0; i < pLevel->pOutdoors->nGridWidth; ++i)        // <-- nGridWidth
{
    if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i, 0).bLvlLink)
    {
        // ...
    }
}

for (int i = 0; i < pLevel->pOutdoors->nGridWidth; ++i)        // <-- nGridWidth
{
    if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i,
            pLevel->pOutdoors->nGridHeight - 2).bLvlLink)
    {
        // ...
    }
}

Evidence (1.10f 0x6FD84580)

void __fastcall DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(struct D2DrlgLevelStrc *pLevel)
{
    int v2, v4, v5, v6, v7, v8;
    struct D2DrlgOutdoorInfoStrc *pOutdoors;

    v2 = 0;
    pOutdoors = pLevel->pOutdoors;
    if ( pOutdoors->nGridWidth > 0 )                                       // <-- X-scan bound by nGridWidth
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], v2, 0) & 0x400) == 0 )
        {
            if ( ++v2 >= pOutdoors->nGridWidth )                           //     nGridWidth
                goto LABEL_6;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, v2, 0, 909,         // 909 = LVLPREST_ACT5_BARRICADE_EXIT_32X16
            2 * (pLevel->nLevelId == 110) - 1, 0);                         // 110 = LEVEL_BLOODYFOOTHILLS
    }
LABEL_6:
    v4 = 0;
    v5 = pOutdoors->nGridHeight - 2;                                       // bottom row = nGridHeight - 2
    if ( pOutdoors->nGridWidth > 0 )                                       // <-- X-scan bound by nGridWidth
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], v4, v5) & 0x400) == 0 )
        {
            if ( ++v4 >= pOutdoors->nGridWidth )                           //     nGridWidth
                goto LABEL_11;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, v4, v5, 908,        // 908 = LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16
            2 * (pLevel->nLevelId == 110) - 1, 0);
    }
LABEL_11:
    v6 = 0;
    if ( pOutdoors->nGridHeight > 0 )                                      // <-- Y-scan bound by nGridHeight
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], 0, v6) & 0x400) == 0 )
        {
            if ( ++v6 >= pOutdoors->nGridHeight )
                goto LABEL_16;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, v6, 910,         // 910 = LVLPREST_ACT5_BARRICADE_EXIT_16X32
            2 * (pLevel->nLevelId == 110) - 1, 0);
    }
LABEL_16:
    v7 = pOutdoors->nGridWidth - 2;                                        // right column = nGridWidth - 2
    v8 = 0;
    if ( pOutdoors->nGridHeight > 0 )                                      // <-- Y-scan bound by nGridHeight
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], v7, v8) & 0x400) == 0 )
        {
            if ( ++v8 >= pOutdoors->nGridHeight )
                return;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, v7, v8, 907,        // 907 = LVLPREST_ACT5_BARRICADE_ENTRANCE_16X32
            2 * (pLevel->nLevelId == 110) - 1, 0);
    }
}

The pattern is symmetric: X-scans (loops 1 and 2 - top row / bottom
row) are bounded by nGridWidth; Y-scans (loops 3 and 4 - left column
/ right column) are bounded by nGridHeight. The port had the two
X-scans using the Y-scan bound.


Fix 8 - DRLGACTIVATE_RoomExPropagateSetStatus / DRLGACTIVATE_RoomSetAndPropagateStatus: status handler re-fired on every propagation instead of once at the 0->1 ref transition

File: source/D2Common/src/Drlg/DrlgActivate.cpp
1.10f address: 0x6FD73A30 (DRLGACTIVATE_RoomExPropagateSetStatus)
1.14d address: 0x61B390 (InitNearRooms) and 0x61B490
(AddRoomDataActual)

Symptom

The per-status handlers in gRoomExSetStatus[] (which tile / untile a
room and adjust its pTileGrid) fired more than once for the same
room+status as a status was propagated across the near-room set,
instead of exactly once on the room's first reference at that status.
The extra dispatches re-tiled rooms the reference leaves untouched
during a given propagation, leaving stale/duplicated tile emission -
the residual CELL_ONLY diffs that survived Fixes 1-7 on the 1.14d
side (e.g. L28 Barracks at seed 1053646565).

Root cause

Retail keeps a per-room wRoomsInList[ROOMSTATUS_COUNT] array of
cumulative reference counts, one slot per status. When a status is
applied, it walks wRoomsInList[0..nStatus] and dispatches the status
handler only if every one of those slots is still zero - the
0 -> 1 first-reference transition - and only then increments
wRoomsInList[nStatus].

MOO reconstructed the gate as nFirstStatusWithRefCount == nStatus
alone. DRLGACTIVATE_RoomExFindFirstStatusWithRefCount returns the
first slot in [0..] with a non-zero count, so == nStatus is true in
two distinct situations: (a) slots [0..nStatus-1] are empty and
[nStatus] is already non-zero (a later reference - retail does NOT
dispatch here), and (b) all of [0..nStatus] are empty (the genuine
first reference - retail dispatches). The reconstruction dispatched in
both, so it re-fired the handler on every propagation while the lower
slots happened to be empty. The missing term is
wRoomsInList[nStatus] == 0, which excludes case (a).

Old code

const D2DrlgRoomStatus nFirstStatusWithRefCount =
    DRLGACTIVATE_RoomExFindFirstStatusWithRefCount(pNearRoom, D2DrlgRoomStatus(nStatus));
if (nFirstStatusWithRefCount == nStatus)
{
    gRoomExSetStatus[nStatus](pNearRoom);
}

Fix

const D2DrlgRoomStatus nFirstStatusWithRefCount =
    DRLGACTIVATE_RoomExFindFirstStatusWithRefCount(pNearRoom, D2DrlgRoomStatus(nStatus));
if (nFirstStatusWithRefCount == nStatus && pNearRoom->wRoomsInList[nStatus] == 0)
{
    gRoomExSetStatus[nStatus](pNearRoom);
}

(the same one-line addition applies to the sibling gate in
DRLGACTIVATE_RoomSetAndPropagateStatus, keyed on pDrlgRoom.)

Evidence (1.10f 0x6FD73A30)

v10 is the near room (D2DrlgRoomStrc*), v10 + 174 is
wRoomsInList[0], a3 is nStatus. The while (!*v12) loop advances
through wRoomsInList[0], [1], ..., [nStatus]; only when the index has
passed nStatus (v11 > a3, i.e. all those slots were zero) does
it dispatch. The ++wRoomsInList[nStatus] sits after the check, so
the gate sees the pre-transition state.

if ( (unsigned __int8)v10[172] >= (int)(unsigned __int8)a3 )       // fRoomStatus >= nStatus
{
    v11 = 0;
    v12 = v10 + 174;                                               // &wRoomsInList[0]
    while ( !*v12 )                                                // while wRoomsInList[v11] == 0
    {
        ++v11;
        ++v12;
        if ( v11 > (unsigned __int8)a3 )                           // walked [0..nStatus] all-zero
        {
            funcs_6FD73AB6[(unsigned __int8)a3](v10);              // gRoomExSetStatus[nStatus]
            break;
        }
    }
}
++*(_WORD *)&v10[2 * (unsigned __int8)a3 + 174];                   // ++wRoomsInList[nStatus] (AFTER)

Evidence (1.14d 0x61B390 InitNearRooms, mirrored at 0x61B490 AddRoomDataActual)

Same loop shape, wRoomsInList at pRoom + 12, fRoomStatus at
pRoom + 68:

if ( *(unsigned __int8 *)(v11 + 68) >= (int)a3 )                   // fRoomStatus >= nStatus
{
    v12 = 0;
    v13 = (_WORD *)(v11 + 12);                                     // &wRoomsInList[0]
    while ( !*v13 )                                                // while wRoomsInList[v12] == 0
    {
        ++v12;
        ++v13;
        if ( v12 > a3 )                                            // walked [0..nStatus] all-zero
        {
            funcs_61B410[a3](v11);                                 // gRoomExSetStatus[nStatus]
            break;
        }
    }
}
++*(_WORD *)(v11 + 2 * a3 + 12);                                   // ++wRoomsInList[nStatus] (AFTER)

Both retail versions gate on "all of wRoomsInList[0..nStatus] are
zero" and increment afterwards - identical logic, so the fix is
ungated.


Fix 9 - DRLGROOMTILE_AddLinkedTileData: non-floor tiles matched any link, chaining onto the floor tile list

File: source/D2Common/src/Drlg/DrlgRoomTile.cpp
1.10f address: 0x6FD89930 (DRLGROOMTILE_AddLinkedTileData)
1.14d address: 0x66E620 (DRLGROOMTILE_AddTileData)

Symptom

When adding a non-floor tile (wall / shadow / warp) to a room's tile
grid, MOO walked the pTileGrid->pMapLinks list and stopped at the
first link whose bFloor flag did not matter, so a non-floor tile
could be chained onto the floor link's tile list (and vice-versa the
match was too loose). The wrong link's tile list then fed the wrong
tile into collision emission - CELL_ONLY diffs on rooms with both
floor and non-floor links.

Root cause

The link-match predicate needs the link's bFloor to equal
whether the incoming tile is a floor tile:
link->bFloor == (nTileType == TILETYPE_FLOOR). MOO's reconstruction
was (bFloor && nTileType == FLOOR) || (nTileType != FLOOR), whose
second clause matches any link as soon as the tile is non-floor -
so a non-floor tile grabbed whatever link came first, including the
floor link.

Old code

for (pCurLink = pDrlgRoom->pTileGrid->pMapLinks; pCurLink; pCurLink = pCurLink->pNext)
{
    if ((pCurLink->bFloor && nTileType == TILETYPE_FLOOR) || (nTileType != TILETYPE_FLOOR))
    {
        break;
    }
}

Fix

for (pCurLink = pDrlgRoom->pTileGrid->pMapLinks; pCurLink; pCurLink = pCurLink->pNext)
{
    if (pCurLink->bFloor == (nTileType == TILETYPE_FLOOR))
    {
        break;
    }
}

Evidence (1.10f 0x6FD89930)

i walks pMapLinks (i[2] = pNext), *i = link->bFloor, v6 =
nTileType (0 == TILETYPE_FLOOR). The two branches accept a link
only when bFloor and "is-floor" agree, and a freshly allocated link
is seeded *v10 = (v6 == 0) - i.e. bFloor = (nTileType == FLOOR):

for ( i = **(_DWORD ***)(a2 + 168); i; i = (_DWORD *)i[2] )        // walk pMapLinks
{
    if ( *i )                     { if ( !v6 ) goto LABEL_11; }    // bFloor:  match iff nTileType == FLOOR
    else if ( v6 )                goto LABEL_11;                   // !bFloor: match iff nTileType != FLOOR
}
v10 = Fog_10045(a1, 12, ...);                                     // no match -> alloc new link
*v10 = v6 == 0;                                                   // bFloor = (nTileType == FLOOR)

Evidence (1.14d 0x66E620)

Byte-identical predicate, pMapLinks at a1 + 84:

for ( i = **(_DWORD ***)(a1 + 84); i; i = (_DWORD *)i[2] )
{
    if ( *i )                     { if ( !a2 ) goto LABEL_11; }    // a2 = nTileType
    else if ( a2 )                goto LABEL_11;
}
v9 = AllocServerMemory(".\\DRLG\\RoomTile.cpp", 0x37Bu, 0);
*v9 = a2 == 0;                                                    // bFloor = (nTileType == FLOOR)

if (*i) { if (!nTileType) match; } else if (nTileType) match; is
exactly the boolean equality link->bFloor == (nTileType == FLOOR) in
both retail versions.


Note (not a bug) - the UBERS build flag must be defined for the 1.14d uber levels

Not a DRLG-code fix, but the prerequisite that makes the 1.14d
uber-level comparison (levels 133-136) possible at all. In D2MOO,
D2_VERSION_HAS_UBERS is derived inside D2BuildInformation.h
(#if D2_VERSION_MAJOR >= 1 && D2_VERSION_MINOR >= 11), which is
reached through D2Config.h. A build that force-includes only the
narrow headers it needs (as the clientless consumer here does) never
pulls in D2Config.h, so the macro stays undefined and every
#ifdef D2_VERSION_HAS_UBERS block compiles out: the uber level-id
enum entries (LEVEL_PANDEMONIUMRUN1..FINALE) cease to exist, and the
uber routing in DRLGOUTDOORS_GenerateLevel (force level 134 to the
Act II outdoor initializer) and DRLGOUTDESR_InitAct2OutdoorLevel
(the case LEVEL_PANDEMONIUMRUN2: desert-warp preset) is stripped.
Level 134 then falls through to DRLG_GetActNoFromLevelId's Act V
result and runs the Act V siege init, which null-derefs.

The D2MOO uber code itself is correct - it keys on
LEVEL_PANDEMONIUMRUN2, which resolves to 134 once the macro is
defined, and the uber enum entries are appended (no id shift for the
standard 1-132 levels). The fix is purely build configuration: define
D2_VERSION_HAS_UBERS for the 1.14d build (or force-include
D2BuildInformation.h). With it defined, levels 133-136 generate and
match 1.14d d2mapapi 136/136 with zero cell/preset diffs.


Note (not a bug) - DS1 loaders must pad past EOF

Not a fix in this PR, but worth flagging for anyone driving MOO's
DRLG generator against retail assets: some retail .ds1 files declare
more records in a section header than actually exist on disk. The
canonical case is 1.10f's Act1\Outdoors\Trees.ds1, whose header says
nSubstGroups = 14 but only 13 complete records follow before EOF.
MOO's DS1 parser trusts the header and reads the declared count
straight out of the file buffer, so on the 14th read it walks past
EOF. On the real game this is masked - Storm's allocator zero-fills
tail bytes and the read returns zeros. Buffers sized to exactly the
file length return uninitialised heap memory, and the garbage records
perturb DRLG cell emission as CELL_ONLY diffs (L6 Black Marsh is the
level where this shows up first, since it's the one that pulls
Trees.ds1 as a preset).

Pad DS1 buffers by 256 zero bytes past EOF and the parser behaves as
it does on retail.


Test results

All nine fixes - dual-version parity (5-seed)

Seeds {42, 1234567, 1053646565, 987654321, 111222333}, difficulty 0,
each level compared against the matching-version d2mapapi:

1.10f code + 1.10f data (no UBERS)  vs 1.10f d2mapapi:
    per seed: 132/132 PERFECT   (max level 132, no ubers)
    5 seeds:  660/660 PERFECT    CELL_ONLY=0 PRESET_DIFF=0 STRUCTURAL=0

1.14d code + 1.14d data (with UBERS) vs 1.14d d2mapapi:
    per seed: 136/136 PERFECT   (incl uber levels 133-136)
    5 seeds:  680/680 PERFECT    CELL_ONLY=0 PRESET_DIFF=0 STRUCTURAL=0

Every level in both configurations matches on collision cells, preset
NPCs, and preset objects. The 1.10f run reaches level 132 (no ubers,
correct); the 1.14d run reaches 136, adding the four Pandemonium-event
levels.

Acts I-IV parity (Fixes 1-6)

The 100-seed sweep was empty (sweep-100/anomalies.txt has 0 lines)
after Fixes 1-6. Full summary line was:

Total across 100 seeds: PERFECT=10300  CELL_ONLY=0  PRESET_DIFF=0
                        STRUCTURAL=0  (of 10300)

Level breakdown per seed: 103 levels classified per seed (Acts
I-IV). Act V levels were skipped by the sweep because
DUNGEON_AllocAct(ACT_V, ...) was NULL-deref'ing on the missing UBERS
Levels.bin records - a consumer-side setup problem, not a MOO DRLG
bug.

1,000-seed sweep (Acts I-IV configuration) completed clean:
1000/1000 seeds, all PERFECT across 103 levels/seed = 103,000
level comparisons. Zero CELL_ONLY, zero PRESET_DIFF, zero STRUCTURAL.

Act V parity (Fix 7)

Once the consumer-side UBERS problem was worked around and Act V
classification was actually reached, Fix 7 was needed to land parity
on Act V outdoor levels. The 5-seed sweep after Fix 7:

Total across 5 seeds: PERFECT=660  CELL_ONLY=0  PRESET_DIFF=0
                      STRUCTURAL=0  (of 660)

= 132 levels/seed classified (all of Acts I-V), 5 seeds
{42, 1234567, 1053646565, 987654321, 111222333}. Seed 987654321 was
the one that reliably exercised the loop-bound bug via crashing
before the fix; post-fix it hits 132/132 PERFECT across 8 repeated
sequential runs (each with a fresh d2mapapi server), confirming the
non-determinism is gone.

A fresh 1,000-seed sweep with all seven fixes and Act V unlocked
(132 levels/seed = 132,000 comparisons) is queued to run; results
will be posted as a follow-up when it completes.


Attribution

This investigation was carried out with Claude Code (Anthropic
Claude Opus 4.7 / 4.8)
driving the reproduction harness, the
bisect hooks, the IDA queries, and the source edits.

@ResurrectedTrader
ResurrectedTrader force-pushed the master branch 2 times, most recently from b8c853b to 3b68918 Compare July 5, 2026 12:07
@Lectem

Lectem commented Jul 5, 2026

Copy link
Copy Markdown
Member

Thanks for the PR, I had run similar experiments back then but only checked maps reachable from towns iirc.
I'll try having a look at the fixes this week, unless someone else can confirm those for me.

@ResurrectedTrader

ResurrectedTrader commented Jul 5, 2026

Copy link
Copy Markdown
Author

I'm still doing some testing, seems that I had UBER's flag set which caused most of A5 to crash and be skipped when using 1.10f mpqs.
Also, seems that I had version defines set to 1.14d, I think actually setting it to 1.10f causes regressions.
A separate claude session confirmed that the bug fixes are genuine when compared to what's visible in IDA, but there might be other skeletons that I haven't found yet.

@ResurrectedTrader ResurrectedTrader changed the title DRLG: fix six bugs closing the gap to 1.10f DRLG: fix seven bugs closing the gap to 1.10f Jul 5, 2026
@ResurrectedTrader

ResurrectedTrader commented Jul 5, 2026

Copy link
Copy Markdown
Author

Seems like the robot found another bug, but there must be another bug somewhere that neutralizes and fixes things:

● Verified via 1.10f DISASSEMBLY (sub_6FD89930 @ 0x6FD8997E-6FD89988) — the code IS strict match, IDENTICAL to my fix:                                                                                                                                                                                              
  0x6fd8997e: test ebp, ebp        ; ebp = (nTileType == 0)                                                                                                                                                                                                                                                         
  0x6fd89980: jnz loc_6FD899C7     ; FLOOR link + FLOOR call → USE                                                                                                                                                                                                                                                  
  0x6fd89982: jmp continue         ; FLOOR link + non-FLOOR call → continue                                                                                                                                                                                                                                         
  0x6fd89984: test ebp, ebp                                                                                                                                                                                                                                                                                         
  0x6fd89986: jz loc_6FD899C7      ; non-FLOOR link + non-FLOOR call → USE
● Update(dependencies\D2MOO\source\D2Common\src\Drlg\DrlgRoomTile.cpp)                                                                                                                                                                                                                                              
  ⎿  Added 2 lines, removed 7 lines                                                                                                                                                                                                                                                                                 
      887      D2DrlgTileLinkStrc* pCurLink;                                                                                                                                                                                                                                                                        
      888      for (pCurLink = pDrlgRoom->pTileGrid->pMapLinks; pCurLink; pCurLink = pCurLink->pNext)                                                                                                                                                                                                               
      889      {                                                                                                                                                                                                                                                                                                    
      890 -      // Strict type match: floor-link matches floor-call, non-floor-link matches non-floor-call.                                                                                                                                                                                                        
      891 -      // 1.10f D2Common.dll @ 0x6FD89977 and 1.14d Game.exe AddTileData @ 0x66E660 both use this;                                                                                                                                                                                                        
      892 -      // MOO's original port had a bogus second clause `nTileType != FLOOR` that matched ANY link                                                                                                                                                                                                        
      893 -      // for non-floor calls. Harmless in practice under 1.10f layout (rooms typically have                                                                                                                                                                                                              
      894 -      // exactly one tileLink at add-time) but visible with 1.14d structural changes.                                                                                                                                                                                                                    
      895 -      if ((pCurLink->bFloor && nTileType == TILETYPE_FLOOR) ||                                                                                                                                                                                                                                           
      896 -          (!pCurLink->bFloor && nTileType != TILETYPE_FLOOR))                                                                                                                                                                                                                                            
      890 +      if ((pCurLink->bFloor && nTileType == TILETYPE_FLOOR) || (nTileType != TILETYPE_FLOOR))                                                                                                                                                                                                            
      891        {                                                                                                                                                                                                                                                                                                  
      892 +        // Use current tile                                                                                                                                                                                                                                                                              
      893          break;                                                                                                                                                                                                                                                                                           
      894        }                                                                                                                                                                                                                                                                                                  
      895      }

undoing the bugfix (i.e., fixing the bug) makes it diverge from the game.

Nine independent bugs in `D2Common/src/Drlg/` where the ported C++
diverges from the retail x86 binary. All nine were found by driving
MOO's DRLG generator against **live retail game code running under
[d2mapapi](https://github.com/jcageman/d2mapapi)** and diffing the
output per-level, then reading the retail decompile in **IDA** to
confirm each divergence and match the fix to the original bytes. Fixes
1-7 were nailed against **1.10f `D2Common.dll`**; fixes 8-9 are MOO
reconstruction bugs cross-checked against **both** 1.10f
`D2Common.dll` and 1.14d `Game.exe` (they land the last residual
diffs on the 1.14d side and are byte-for-byte the same logic in both
retail versions, so they need no version gate).

With all nine fixes in place, MOO's DRLG output is **byte-perfect
against both targets** across a 5-seed sweep
`{42, 1234567, 1053646565, 987654321, 111222333}`:

- **1.10f code + 1.10f data (no UBERS) vs. 1.10f d2mapapi:** 132/132
  levels PERFECT every seed.
- **1.14d code + 1.14d data (with UBERS) vs. 1.14d d2mapapi:** 136/136
  levels PERFECT every seed - including the four Pandemonium-event
  "uber" levels 133-136 (Matron's Den, Forgotten Sands, Furnace of
  Pain, Uber Tristram).

Before these fixes the sweep showed dozens of `CELL_ONLY` diffs and a
handful of `PRESET_DIFF` residuals.

Fixes 1-6 land Acts I-IV parity. Fix 7 lands Act V outdoor parity - it
was masked earlier because a separate consumer-side setup issue
(missing UBERS `Levels.bin` records) had `DUNGEON_AllocAct(ACT_V, ...)`
NULL-deref before Act V DRLG code ran, so the crash-once-reachable X-scan
loop bug in `DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits` was never
exercised. Fixes 8-9 close the residual `CELL_ONLY` diffs that survived
1-7 on the 1.14d side. The related consumer-side UBERS build fix (so
the uber levels 133-136 generate at all instead of crashing) is
described in the closing note.

Every fix below cites the specific retail address(es) - 1.10f, and for
fixes 8-9 the 1.14d address too - so you can open the binary in your
own tools and check the pseudocode.

---

## Methodology

Same shape for every bug:

1. **Reproduce.** A clientless test compiles MOO's DRLG generator and,
   for each level (seed x difficulty x area), asks live 1.10f
   `D2Common.dll` through d2mapapi for the same input. We diff:
   - `mapRows` - subtile collision (per-cell int)
   - `npcs` / `objects` - preset unit sets `{id, x, y}`
   - `levelOrigin` / `width` / `height` - level bounding box
   Classification per level: `PERFECT` / `CELL_ONLY` / `PRESET_DIFF` /
   `STRUCTURAL`. Any non-`PERFECT` is a bug on MOO's side (d2mapapi is
   running the reference DLL, not our code).

2. **Bisect.** Env-gated `fprintf` traces inside MOO plus a
   **MinHook-based hook layer inside d2mapapi** log the same call sites
   on the 1.10f side (`DRLGACTIVATE_*`, `DRLGROOM_AllocRoomEx`,
   `SEED_RollRandomNumber`, `SpawnHardcodedPresetUnits`,
   `AddPresetUnitToDrlgMap`, ...). Both traces dump `pLevel->pSeed`,
   `pDrlgRoom->pSeed`, `wRoomsInList[]`, `fRoomStatus` and the file
   positions of every parsed `.ds1` unit. First divergence between the
   two logs pinpoints the offending function.

3. **Decompile.** Once the function is known, IDA Hex-Rays on the
   1.10f address gives the pseudocode.

4. **Patch and re-sweep.** Fix, rebuild, re-run the sweep. Escalate the
   sweep size (5 seeds -> 100 -> 1,000) once a fix survives the smaller
   pool.

Test hardness after this PR:

- **5-seed sweep** `{42, 1234567, 1053646565, 987654321, 111222333}` x
  103 levels = **515/515 PERFECT**.
- **100-seed sweep** (deterministic set from C#'s `System.Random(42).Next()`)
  x 103 levels = **10,300/10,300 PERFECT**, zero anomalies file.
- **1,000-seed sweep** currently running; no divergence observed in
  first ~500 seeds.

---

## Fix 1 - `DRLGACTIVATE_RoomExIdentifyRealStatus`: transition to `ROOMSTATUS_COUNT` was silently a no-op

**File:** `source/D2Common/src/Drlg/DrlgActivate.cpp`
**1.10f address:** `0x6FD73790`

### Symptom

Rooms whose visibility ref-counts had drained back to zero kept their
old `fRoomStatus`. `RoomExStatusUnset_Untile`'s
`fRoomStatus == ROOMSTATUS_COUNT` check therefore never passed,
`FreeRoom` never fired during propagation, and `pTileGrid` on
neighbouring rooms stayed alive throughout initial DRLG generation.
That in turn made `GetLinkedTileData` find pre-existing tiles on
already-processed rooms and take an early-return that 1.10f never takes
- visible as `CELL_ONLY` divergences on Act I wilderness levels.

### Root cause

The port routed the transition through
`DRLGACTIVATE_UpdateRoomExStatusImpl`, which is guarded by
`if (fRoomStatus > nStatus)` (lower = higher priority).
`ROOMSTATUS_COUNT = 4` is the highest value in the enum, so `X > 4` is
never true - the update path is dead for exactly the transition it's
meant to perform.

### Old code

```cpp
DRLGACTIVATE_UpdateRoomExStatusImpl(pDrlgRoom, nFirstStatusWithRefCount);
```

### Fix

```cpp
DRLGACTIVATE_RoomExStatusUnlink(pDrlgRoom);
if (nFirstStatusWithRefCount < ROOMSTATUS_COUNT)
{
    DRLGACTIVATE_RoomExStatuslink(&pDrlgRoom->pLevel->pDrlg->tStatusRoomsLists[nFirstStatusWithRefCount], pDrlgRoom);
}
pDrlgRoom->fRoomStatus = nFirstStatusWithRefCount;
```

### Evidence (1.10f `0x6FD73790`)

```c
void __fastcall DRLGACTIVATE_RoomExIdentifyRealStatus(struct D2DrlgRoomStrc *pDrlgRoom)
{
    fRoomStatus = pDrlgRoom->fRoomStatus;
    if ( fRoomStatus >= 4u || !pDrlgRoom->wRoomsInList[fRoomStatus] )
    {
        // ... scan wRoomsInList[] for first non-zero slot -> v2 (v3 = slot index)
        if ( fRoomStatus != v2 )
        {
            pStatusPrev = pDrlgRoom->pStatusPrev;
            if ( pStatusPrev )
            {
                pStatusNext = pDrlgRoom->pStatusNext;
                if ( pStatusNext )
                {
                    pStatusPrev->pStatusNext = pStatusNext;
                    pDrlgRoom->pStatusNext->pStatusPrev = pDrlgRoom->pStatusPrev;
                    pDrlgRoom->pStatusPrev = nullptr;
                    pDrlgRoom->pStatusNext = nullptr;
                }
            }
            if ( v2 < 4u )                                          // v2 < ROOMSTATUS_COUNT
            {
                v6 = &pDrlgRoom->pLevel->pDrlg->tStatusRoomsLists[v3];
                pDrlgRoom->pStatusNext = v6;
                pDrlgRoom->pStatusPrev = v6->pStatusPrev;
                v6->pStatusPrev->pStatusNext = pDrlgRoom;
                v6->pStatusPrev = pDrlgRoom;
                pDrlgRoom->fRoomStatus = v2;
            }
        }
        pDrlgRoom->fRoomStatus = v2;                                // unconditional
    }
}
```

Note the **unconditional** `pDrlgRoom->fRoomStatus = v2;` at the tail -
even the `v2 == COUNT` path (no new list to link into) still updates
the field. That is exactly what MOO was missing.

---

## Fix 2 - `DRLGACTIVATE_Update`: `for` loop that never executed

**File:** `source/D2Common/src/Drlg/DrlgActivate.cpp`
**1.10f address:** `0x6FD73F20`

### Symptom

The client-visibility "sweep" that walks the `ClientOutOfSight` list
and initialises out-of-sight rooms never ran a single iteration.

### Root cause

Textbook copy-paste of the loop-head pointer into the loop-continuation
predicate: both bounds are the same pointer, so the body never runs.
The 1.10f binary uses a proper `do { ... } while (p != head && ...)`
that starts at the head and terminates on the first return-to-head.

### Old code

```cpp
D2DrlgRoomStrc* pCurRoomEx;
for (pCurRoomEx = pDrlgRoomExListHead;
     pCurRoomEx != pDrlgRoomExListHead;               // always false
     pCurRoomEx = pCurRoomEx->pStatusNext)
{
    if (pCurRoomEx != &pDrlg->tStatusRoomsLists[ROOMSTATUS_CLIENT_OUT_OF_SIGHT])
    {
        DRLGACTIVATE_RoomEx_EnsureHasRoom(pCurRoomEx, false);
    }
    if (pDrlg->nRoomsInitSinceLastUpdate != 0)
    {
        break;
    }
}
```

### Fix

```cpp
D2DrlgRoomStrc* pCurRoomEx = pDrlgRoomExListHead;
do
{
    if (pCurRoomEx != &pDrlg->tStatusRoomsLists[ROOMSTATUS_CLIENT_OUT_OF_SIGHT])
    {
        DRLGACTIVATE_RoomEx_EnsureHasRoom(pCurRoomEx, false);
    }
    pCurRoomEx = pCurRoomEx->pStatusNext;
} while (pCurRoomEx != pDrlgRoomExListHead
         && pDrlg->nRoomsInitSinceLastUpdate == 0);
```

### Evidence (1.10f `0x6FD73F20`)

```c
void __stdcall DRLGACTIVATE_Update(struct D2DrlgStrc *pDrlg)
{
    // ...
    v4 = pDrlg->pDrlgRoom;
    if ( v4 )
    {
        v5 = pDrlg->pDrlgRoom;
        do
        {
            if ( (v5->dwFlags & 0x100000) == 0
              && v5 != &pDrlg->tStatusRoomsLists[2]                  // != CLIENT_OUT_OF_SIGHT list head
              && !v5->pRoom )
            {
                // ... allocate + init room, bump counters ...
            }
            v5 = v5->pStatusNext;
        }
        while ( v5 != v4 && !pDrlg->nRoomsInitSinceLastUpdate );      // <-- do/while
        pDrlg->pDrlgRoom = v5;
    }
}
```

Straightforward
`do { body; p = p->pStatusNext; } while (p != head && !nRoomsInitSinceLastUpdate);`.

---

## Fix 3 - `DRLGOUTPLACE_PlaceAct1245OutdoorBorders`: `bHasDirection` was sticky across vertex iterations

**File:** `source/D2Common/src/Drlg/DrlgOutPlace.cpp`
**1.10f address:** `0x6FD80ED7` (inside `0x6FD80E10`)

### Symptom

Once one vertex in the border chain OR'd `bHasDirection = true` into
`tLvlPrestPackedInfo.nPackedValue`, every subsequent iteration
inherited that flag regardless of its own vertex direction. On levels
whose border chain contains a non-endpoint vertex with `nDirection ==
1` (L6 Black Marsh being the canonical case), later segment cells
wrote the sticky `bHasDirection` flag and the river spawn refused to
place - visible as `STRUCTURAL` on L6.

### Root cause

The port initialised `tLvlPrestPackedInfo` **once outside the
`do/while` loop**. 1.10f recomputes the equivalent
(`v60 = 2*(v49 != 0) + 1`) at the top of every iteration.

### Old code

```cpp
D2DrlgOutdoorPackedGrid2InfoStrc tLvlPrestPackedInfo{ 0 };
tLvlPrestPackedInfo.nUnkb00 = true;
tLvlPrestPackedInfo.bHasDirection = pDrlgVertex->nDirection != 0;

do
{
    // ... loop body, no re-init here ...
} while (pDrlgVertex != pLevel->pOutdoors->pVertex);
```

### Fix

```cpp
D2DrlgOutdoorPackedGrid2InfoStrc tLvlPrestPackedInfo{ 0 };

do
{
    tLvlPrestPackedInfo.nPackedValue = 0;
    tLvlPrestPackedInfo.nUnkb00 = true;
    tLvlPrestPackedInfo.bHasDirection = pDrlgVertex->nDirection != 0;

    // ... rest of the loop body ...
} while (pDrlgVertex != pLevel->pOutdoors->pVertex);
```

### Evidence (1.10f `0x6FD80ED7`)

`v60` is the `D2DrlgOutdoorPackedGrid2InfoStrc` union local
(`tLvlPrestPackedInfo`, a bit-packed 32-bit value: bit 0 = `nUnkb00`,
bit 1 = `bHasDirection`, ...). Hex-Rays does not resolve the bit-packed
local so it prints the bit assignments as raw arithmetic - the
annotations below decode each expression back to its named field.
`v49` is the loop-local copy of `pDrlgVertex->nDirection`.

```c
void __fastcall DRLGOUTPLACE_PlaceAct1245OutdoorBorders(struct D2DrlgLevelStrc *pLevel)
{
    // ...
    for ( i = v5; ; v5 = i )
    {
        DRLGVER_GetCoordDiff(pVertex, &v52, &v51);
        // ...
        v49       = *(_BYTE *)(pVertex + 8);          // pDrlgVertex->nDirection
        nLevelType = v53->nLevelType;
        v60       = 2 * (v49 != 0) + 1;               // <-- per-iteration re-init:
                                                      //     nPackedValue  = 0
                                                      //     nUnkb00       = 1
                                                      //     bHasDirection = (v49 != 0)
        switch ( nLevelType ) { /* ... */ }
        // ... body ...
        if ( (struct D2DrlgVertexStrc *)pVertex == v56->pVertex )
            break;
    }
}
```

`v60 = 2 * (v49 != 0) + 1` is `(bHasDirection << 1) | nUnkb00` with
`nUnkb00 = 1` - i.e. the whole packed value is re-seeded per iteration
from the *current* vertex's `nDirection`. That is exactly what MOO's
post-fix
`tLvlPrestPackedInfo.nPackedValue = 0; nUnkb00 = true; bHasDirection = pDrlgVertex->nDirection != 0;`
does.

---

## Fix 4 - `DRLGOUTPLACE_PlaceAct1245OutdoorBorders`: `bHasDirection` was set true even when both vertices had `nDirection == 0`

**File:** `source/D2Common/src/Drlg/DrlgOutPlace.cpp`
**1.10f address:** `0x6FD81167` (inside `0x6FD80E10`)

### Symptom

Segments in wilderness borders whose current *and* next vertex both
had `nDirection == 0` were writing `bHasDirection = 1` into the packed
grid, where retail writes `bHasDirection = 0`. That perturbed
downstream `AlterGridFlag` calls on those cells, shifting a handful of
segment `LvlPrestId`s and causing per-seed `CELL_ONLY` diffs on
`LEVEL_MOOMOOFARM` and other non-{BLOODMOOR, COLDPLAINS,
BURIALGROUNDS} Act I wilderness levels.

`BLOODMOOR / COLDPLAINS / BURIALGROUNDS` run the pre-loop that sets
`pDrlgVertex->nDirection = 1` on select vertices, so at least one of
each vertex pair has a non-zero direction. Other wilderness levels
skip that pre-loop, so pairs with two adjacent zero-direction vertices
exist - which is exactly where the bug fires.

### Root cause

The port's else-branch (when `pDrlgVertex->nDirection == 0`) fell back
to `pNextVertex->nDirection` for the effective `nDirection`, but then
set `tLvlPrestPackedInfo.bHasDirection = true` **unconditionally** -
even if `pNextVertex->nDirection` was also zero. Retail only sets that
bit when the effective (short-circuited) direction is non-zero, so
when both vertices are `nDirection == 0` the bit stays clear.

### Old code

```cpp
nDirection = pDrlgVertex->nDirection;
if (nDirection)
{
    tLvlPrestPackedInfo.bHasDirection = true;
}
else
{
    nDirection = pNextVertex->nDirection;
    tLvlPrestPackedInfo.bHasDirection = true;   // <-- unconditional
}
```

### Fix

```cpp
nDirection = pDrlgVertex->nDirection;
if (nDirection == 0)
{
    nDirection = pNextVertex->nDirection;
}
if (nDirection)
{
    tLvlPrestPackedInfo.bHasDirection = true;   // <-- conditional on effective != 0
}

switch (pLevel->nLevelType)
{
case LVLTYPE_ACT1_WILDERNESS:
    v41 = nDirection == 0;
    break;
// ...
}
```

### Evidence (1.10f `0x6FD81167`)

`v49` is `pDrlgVertex->nDirection` at the top of the loop and is
rewritten to `pNextVertex->nDirection` by the `||` short-circuit here
if it was zero. `v60` is `tLvlPrestPackedInfo.nPackedValue`;
`LOBYTE(v38) = v60 | 2` sets bit 1 of that packed value
(`bHasDirection = true`). Crucially, that `|= 2` sits **inside** the
`if` — it fires only when the short-circuit produced a non-zero
effective direction. That's the retail behaviour MOO's `else` branch
was missing.

```c
// ... continuation of DRLGOUTPLACE_PlaceAct1245OutdoorBorders, tail block ...
pVertex = (int)i;                                                 // pNextVertex
if ( v49 || (v49 = *((_BYTE *)i + 8)) != 0 )                      // <-- short-circuit:
{                                                                 //   v49 stays pDrlgVertex->nDirection if != 0,
    v38 = v60;                                                    //   else v49 = pNextVertex->nDirection.
    LOBYTE(v38) = v60 | 2;                                        //   bHasDirection = true, ONLY inside the if
    v60 = v38;                                                    //   (bit 1 of nPackedValue)
}
switch ( v53->nLevelType )
{
    case 2:                                                       // LVLTYPE_ACT1_WILDERNESS
        v40 = v49 == 0;                                           // reads *effective* v49
        break;
    case 0x10:  v40 = 2; break;                                   // ACT2_DESERT
    case 0x1B:  v40 = 3; break;                                   // ACT4_MESA
    case 0x1F:  v40 = sub_6FD84100(v53); break;                   // ACT5_BARRICADE
    default:    v40 = -1; break;
}
```

1.14d's `Game.exe PlaceAct1245OutdoorBorders @ 0x675B60` has the same
`v59 || (v59 = pNextVertex->nDirection)` short-circuit followed by a
gated `v46 |= 2u`, so the two retail versions agree - no version
divergence to switch on.

---

## Fix 5 - `DRLGOUTWILD_SpawnRiver`: bridge row selection had the wrong modulus

**File:** `source/D2Common/src/Drlg/DrlgOutWild.cpp`
**1.10f address:** `0x6FD85258`

### Symptom

For outdoor levels with `OUTDOOR_RIVER | OUTDOOR_BRIDGE`, MOO tested a
different (and generally wrong) set of grid rows for a valid
bridge-crossing spawn cell than 1.10f. On some seeds this shifts the
crossing by a row; on others it fails to find a valid cell at all -
measurable as `CELL_ONLY` / `STRUCTURAL` on Act I wilderness levels
that host bridges.

### Root cause

MOO's modulus was `gridHeight - 1` with no `+ 1` offset. The reference
formula is `(nRand + i) % (gridHeight - 2) + 1`, which produces a
completely different set of visited rows.

### Old code

```cpp
int nRand = SEED_RollLimitedRandomNumber(&pLevel->pSeed, pLevel->pOutdoors->nGridHeight - 2);

for (int i = 0; i < pLevel->pOutdoors->nGridHeight - 2; ++i)
{
    int nY = (nRand + i) % (pLevel->pOutdoors->nGridHeight - 1);
    // ...
}
```

### Fix

```cpp
const int nRowRange = pLevel->pOutdoors->nGridHeight - 2;
int nRand = SEED_RollLimitedRandomNumber(&pLevel->pSeed, nRowRange);

for (int i = 0; i < nRowRange; ++i)
{
    int nY = (nRand + i) % nRowRange + 1;
    // ...
}
```

### Evidence (1.10f `0x6FD85258`, inside `DRLGOUTWILD_SpawnRiver` @ `0x6FD850B0`)

```c
void __fastcall DRLGOUTWILD_SpawnRiver(struct D2DrlgLevelStrc *pLevel, int nX)
{
    // ...
    v24 = pLevel->pOutdoors;
    v12 = v24->nGridHeight - 2;                                    // <-- modulus
    // ... seed roll into v21 = SEED_RollLimitedRandomNumber(&pLevel->pSeed, v12)
    v22 = 0;
    if ( v12 > 0 )
    {
        while ( 1 )
        {
            v15 = nX - 1;
            v16 = (int)(v21 + v22) % v12 + 1;                      // <-- (nRand + i) % (nGridHeight-2) + 1
            // ... TestGridCellSpawnValid checks at (v15, v16) and (nX+2, v16) ...
            if ( ++v22 >= v12 )
                return;
        }
    }
}
```

`(nRand + i) % (nGridHeight - 2) + 1` - modulus is `nGridHeight - 2`,
offset by `+ 1`.

---

## Fix 6 - `DRLG_OUTDOORS_GenerateDirtPath`: right-border column of the 9x9 floor grid was never written

**File:** `source/D2Common/src/Drlg/DrlgOutdoors.cpp`
**1.10f address:** `0x6FD7F240` (inside `0x6FD7EFE0`)

### Symptom

On Cold Plains and other Act I outdoor rooms, the right-border column
(`x = pDrlgCoord.nWidth`) of the floor grid stayed empty - two
floor-tile cells short of the reference. Directly visible as
`CELL_ONLY` diffs concentrated at the room's right edge.

### Root cause

The port's loop was `nX <= nWidth`, one iteration short of 1.10f's
loop, which breaks when `v14 > pDrlgCoord.nWidth + 1` - i.e. keeps
going through `nX == nWidth`, one column past what MOO covered.

### Old code

```cpp
for (int nX = 1; nX <= pDrlgRoom->pDrlgCoord.nWidth; ++nX)
```

### Fix

```cpp
for (int nX = 1; nX <= pDrlgRoom->pDrlgCoord.nWidth + 1; ++nX)
```

### Evidence (1.10f `0x6FD7F240`, tail of `DRLG_OUTDOORS_GenerateDirtPath` @ `0x6FD7EFE0`)

```c
void __fastcall DRLG_OUTDOORS_GenerateDirtPath(struct D2DrlgLevelStrc *pLevel,
                                               struct D2DrlgRoomStrc *pDrlgRoom)
{
    // ...
    v23 = &pDrlgRoom->pDrlgCoord;
    if ( p_pDrlgCoord->nWidth + 1 >= 1 )
    {
        v20 = 2;                                                   // starts at column 2 (1-based origin + 1)
        while ( 1 )
        {
            // ... body reads (v20-2, v13-1)..(v20, v13+1) neighbours ...
            v20 = v14 + 1;                                         // advance
            if ( v14 > v23->nWidth + 1 )                           // <-- break when v14 > nWidth + 1
                break;
            p_pDrlgCoord = v23;
        }
    }
}
```

The break at `0x6FD7F240` is
`if (v14 > pDrlgCoord.nWidth + 1) break;`, so the last iteration that
runs the body has `v14 == pDrlgCoord.nWidth + 1`, which corresponds to
writing at column `pDrlgCoord.nWidth`. MOO's
`nX <= pDrlgCoord.nWidth` bound is one iteration short.

---

## Fix 7 - `DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits`: X-scan loops used height instead of width

**File:** `source/D2Common/src/Drlg/DrlgOutSiege.cpp`
**1.10f address:** `0x6FD84580`

### Symptom

Act V wilderness levels (`LEVEL_BLOODYFOOTHILLS` L110,
`LEVEL_FRIGIDHIGHLANDS` L111) intermittently crashed with either
`EXCEPTION_ACCESS_VIOLATION` or `STATUS_HEAP_CORRUPTION` (0xC0000374)
partway through Act V classify. The failure was seed-sensitive but not
seed-deterministic - across 8 sequential runs of the same seed 987654321,
5 crashed at various levels between L109 and L132 (three at heap-corrupt
detection during a later alloc, one at direct AV) and 3 returned 132/132
PERFECT. Same MOO code path, same input; only the transient heap layout
differed.

### Root cause

The first two loops in `PlaceBarricadeEntrancesAndExits` are X-scans:
they walk cells along the top row (`nY = 0`) then along the second-to-
last row (`nY = nGridHeight - 2`), looking for the first `bLvlLink` cell
to spawn the top / bottom barricade preset on. The port bounded these
loops on `pOutdoors->nGridHeight` while passing the induction variable
as the X coord. Retail 1.10f bounds them on `pOutdoors->nGridWidth`.

Because Act V wilderness levels are always taller than they are wide
(the reproducing shape was `nGridWidth = 8`, `nGridHeight = 20`), the
induction variable ran to 19 - twelve cells past the row - and computed
`&pOutdoors->pGrid[2].pCellsFlags[nX + pCellsRowOffsets[nY]]` with `nX =
8..19`. `pCellsRowOffsets[nY]` still returned valid offsets, but the
resulting flat index landed anywhere from just past the row into
unmapped territory. The two Y-scan loops immediately after (left / right
columns) were correct - they iterate the Y coord and are already bound
by `nGridHeight`.

The wild write's target was determined by transient heap state (ASLR
+ prior CRT / logger / JSON allocations), so the same seed sometimes
overwrote a benign heap block (silent, test still passed), sometimes
corrupted a block header (heap manager caught it at the next
alloc / free as `STATUS_HEAP_CORRUPTION`), and sometimes hit an
unmapped page (immediate AV). None of the outcomes were about MOO's
DRLG decisions differing between runs; the DRLG path itself was fully
deterministic.

### Why this wasn't caught earlier

`DUNGEON_AllocAct(ACT_V, ...)` used to NULL-deref inside
`sub_6FD823C0(gAct5UbersDrlgLink, ...)` because `Levels.bin` in the
build being tested lacked the 1.11+ UBERS records that the D2COMMON
build was compiled to reference. Every historical sweep aborted at Act
V setup and only classified 103 levels/seed - Acts I-IV - so
`PlaceBarricadeEntrancesAndExits` was never reached. Unblocking Act V
comparison (see the header note) is what made this seven-year-old
loop-bound typo observable.

### Old code

```cpp
void __fastcall DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(D2DrlgLevelStrc* pLevel)
{
    for (int i = 0; i < pLevel->pOutdoors->nGridHeight; ++i)   // <-- nGridHeight
    {
        if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i, 0).bLvlLink)
        {
            DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, i, 0,
                LVLPREST_ACT5_BARRICADE_EXIT_32X16,
                2 * (pLevel->nLevelId == LEVEL_BLOODYFOOTHILLS) - 1, 0);
            break;
        }
    }

    for (int i = 0; i < pLevel->pOutdoors->nGridHeight; ++i)   // <-- nGridHeight
    {
        if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i,
                pLevel->pOutdoors->nGridHeight - 2).bLvlLink)
        {
            DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, i,
                pLevel->pOutdoors->nGridHeight - 2,
                LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16,
                2 * (pLevel->nLevelId == LEVEL_BLOODYFOOTHILLS) - 1, 0);
            break;
        }
    }

    // Y-scan loops (unchanged) follow ...
}
```

### Fix

```cpp
for (int i = 0; i < pLevel->pOutdoors->nGridWidth; ++i)        // <-- nGridWidth
{
    if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i, 0).bLvlLink)
    {
        // ...
    }
}

for (int i = 0; i < pLevel->pOutdoors->nGridWidth; ++i)        // <-- nGridWidth
{
    if (DRLGOUTDOORS_GetPackedGrid2Info(pLevel->pOutdoors, i,
            pLevel->pOutdoors->nGridHeight - 2).bLvlLink)
    {
        // ...
    }
}
```

### Evidence (1.10f `0x6FD84580`)

```c
void __fastcall DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(struct D2DrlgLevelStrc *pLevel)
{
    int v2, v4, v5, v6, v7, v8;
    struct D2DrlgOutdoorInfoStrc *pOutdoors;

    v2 = 0;
    pOutdoors = pLevel->pOutdoors;
    if ( pOutdoors->nGridWidth > 0 )                                       // <-- X-scan bound by nGridWidth
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], v2, 0) & 0x400) == 0 )
        {
            if ( ++v2 >= pOutdoors->nGridWidth )                           //     nGridWidth
                goto LABEL_6;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, v2, 0, 909,         // 909 = LVLPREST_ACT5_BARRICADE_EXIT_32X16
            2 * (pLevel->nLevelId == 110) - 1, 0);                         // 110 = LEVEL_BLOODYFOOTHILLS
    }
LABEL_6:
    v4 = 0;
    v5 = pOutdoors->nGridHeight - 2;                                       // bottom row = nGridHeight - 2
    if ( pOutdoors->nGridWidth > 0 )                                       // <-- X-scan bound by nGridWidth
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], v4, v5) & 0x400) == 0 )
        {
            if ( ++v4 >= pOutdoors->nGridWidth )                           //     nGridWidth
                goto LABEL_11;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, v4, v5, 908,        // 908 = LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16
            2 * (pLevel->nLevelId == 110) - 1, 0);
    }
LABEL_11:
    v6 = 0;
    if ( pOutdoors->nGridHeight > 0 )                                      // <-- Y-scan bound by nGridHeight
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], 0, v6) & 0x400) == 0 )
        {
            if ( ++v6 >= pOutdoors->nGridHeight )
                goto LABEL_16;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, v6, 910,         // 910 = LVLPREST_ACT5_BARRICADE_EXIT_16X32
            2 * (pLevel->nLevelId == 110) - 1, 0);
    }
LABEL_16:
    v7 = pOutdoors->nGridWidth - 2;                                        // right column = nGridWidth - 2
    v8 = 0;
    if ( pOutdoors->nGridHeight > 0 )                                      // <-- Y-scan bound by nGridHeight
    {
        while ( (DRLGGRID_GetGridEntry(&pOutdoors->pGrid[2], v7, v8) & 0x400) == 0 )
        {
            if ( ++v8 >= pOutdoors->nGridHeight )
                return;
        }
        DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, v7, v8, 907,        // 907 = LVLPREST_ACT5_BARRICADE_ENTRANCE_16X32
            2 * (pLevel->nLevelId == 110) - 1, 0);
    }
}
```

The pattern is symmetric: X-scans (loops 1 and 2 - top row / bottom
row) are bounded by `nGridWidth`; Y-scans (loops 3 and 4 - left column
/ right column) are bounded by `nGridHeight`. The port had the two
X-scans using the Y-scan bound.

---

## Fix 8 - `DRLGACTIVATE_RoomExPropagateSetStatus` / `DRLGACTIVATE_RoomSetAndPropagateStatus`: status handler re-fired on every propagation instead of once at the 0->1 ref transition

**File:** `source/D2Common/src/Drlg/DrlgActivate.cpp`
**1.10f address:** `0x6FD73A30` (`DRLGACTIVATE_RoomExPropagateSetStatus`)
**1.14d address:** `0x61B390` (`InitNearRooms`) and `0x61B490`
(`AddRoomDataActual`)

### Symptom

The per-status handlers in `gRoomExSetStatus[]` (which tile / untile a
room and adjust its `pTileGrid`) fired more than once for the same
room+status as a status was propagated across the near-room set,
instead of exactly once on the room's first reference at that status.
The extra dispatches re-tiled rooms the reference leaves untouched
during a given propagation, leaving stale/duplicated tile emission -
the residual `CELL_ONLY` diffs that survived Fixes 1-7 on the 1.14d
side (e.g. L28 Barracks at seed 1053646565).

### Root cause

Retail keeps a per-room `wRoomsInList[ROOMSTATUS_COUNT]` array of
cumulative reference counts, one slot per status. When a status is
applied, it walks `wRoomsInList[0..nStatus]` and dispatches the status
handler **only if every one of those slots is still zero** - the
`0 -> 1` first-reference transition - and only then increments
`wRoomsInList[nStatus]`.

MOO reconstructed the gate as `nFirstStatusWithRefCount == nStatus`
alone. `DRLGACTIVATE_RoomExFindFirstStatusWithRefCount` returns the
first slot in `[0..]` with a non-zero count, so `== nStatus` is true in
**two** distinct situations: (a) slots `[0..nStatus-1]` are empty and
`[nStatus]` is *already non-zero* (a later reference - retail does NOT
dispatch here), and (b) all of `[0..nStatus]` are empty (the genuine
first reference - retail dispatches). The reconstruction dispatched in
both, so it re-fired the handler on every propagation while the lower
slots happened to be empty. The missing term is
`wRoomsInList[nStatus] == 0`, which excludes case (a).

### Old code

```cpp
const D2DrlgRoomStatus nFirstStatusWithRefCount =
    DRLGACTIVATE_RoomExFindFirstStatusWithRefCount(pNearRoom, D2DrlgRoomStatus(nStatus));
if (nFirstStatusWithRefCount == nStatus)
{
    gRoomExSetStatus[nStatus](pNearRoom);
}
```

### Fix

```cpp
const D2DrlgRoomStatus nFirstStatusWithRefCount =
    DRLGACTIVATE_RoomExFindFirstStatusWithRefCount(pNearRoom, D2DrlgRoomStatus(nStatus));
if (nFirstStatusWithRefCount == nStatus && pNearRoom->wRoomsInList[nStatus] == 0)
{
    gRoomExSetStatus[nStatus](pNearRoom);
}
```

(the same one-line addition applies to the sibling gate in
`DRLGACTIVATE_RoomSetAndPropagateStatus`, keyed on `pDrlgRoom`.)

### Evidence (1.10f `0x6FD73A30`)

`v10` is the near room (`D2DrlgRoomStrc*`), `v10 + 174` is
`wRoomsInList[0]`, `a3` is `nStatus`. The `while (!*v12)` loop advances
through `wRoomsInList[0], [1], ..., [nStatus]`; only when the index has
passed `nStatus` (`v11 > a3`, i.e. **all** those slots were zero) does
it dispatch. The `++wRoomsInList[nStatus]` sits *after* the check, so
the gate sees the pre-transition state.

```c
if ( (unsigned __int8)v10[172] >= (int)(unsigned __int8)a3 )       // fRoomStatus >= nStatus
{
    v11 = 0;
    v12 = v10 + 174;                                               // &wRoomsInList[0]
    while ( !*v12 )                                                // while wRoomsInList[v11] == 0
    {
        ++v11;
        ++v12;
        if ( v11 > (unsigned __int8)a3 )                           // walked [0..nStatus] all-zero
        {
            funcs_6FD73AB6[(unsigned __int8)a3](v10);              // gRoomExSetStatus[nStatus]
            break;
        }
    }
}
++*(_WORD *)&v10[2 * (unsigned __int8)a3 + 174];                   // ++wRoomsInList[nStatus] (AFTER)
```

### Evidence (1.14d `0x61B390` `InitNearRooms`, mirrored at `0x61B490` `AddRoomDataActual`)

Same loop shape, `wRoomsInList` at `pRoom + 12`, `fRoomStatus` at
`pRoom + 68`:

```c
if ( *(unsigned __int8 *)(v11 + 68) >= (int)a3 )                   // fRoomStatus >= nStatus
{
    v12 = 0;
    v13 = (_WORD *)(v11 + 12);                                     // &wRoomsInList[0]
    while ( !*v13 )                                                // while wRoomsInList[v12] == 0
    {
        ++v12;
        ++v13;
        if ( v12 > a3 )                                            // walked [0..nStatus] all-zero
        {
            funcs_61B410[a3](v11);                                 // gRoomExSetStatus[nStatus]
            break;
        }
    }
}
++*(_WORD *)(v11 + 2 * a3 + 12);                                   // ++wRoomsInList[nStatus] (AFTER)
```

Both retail versions gate on "all of `wRoomsInList[0..nStatus]` are
zero" and increment afterwards - identical logic, so the fix is
ungated.

---

## Fix 9 - `DRLGROOMTILE_AddLinkedTileData`: non-floor tiles matched any link, chaining onto the floor tile list

**File:** `source/D2Common/src/Drlg/DrlgRoomTile.cpp`
**1.10f address:** `0x6FD89930` (`DRLGROOMTILE_AddLinkedTileData`)
**1.14d address:** `0x66E620` (`DRLGROOMTILE_AddTileData`)

### Symptom

When adding a non-floor tile (wall / shadow / warp) to a room's tile
grid, MOO walked the `pTileGrid->pMapLinks` list and stopped at the
**first** link whose `bFloor` flag did not matter, so a non-floor tile
could be chained onto the *floor* link's tile list (and vice-versa the
match was too loose). The wrong link's tile list then fed the wrong
tile into collision emission - `CELL_ONLY` diffs on rooms with both
floor and non-floor links.

### Root cause

The link-match predicate needs the link's `bFloor` to **equal**
whether the incoming tile is a floor tile:
`link->bFloor == (nTileType == TILETYPE_FLOOR)`. MOO's reconstruction
was `(bFloor && nTileType == FLOOR) || (nTileType != FLOOR)`, whose
second clause matches **any** link as soon as the tile is non-floor -
so a non-floor tile grabbed whatever link came first, including the
floor link.

### Old code

```cpp
for (pCurLink = pDrlgRoom->pTileGrid->pMapLinks; pCurLink; pCurLink = pCurLink->pNext)
{
    if ((pCurLink->bFloor && nTileType == TILETYPE_FLOOR) || (nTileType != TILETYPE_FLOOR))
    {
        break;
    }
}
```

### Fix

```cpp
for (pCurLink = pDrlgRoom->pTileGrid->pMapLinks; pCurLink; pCurLink = pCurLink->pNext)
{
    if (pCurLink->bFloor == (nTileType == TILETYPE_FLOOR))
    {
        break;
    }
}
```

### Evidence (1.10f `0x6FD89930`)

`i` walks `pMapLinks` (`i[2]` = `pNext`), `*i` = `link->bFloor`, `v6` =
`nTileType` (0 == `TILETYPE_FLOOR`). The two branches accept a link
only when `bFloor` and "is-floor" agree, and a freshly allocated link
is seeded `*v10 = (v6 == 0)` - i.e. `bFloor = (nTileType == FLOOR)`:

```c
for ( i = **(_DWORD ***)(a2 + 168); i; i = (_DWORD *)i[2] )        // walk pMapLinks
{
    if ( *i )                     { if ( !v6 ) goto LABEL_11; }    // bFloor:  match iff nTileType == FLOOR
    else if ( v6 )                goto LABEL_11;                   // !bFloor: match iff nTileType != FLOOR
}
v10 = Fog_10045(a1, 12, ...);                                     // no match -> alloc new link
*v10 = v6 == 0;                                                   // bFloor = (nTileType == FLOOR)
```

### Evidence (1.14d `0x66E620`)

Byte-identical predicate, `pMapLinks` at `a1 + 84`:

```c
for ( i = **(_DWORD ***)(a1 + 84); i; i = (_DWORD *)i[2] )
{
    if ( *i )                     { if ( !a2 ) goto LABEL_11; }    // a2 = nTileType
    else if ( a2 )                goto LABEL_11;
}
v9 = AllocServerMemory(".\\DRLG\\RoomTile.cpp", 0x37Bu, 0);
*v9 = a2 == 0;                                                    // bFloor = (nTileType == FLOOR)
```

`if (*i) { if (!nTileType) match; } else if (nTileType) match;` is
exactly the boolean equality `link->bFloor == (nTileType == FLOOR)` in
both retail versions.

---

## Note (not a bug) - the UBERS build flag must be defined for the 1.14d uber levels

Not a DRLG-code fix, but the prerequisite that makes the 1.14d
uber-level comparison (levels 133-136) possible at all. In D2MOO,
`D2_VERSION_HAS_UBERS` is derived inside `D2BuildInformation.h`
(`#if D2_VERSION_MAJOR >= 1 && D2_VERSION_MINOR >= 11`), which is
reached through `D2Config.h`. A build that force-includes only the
narrow headers it needs (as the clientless consumer here does) never
pulls in `D2Config.h`, so the macro stays undefined and **every**
`#ifdef D2_VERSION_HAS_UBERS` block compiles out: the uber level-id
enum entries (`LEVEL_PANDEMONIUMRUN1..FINALE`) cease to exist, and the
uber routing in `DRLGOUTDOORS_GenerateLevel` (force level 134 to the
Act II outdoor initializer) and `DRLGOUTDESR_InitAct2OutdoorLevel`
(the `case LEVEL_PANDEMONIUMRUN2:` desert-warp preset) is stripped.
Level 134 then falls through to `DRLG_GetActNoFromLevelId`'s Act V
result and runs the Act V siege init, which null-derefs.

The D2MOO uber code itself is correct - it keys on
`LEVEL_PANDEMONIUMRUN2`, which resolves to 134 once the macro is
defined, and the uber enum entries are appended (no id shift for the
standard 1-132 levels). The fix is purely build configuration: define
`D2_VERSION_HAS_UBERS` for the 1.14d build (or force-include
`D2BuildInformation.h`). With it defined, levels 133-136 generate and
match 1.14d d2mapapi 136/136 with zero cell/preset diffs.

---

## Note (not a bug) - DS1 loaders must pad past EOF

Not a fix in this PR, but worth flagging for anyone driving MOO's
DRLG generator against retail assets: some retail `.ds1` files declare
more records in a section header than actually exist on disk. The
canonical case is 1.10f's `Act1\Outdoors\Trees.ds1`, whose header says
`nSubstGroups = 14` but only 13 complete records follow before EOF.
MOO's DS1 parser trusts the header and reads the declared count
straight out of the file buffer, so on the 14th read it walks past
EOF. On the real game this is masked - Storm's allocator zero-fills
tail bytes and the read returns zeros. Buffers sized to exactly the
file length return uninitialised heap memory, and the garbage records
perturb DRLG cell emission as CELL_ONLY diffs (L6 Black Marsh is the
level where this shows up first, since it's the one that pulls
Trees.ds1 as a preset).

Pad DS1 buffers by 256 zero bytes past EOF and the parser behaves as
it does on retail.

---

## Test results

### All nine fixes - dual-version parity (5-seed)

Seeds `{42, 1234567, 1053646565, 987654321, 111222333}`, difficulty 0,
each level compared against the matching-version d2mapapi:

```
1.10f code + 1.10f data (no UBERS)  vs 1.10f d2mapapi:
    per seed: 132/132 PERFECT   (max level 132, no ubers)
    5 seeds:  660/660 PERFECT    CELL_ONLY=0 PRESET_DIFF=0 STRUCTURAL=0

1.14d code + 1.14d data (with UBERS) vs 1.14d d2mapapi:
    per seed: 136/136 PERFECT   (incl uber levels 133-136)
    5 seeds:  680/680 PERFECT    CELL_ONLY=0 PRESET_DIFF=0 STRUCTURAL=0
```

Every level in both configurations matches on collision cells, preset
NPCs, and preset objects. The 1.10f run reaches level 132 (no ubers,
correct); the 1.14d run reaches 136, adding the four Pandemonium-event
levels.

### Acts I-IV parity (Fixes 1-6)

The 100-seed sweep was empty (`sweep-100/anomalies.txt` has 0 lines)
after Fixes 1-6. Full summary line was:

```
Total across 100 seeds: PERFECT=10300  CELL_ONLY=0  PRESET_DIFF=0
                        STRUCTURAL=0  (of 10300)
```

Level breakdown per seed: **103 levels** classified per seed (Acts
I-IV). Act V levels were skipped by the sweep because
`DUNGEON_AllocAct(ACT_V, ...)` was NULL-deref'ing on the missing UBERS
`Levels.bin` records - a consumer-side setup problem, not a MOO DRLG
bug.

**1,000-seed sweep** (Acts I-IV configuration) completed clean:
**1000/1000 seeds, all PERFECT** across 103 levels/seed = 103,000
level comparisons. Zero CELL_ONLY, zero PRESET_DIFF, zero STRUCTURAL.

### Act V parity (Fix 7)

Once the consumer-side UBERS problem was worked around and Act V
classification was actually reached, Fix 7 was needed to land parity
on Act V outdoor levels. The 5-seed sweep after Fix 7:

```
Total across 5 seeds: PERFECT=660  CELL_ONLY=0  PRESET_DIFF=0
                      STRUCTURAL=0  (of 660)
```

= 132 levels/seed classified (all of Acts I-V), 5 seeds
`{42, 1234567, 1053646565, 987654321, 111222333}`. Seed 987654321 was
the one that reliably exercised the loop-bound bug via crashing
before the fix; post-fix it hits 132/132 PERFECT across 8 repeated
sequential runs (each with a fresh d2mapapi server), confirming the
non-determinism is gone.

A fresh **1,000-seed sweep with all seven fixes and Act V unlocked**
(132 levels/seed = 132,000 comparisons) is queued to run; results
will be posted as a follow-up when it completes.

---

## Attribution

This investigation was carried out with **Claude Code (Anthropic
Claude Opus 4.7 / 4.8)** driving the reproduction harness, the
bisect hooks, the IDA queries, and the source edits.
@ResurrectedTrader

Copy link
Copy Markdown
Author

Ok, this seems to fix all of the issues.
This now generates 1.10f and 1.14d + UBERS flag correctly for 100 seeds on each version.

@ResurrectedTrader ResurrectedTrader changed the title DRLG: fix seven bugs closing the gap to 1.10f DRLG: fix nine bugs closing the gap to 1.10f Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants