138 lines
7.2 KiB
Markdown
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.
|