diablo2-web/tools/d2moo-oracle
troytt d393a48043 feat(drop): embed MonStats and SuperUniques with 1.13c multi-diff TC parity (Fixes #410, #411)
- Pack and embed authentic 1.13c MonStats drop columns (734 kinds) with noRatio support
- Embed SuperUniques multi-difficulty TCs (66 unique bosses) and fail-fast loading
- Fix SuperUnique minion TC3 inheritance and minion monsterType identification
- Add adversarial drop parity audit and superunique minion test suites
- Update D2MOO oracle seed generator and Paladin skill tree parity
2026-09-25 13:44:35 +00:00
..
include feat(drop): embed MonStats and SuperUniques with 1.13c multi-diff TC parity (Fixes #410, #411) 2026-09-25 13:44:35 +00:00
src feat(drop): embed MonStats and SuperUniques with 1.13c multi-diff TC parity (Fixes #410, #411) 2026-09-25 13:44:35 +00:00
.gitignore feat(drop): embed MonStats and SuperUniques with 1.13c multi-diff TC parity (Fixes #410, #411) 2026-09-25 13:44:35 +00:00
README.md feat(drop): embed MonStats and SuperUniques with 1.13c multi-diff TC parity (Fixes #410, #411) 2026-09-25 13:44:35 +00:00
build.sh feat(drop): embed MonStats and SuperUniques with 1.13c multi-diff TC parity (Fixes #410, #411) 2026-09-25 13:44:35 +00:00

README.md

D2MOO DRLG oracle

Native build of D2MOO's D2Common level generator (DRLG). It is the reference the TypeScript Act I outdoor port (src/game/drlg/) is diffed against, stage by stage and RNG call by RNG call.

D2MOO's source is not vendored. The build reads it from D2MOO_SRC (default ~/tmp/D2MOO), pinned to commit 5596f5cb6c5251a0a07c6637d26458b06099d516 (MIT license). Only this driver, the runtime shims and the build script live in this repository.

Build and run

# 1. Game data: compiled tables, every DS1 of LvlPrest/LvlSub, every DT1 the DRLG loads.
npx tsx scripts/extract-d2moo-tables.ts samples/d2 tools/d2moo-oracle/data

# 2. 32-bit build (needs g++ with -m32 / gcc-multilib).
./tools/d2moo-oracle/build.sh

# 3. Generate.
./tools/d2moo-oracle/build/d2moo-oracle --data tools/d2moo-oracle/data \
  --seed 0x12345678 --levels 2,3,4,5,6,7,17,39 \
  [--difficulty 0] [--isolated] [--trace out.trace] --out out.json
  • --seed is the D2 game seed (DRLG_AllocDrlg nInitSeed). The whole act, including the level layout of DRLGOUTPLACE_CreateLevelConnections, derives from it.
  • --levels must be Act I outdoor levels (DrlgType 3).
  • --isolated allocates a fresh act for every level. The output must equal the sequential run, which proves that a level's generation does not depend on other levels.
  • Both build/ and data/ are git-ignored.

The binary is 32-bit so that every D2MOO struct has the exact layout of the compiled 1.13c .bin tables. LoadBinTable checks count * sizeof(record) == body for every table.

What runs natively

The driver runs D2MOO's own code for:

  • DRLG_AllocDrlg: the seed chain, DRLGOUTPLACE_CreateLevelConnections and the town.
  • DRLG_InitLevel, then DRLGOUTDOORS_GenerateLevel and DRLGOUTWILD_InitAct1OutdoorLevel.
  • Room creation.
  • Room activation, mirroring DRLGACTIVATE_InitializeRoomEx (DrlgActivate.cpp:317-331) and DRLGACTIVATE_RoomEx_EnsureHasRoom (95-105) up to DRLGROOMTILE_InitRoomGrids:
    1. DRLGROOMTILE_LoadDT1FilesForRoom
    2. DRLGPRESET_SpawnHardcodedPresetUnits (preset rooms only)
    3. sub_6FD77BB0
    4. DRLGROOMTILE_InitRoomGrids, which re-seeds the room from dwInitSeed.

The outside world is provided by src/stubs.cpp. Its policy: anything the generator genuinely needs is implemented with real data, and everything else fails fast (exit code 2).

Real implementations:

  • Data tables: loaded in the same relative order as DATATBLS_LoadAllTxts. The monster tables come first because the LvlSub DS1 loader translates monster preset units through MonPreset.
  • DS1 archive reads.
  • DUNGEON_GameTileToSubtileCoords and DUNGEON_GameTileToClientCoords.
  • sub_6FDAB750 and sub_6FDAB610 (src/path_misc.cpp, copied from D2MOO PathMisc.cpp).
  • D2CMP tile libraries, parsed from the real DT1 files. This is not optional: DRLGROOMTILE_GetTileCache rolls the room seed iff the rarity sum of the matching tiles is > 0. It runs inside LvlSub stamping (sub_6FD8ACE0 → DRLGROOMTILE_InitTileShadow), so the tile data advances the substitution RNG stream.
    • For Act I every shadow lookup returns rarity 0, so there is no roll. This is visible in the trace as T 13 … 1 0.
    • The order of the returned tiles (slot order, then DT1 file order) is not verified against D2CMP.dll. It only decides which tile variant is picked, and the oracle does not dump that.

Modelling decisions (documented, not guessed):

  • Rooms are activated in pFirstRoomEx order. A preset map that spans several rooms loads its DS1 with the seed of the first activated room. That seed only matters for the randomly skipped units of DRLGPRESET_AddPresetUnitToDrlgMap (Tormentor, Taintbreeder, Riftwraith, floor traps, object 581), and none of those occur in Act I outdoor DS1s.
  • Activating a room initialises the levels behind its warps (sub_6FD77BB0 → DRLG_InitLevel). The sequential mode therefore generates every requested level first and activates the rooms afterwards, so each level's RNG trace stays under its own context.
  • Tile selection (DRLGROOMTILE_AddRoomMapTiles) and unit spawning are outside the oracle.

Output (schema 2)

{schema: 2, oracle: {d2moo, arch: "i386"}, gameSeed, difficulty, isolated,
 drlg: {startSeed, seed: [low, high]},
 act: [{id, drlgType, levelType, coord: [x, y, w, h], flags, seed,
        outdoor?: {flags, roomData: [orth]}, preset?: {direction, map}}],   // sorted by id
 levels: [{id,
   levelGrid: {flags, coord, grids: [grid x4], ring: [vertex], nVertices,
               vertices: [vertex x24], pathStarts: [null | [[x, y, dir, flags]...] x6],
               roomData: [orth], seed},                 // after InitAct1OutdoorLevel, before rooms
   nRooms, rooms: [{type, coord, flags, otherFlags, dt1Mask, initSeed, seed,
                    outdoor?: {flags, flagsEx, subType, subTheme, subThemePicked},
                    preset?: {prest, picked, flags, map}}],                  // creation order
   maps: [{prest, picked, coord, file, hasInfo, units}],
   levelSeedAfterRooms, warp: {n, xy},
   activation: [{seed, tile, wall, floor, dirt} |                            // outdoor rooms
                {seed, walls: [grid], tiles: [grid], floors: [grid], shadow}, // preset rooms
                ... units]}]}
  • grid is {w, h, cells: [row-major int32]}, or null when the grid was never allocated.
  • vertex is [x, y, direction, flags, next]. next is -1 for null, 0..23 for pVertices[i], and 100+k for the k-th ring vertex.
  • orth is {level, dir, preset, init, box}.
  • Units are [type, index, mode, x, y, spawned].
  • All values are the raw packed words exactly as D2Common stores them.

RNG trace (--trace)

The trace has one event per line. The TypeScript port writes the same format:

I <label> <low>                    SEED_InitLowSeed (high seed 666)
S <label> <low> <high>             SEED_SetSeeds
R <label> 0 <low>:<high>           SEED_RollRandomNumber, 64-bit state after the roll (hex)
L <label> <max> <result>           SEED_RollLimitedRandomNumber (max <= 0 does not roll)
P <label> 100 <result>             SEED_RollPercentage
T <type> <style> <seq> <n> <sum>   D2CMP_10088_GetTiles lookup: n tiles, rarity sum
# <context>                        driver context (drlg, L<id>, L<id>/a<room>)

Labels are <context>#<n>, assigned when a seed is initialised. The build fails if any D2MOO object that rolls a seed was not compiled against the traced include/D2Seed.h.

Defects of the previous ad-hoc runner (~/tmp/d2moo_runner)

Its output must not be used as a reference:

  1. Outdoor rooms called DRLGOUTPLACE_InitOutdoorRoomGrids directly. That skipped DRLGROOMTILE_InitRoomGrids' re-seed from dwInitSeed (D2Common.0x6FD89FA0), so every substitution was rolled from the wrong seed.
  2. sub_6FDAB750 was stubbed to return 0. It drives the dirt-path search in sub_6FD80750, so the dirt paths were corrupted.
  3. Missing files returned a zeroed 64-byte buffer. MonPreset, Objects and SuperUniques were dummy tables. FOG_DisplayAssert and FOG_DisplayWarning only printed.
  4. It was a 64-bit build, which needed a hand-patched LvlSub layout.
  5. D2CMP tile lookups were no-ops, so the shadow rarity rolls could not happen.