- 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 |
||
|---|---|---|
| .. | ||
| include | ||
| src | ||
| .gitignore | ||
| README.md | ||
| build.sh | ||
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
--seedis the D2 game seed (DRLG_AllocDrlgnInitSeed). The whole act, including the level layout ofDRLGOUTPLACE_CreateLevelConnections, derives from it.--levelsmust be Act I outdoor levels (DrlgType 3).--isolatedallocates 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/anddata/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_CreateLevelConnectionsand the town.DRLG_InitLevel, thenDRLGOUTDOORS_GenerateLevelandDRLGOUTWILD_InitAct1OutdoorLevel.- Room creation.
- Room activation, mirroring
DRLGACTIVATE_InitializeRoomEx(DrlgActivate.cpp:317-331) andDRLGACTIVATE_RoomEx_EnsureHasRoom(95-105) up toDRLGROOMTILE_InitRoomGrids:DRLGROOMTILE_LoadDT1FilesForRoomDRLGPRESET_SpawnHardcodedPresetUnits(preset rooms only)sub_6FD77BB0DRLGROOMTILE_InitRoomGrids, which re-seeds the room fromdwInitSeed.
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_GameTileToSubtileCoordsandDUNGEON_GameTileToClientCoords.sub_6FDAB750andsub_6FDAB610(src/path_misc.cpp, copied from D2MOO PathMisc.cpp).- D2CMP tile libraries, parsed from the real DT1 files. This is not optional:
DRLGROOMTILE_GetTileCacherolls 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.
- For Act I every shadow lookup returns rarity 0, so there is no roll. This is visible in the
trace as
Modelling decisions (documented, not guessed):
- Rooms are activated in
pFirstRoomExorder. 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 ofDRLGPRESET_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]}]}
gridis{w, h, cells: [row-major int32]}, ornullwhen the grid was never allocated.vertexis[x, y, direction, flags, next].nextis -1 for null, 0..23 forpVertices[i], and 100+k for the k-th ring vertex.orthis{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:
- Outdoor rooms called
DRLGOUTPLACE_InitOutdoorRoomGridsdirectly. That skippedDRLGROOMTILE_InitRoomGrids' re-seed fromdwInitSeed(D2Common.0x6FD89FA0), so every substitution was rolled from the wrong seed. sub_6FDAB750was stubbed toreturn 0. It drives the dirt-path search insub_6FD80750, so the dirt paths were corrupted.- Missing files returned a zeroed 64-byte buffer. MonPreset, Objects and SuperUniques were dummy
tables.
FOG_DisplayAssertandFOG_DisplayWarningonly printed. - It was a 64-bit build, which needed a hand-patched LvlSub layout.
- D2CMP tile lookups were no-ops, so the shadow rarity rolls could not happen.