# D2MOO DRLG oracle Native build of [D2MOO](https://github.com/ThePhrozenKeep/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 ```sh # 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 # or: OUT=/some/dir ./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. - `build.sh` compiles in a temporary directory and installs the binary into `$OUT` (default `tools/d2moo-oracle/build`) with `cp` to `d2moo-oracle.tmp` followed by `mv` over `d2moo-oracle`, so a concurrently running oracle is never replaced by a half-written file. - 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-115) up to `DRLGROOMTILE_AddRoomMapTiles`: 1. `DRLGROOMTILE_LoadDT1FilesForRoom` 2. `DRLGPRESET_SpawnHardcodedPresetUnits` (preset rooms only) 3. `sub_6FD77BB0` 4. `DRLGROOMTILE_InitRoomGrids`, which re-seeds the room from `dwInitSeed`. 5. `DRLGROOMTILE_AddRoomMapTiles`: the tile data of the room (the variant picks of `DRLGROOMTILE_GetTileCache` roll the room seed), the room-seam merge with the near rooms (`DRLGROOMTILE_GetCreateLinkedTileData` / `DRLGROOMTILE_LinkedTileDataManager`), the warps (`DRLGROOMTILE_AddWarp`), then `DRLGOUTROOM_InitializeDrlgOutdoorRoom` or `DRLGPRESET_AddPresetRoomMapTiles` (both build the `DRLGLOGIC_*` coord lists). `DRLG_CreateRoomForRoomEx` (the D2ActiveRoomStrc, built by D2Game) is not run. 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 decides which tile variant `DRLGROOMTILE_GetTileCache` picks, which `mapTiles` dumps (`pTile`); the TypeScript shim (`src/game/drlg/drlg-d2cmp.ts`) uses the same order. - `FOG_AllocPool` / `FOG_ReallocPool` are `calloc` / `realloc`. `FOG_ReallocPool` zero-fills the grown part of the block (glibc `calloc` clears the whole usable size), so the entries that `DRLGROOMTILE_ReallocRoofTileGrid` appends are deterministic. D2Common never writes `unk0x10`, `nIntensity` and `unk0x2C` of those entries; in the game they keep whatever the pool block held. **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. - The activation order also decides the room-seam merge: the room activated later edits the tiles of the near rooms activated before it (also across level seams) and rolls their seeds. - 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. - `pDrlgRoom->pRoom` stays NULL for every room, so `D2Common_COLLISION_FirstFn_6FD41000` is a no-op (D2Collision.cpp returns when `pRoom` is NULL). In the game a room activated earlier already has its active room, so the seam merge of a later room also updates that room's collision grid. That does not change the DRLG tile data or the RNG. - Unit spawning and `DRLG_CreateRoomForRoomEx` are outside the oracle. ## Output (schema 3) ```text {schema: 3, 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, units, seedAfterTiles, mapTiles} | // outdoor rooms {seed, walls: [grid], tiles: [grid], floors: [grid], shadow, units, // preset rooms seedAfterTiles, mapTiles}]}]} mapTiles: {nWalls, nFloors, nShadows, // pTileGrid counters pTiles: {nWalls, nFloors, nRoofs}, // pTileGrid->pTiles counters walls: [tile], floors: [tile], roofs: [tile],// pTiles arrays, exactly pTiles.nX entries each links: [[bFloor, tileRef]], // pTileGrid->pMapLinks chain warps: [[[levelId, room] | null, lvlWarpRow, bEnabled, tileRef, tileRef]], // pRoomTiles chain units, // pPresetUnits (incl. AddWarp's UNIT_TILE units) logical: null | {flags, nLists, indexX: grid, indexY: grid, lists: [coordList]}} tile: [nWidth, nHeight, nPosX, nPosY, unk0x10, dwFlags, pTile, nTileType, unk0x20, unk0x24, nRed, nGreen, nBlue, nIntensity, unk0x2C] // every D2DrlgTileDataStrc field in order ``` - The activation fields up to `units` are captured right after `DRLGROOMTILE_InitRoomGrids`; `seedAfterTiles` is the room seed right after the room's own `DRLGROOMTILE_AddRoomMapTiles`. - `mapTiles` is the final state after every requested room was activated: rooms activated later rewrite tiles and preset units of the near rooms activated before them. - References: a `D2DrlgTileDataStrc*` (`tileRef`, `unk0x20`) is `[levelId, room, "w"|"f"|"r", index]` into that room's `pWallTiles` / `pFloorTiles` / `pRoofTiles` (rooms numbered in `pFirstRoomEx` order, any level of the act); `pTile` is `[DT1 path (lower case, "/"-separated), tile index in that DT1]`; `unk0x10` is an int, or `[levelId, room, "c", index]` when `DRLGLOGIC` stored a `D2RoomCoordListStrc*` in it (index in that room's `pCoordList` chain). `lvlWarpRow` is the LvlWarp.txt row of `pLvlWarpTxtRecord` (-1 for NULL). `coordList` is `[nIndex, bNode, bRoomActive, box0, box1]`; `indexY` cells are 1 + the index of the list in `lists` (0 for NULL). - Shadow tiles live in `pRoofTiles` (`DRLGROOMTILE_CountAllTileTypes` counts them into `nRoofs`). - `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: ```text I