diablo2-web/tools/d2moo-oracle/README.md

138 lines
7.2 KiB
Markdown

# 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
# 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)
```text
{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:
```text
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.