feat(drlg): port 1:1 D2MOO outdoor DRLG for Acts 2, 3, 4, and 5 (#496, #497, #498, #499)

This commit is contained in:
troytt 2026-09-28 11:22:08 +00:00
parent 298c6f6b39
commit d887b39485
31 changed files with 5902 additions and 173 deletions

View File

@ -26,6 +26,7 @@ import { DrlgTraceWriter, dumpAct1, type DrlgDump } from '../src/game/drlg/drlg-
import { firstDiff, firstTraceDivergence, formatDiff, fsDrlgSource, ORACLE_DATA, runOracle } from './lib/drlg-oracle.ts'
interface Args {
act: number | undefined
levels: number[]
seeds: number[]
difficulty: number
@ -39,6 +40,14 @@ interface Args {
traceCheck: boolean
}
const ACT_DEFAULT_OUTDOOR_LEVELS: Record<number, number[]> = {
0: [2, 3, 4, 5, 6, 7, 17, 26, 27, 28, 32, 33, 39],
1: [41, 42, 43, 44, 45, 46],
2: [76, 77, 78, 79, 80, 81, 82, 83],
3: [104, 105, 106, 108],
4: [110, 111, 112, 117, 134],
}
function parseLevels(spec: string): number[] {
const out: number[] = []
for (const part of spec.split(',')) {
@ -54,8 +63,9 @@ function parseLevels(spec: string): number[] {
}
function parseArgs(argv: string[]): Args {
const args: Args = { levels: [], seeds: [], difficulty: 0, isolated: false, dataDir: ORACLE_DATA, stopOnFirst: false, report: null, noActivation: false, traceCheck: false }
const args: Args = { act: undefined, levels: [], seeds: [], difficulty: 0, isolated: false, dataDir: ORACLE_DATA, stopOnFirst: false, report: null, noActivation: false, traceCheck: false }
let nRandom = 0
const positionals: string[] = []
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i]!
const next = (): string => {
@ -63,7 +73,8 @@ function parseArgs(argv: string[]): Args {
if (v === undefined) throw new Error(`missing value for ${a}`)
return v
}
if (a === '--levels') args.levels = parseLevels(next())
if (a === '--act') args.act = Number(next())
else if (a === '--levels') args.levels = parseLevels(next())
else if (a === '--seeds') nRandom = Number(next())
else if (a === '--seed') args.seeds.push(...next().split(',').filter(Boolean).map(s => Number(s) >>> 0))
else if (a === '--difficulty') args.difficulty = Number(next())
@ -73,8 +84,22 @@ function parseArgs(argv: string[]): Args {
else if (a === '--report') args.report = next()
else if (a === '--no-activation') args.noActivation = true
else if (a === '--trace-check') args.traceCheck = true
else if (!a.startsWith('-')) positionals.push(a)
else throw new Error(`unknown argument ${a}`)
}
if (positionals.length > 0) {
const posAct = Number(positionals[0])
const actIdx = posAct >= 1 && posAct <= 5 ? posAct - 1 : posAct
if (!Number.isInteger(actIdx) || actIdx < 0 || actIdx > 4) throw new Error(`bad positional act "${positionals[0]}"`)
args.act = actIdx
if (args.levels.length === 0) args.levels = [...ACT_DEFAULT_OUTDOOR_LEVELS[actIdx]!]
if (positionals.length >= 2 && nRandom === 0 && args.seeds.length === 0) {
nRandom = Number(positionals[1])
}
}
if (args.act !== undefined && (!Number.isInteger(args.act) || args.act < 0 || args.act > 4)) {
throw new Error('--act must be 0..4')
}
if (!Number.isInteger(nRandom) || nRandom < 0) throw new Error('--seeds must be a non-negative integer')
if (nRandom > 0) args.seeds.push(...webcrypto.getRandomValues(new Uint32Array(nRandom)))
if (args.seeds.length === 0) throw new Error('give --seeds N or --seed X')
@ -125,6 +150,7 @@ function main(): void {
const runPort = (seed: number, trace: DrlgTraceWriter | undefined): DrlgDump =>
dumpAct1(createDrlgEnv(source, tables), seed, args.levels, {
...(args.act !== undefined ? { act: args.act } : {}),
difficulty: args.difficulty,
isolated: args.isolated,
actOnly: args.levels.length === 0,
@ -134,7 +160,14 @@ function main(): void {
for (const seed of args.seeds) {
const seedHex = `0x${seed.toString(16).padStart(8, '0')}`
const runOpts = { seed, levels: args.levels, difficulty: args.difficulty, isolated: args.isolated, dataDir: args.dataDir }
const runOpts = {
seed,
levels: args.levels,
...(args.act !== undefined ? { act: args.act } : {}),
difficulty: args.difficulty,
isolated: args.isolated,
dataDir: args.dataDir,
}
let result: SeedResult
try {
const oracleRun = runOracle({ ...runOpts, trace: args.traceCheck })

View File

@ -64,9 +64,17 @@ export function drlgActDt1Files(t: DrlgTables, act1Based: number): string[] {
const key = normalizeDrlgPath(path)
if (!out.has(key)) out.set(key, path)
}
const extraLevelTypes = new Set<number>()
if (act1Based === 5) {
for (let id = 109; id < t.levelDefs.length; id += 1) {
const def = t.levelDefs[id]
if (def && def.dwLevelType > 0) extraLevelTypes.add(def.dwLevelType)
}
}
let rows = 0
for (const rec of t.lvlTypes) {
if (rec.nAct !== act1Based) continue
for (let typeId = 0; typeId < t.lvlTypes.length; typeId += 1) {
const rec = t.lvlTypes[typeId]!
if (rec.nAct !== act1Based && !extraLevelTypes.has(typeId)) continue
rows += 1
// DATATBLS_LoadLevelTypesTxt leaves names of one character or less ("0", "") unprefixed: they are
// unused slots that DRLGROOMTILE_LoadDT1FilesForRoom never opens.

View File

@ -39,6 +39,7 @@ export interface OracleRunOptions {
readonly seed: number
/** Empty: act stage only. */
readonly levels: readonly number[]
readonly act?: number
readonly difficulty?: number
readonly isolated?: boolean
readonly trace?: boolean
@ -58,6 +59,7 @@ export function runOracle(opts: OracleRunOptions): OracleRunResult {
const tracePath = join(dir, 'trace.txt')
const args = ['--data', opts.dataDir ?? ORACLE_DATA, '--seed', `0x${(opts.seed >>> 0).toString(16)}`, '--out', out]
if (opts.levels.length > 0) args.push('--levels', opts.levels.join(','))
if (opts.act !== undefined) args.push('--act', String(opts.act))
if (opts.difficulty !== undefined) args.push('--difficulty', String(opts.difficulty))
if (opts.isolated) args.push('--isolated')
if (opts.trace) args.push('--trace', tracePath)

File diff suppressed because it is too large Load Diff

211
src/game/drlg/drlg-anim.ts Normal file
View File

@ -0,0 +1,211 @@
/**
* D2Common DRLG animated tiles: 1:1 port of D2MOO source/D2Common/src/Drlg/DrlgDrlgAnim.cpp
* (commit 5596f5c, MIT License, Copyright (c) 2020-2025 The Phrozen Keep community).
*/
import {
D2CMP_10079_GetTileFlags,
D2CMP_10081_GetTileRarity,
D2CMP_10088_GetTiles,
type D2TileLibraryEntryStrc,
} from './drlg-d2cmp.ts'
import { DRLGGRID_GetGridEntry } from './drlg-grid.ts'
import {
DRLGROOMTILE_InitFloorTileData,
DRLGROOMTILE_InitShadowTileData,
DRLGROOMTILE_InitWallTileData,
GetMapTileLayer,
MAPTILE_HIDDEN,
PackedTile,
TILETYPE_FLOOR,
TILETYPE_SHADOW,
tileGridOf,
} from './drlg-roomtile.ts'
import {
DRLGROOMFLAG_ANIMATED_FLOOR,
type D2DrlgAnimTileGridStrc,
type D2DrlgGridStrc,
type D2DrlgRoomStrc,
type D2DrlgTileDataStrc,
} from './drlg-types.ts'
// D2TileMaterialFlags (D2CMP.h)
const TILE_FLAGS_LAVA = 0x100
//D2Common.0x6FD75560
export function DRLGANIM_TestLoadAnimatedRoomTiles(
pDrlgRoom: D2DrlgRoomStrc,
pDrlgGrid: D2DrlgGridStrc,
pTileTypeGrid: D2DrlgGridStrc | null,
nTileType: number,
bKillEdgeX: boolean,
bKillEdgeY: boolean,
): void {
const ppTileLibraryEntry: D2TileLibraryEntryStrc[] = []
const pTiles = tileGridOf(pDrlgRoom).pTiles
const nMaxY = (pDrlgRoom.pDrlgCoord.nHeight + (bKillEdgeY ? 0 : 1)) | 0
const nMaxX = (pDrlgRoom.pDrlgCoord.nWidth + (bKillEdgeX ? 0 : 1)) | 0
for (let nY = 0; nY < nMaxY; nY += 1) {
for (let nX = 0; nX < nMaxX; nX += 1) {
const nGridEntry = DRLGGRID_GetGridEntry(pDrlgGrid, nX, nY) >>> 0
if (PackedTile.bShadow(nGridEntry) || PackedTile.bIsWall(nGridEntry) || PackedTile.bIsFloor(nGridEntry)) {
if (pTileTypeGrid) {
nTileType = DRLGGRID_GetGridEntry(pTileTypeGrid, nX, nY)
}
const nStyle = PackedTile.nTileStyle(nGridEntry)
const nSequence = PackedTile.nTileSequence(nGridEntry)
const nTilesCount = D2CMP_10088_GetTiles(pDrlgRoom.pTiles, nTileType, nStyle, nSequence, ppTileLibraryEntry, 40)
if (nTilesCount && D2CMP_10079_GetTileFlags(ppTileLibraryEntry[0]!) & TILE_FLAGS_LAVA) {
if (PackedTile.bIsFloor(nGridEntry)) {
pTiles.nFloors = (pTiles.nFloors + nTilesCount - 1) | 0
} else if (PackedTile.bIsWall(nGridEntry)) {
pTiles.nWalls = (pTiles.nWalls + nTilesCount - 1) | 0
} else {
pTiles.nRoofs = (pTiles.nRoofs + nTilesCount - 1) | 0
}
pDrlgRoom.dwFlags = (pDrlgRoom.dwFlags | DRLGROOMFLAG_ANIMATED_FLOOR) >>> 0
}
}
}
}
}
// Helper function
function DRLGANIM_FindAnimatedTileFrame(
pTileLibraryEntries: D2TileLibraryEntryStrc[],
nbTileEntries: number,
nRarity: number,
nStyle: number,
nSeq: number,
): D2TileLibraryEntryStrc {
for (let nEntryIndex = 0; nEntryIndex < nbTileEntries; nEntryIndex += 1) {
if (D2CMP_10081_GetTileRarity(pTileLibraryEntries[nEntryIndex]!) === nRarity) {
return pTileLibraryEntries[nEntryIndex]!
}
}
throw new Error(`Animating tiles missing Rarity ${nRarity} Tile - Style ${nStyle} Seq ${nSeq}`)
}
//D2Common.0x6FD75740
export function DRLGANIM_AllocAnimationTileGrids(
pDrlgRoom: D2DrlgRoomStrc,
nAnimationSpeed: number,
pWallGrid: D2DrlgGridStrc[],
nWalls: number,
pFloorGrid: D2DrlgGridStrc[],
nFloors: number,
pShadowGrid: D2DrlgGridStrc,
): void {
const pTileGrid = tileGridOf(pDrlgRoom)
DRLGANIM_AllocAnimationTileGrid(
pDrlgRoom,
nAnimationSpeed,
pTileGrid.pTiles.pWallTiles,
pTileGrid.nWalls,
pWallGrid,
nWalls,
)
DRLGANIM_AllocAnimationTileGrid(
pDrlgRoom,
nAnimationSpeed,
pTileGrid.pTiles.pFloorTiles,
pTileGrid.nFloors,
pFloorGrid,
nFloors,
)
DRLGANIM_AllocAnimationTileGrid(
pDrlgRoom,
nAnimationSpeed,
pTileGrid.pTiles.pRoofTiles,
pTileGrid.nShadows,
[pShadowGrid],
1,
)
}
//D2Common.0x6FD757B0
export function DRLGANIM_AllocAnimationTileGrid(
pDrlgRoom: D2DrlgRoomStrc,
nAnimationSpeed: number,
pTiles: D2DrlgTileDataStrc[] | null,
nTiles: number,
pDrlgGrid: D2DrlgGridStrc[],
_nUnused: number,
): void {
if (!pTiles) {
return
}
if (nAnimationSpeed === 0) {
nAnimationSpeed = 80
}
const pTileGrid = tileGridOf(pDrlgRoom)
const pTileLibraryEntries: D2TileLibraryEntryStrc[] = []
for (let i = 0; i < nTiles; i += 1) {
const pCurrentTileData = pTiles[i]!
if (pCurrentTileData.pTile && D2CMP_10079_GetTileFlags(pCurrentTileData.pTile) & TILE_FLAGS_LAVA) {
const nGridIdx = GetMapTileLayer(pCurrentTileData.dwFlags)
const pLayerGrid = pDrlgGrid[nGridIdx < 0 ? 0 : nGridIdx]!
const nGridEntry = DRLGGRID_GetGridEntry(pLayerGrid, pCurrentTileData.nPosX, pCurrentTileData.nPosY) >>> 0
const nStyle = PackedTile.nTileStyle(nGridEntry)
const nSeq = PackedTile.nTileSequence(nGridEntry)
const nFrames = D2CMP_10088_GetTiles(
pDrlgRoom.pTiles,
pCurrentTileData.nTileType,
nStyle,
nSeq,
pTileLibraryEntries,
40,
)
const ppMapTileData = new Array<D2DrlgTileDataStrc | null>(nFrames).fill(null)
const pDrlgAnimTileGrid: D2DrlgAnimTileGridStrc = {
ppMapTileData,
nFrames,
nCurrentFrame: 0,
nAnimationSpeed,
pNext: pTileGrid.pAnimTiles,
}
pTileGrid.pAnimTiles = pDrlgAnimTileGrid
// Animated tiles use rarity as frame index
pCurrentTileData.pTile = DRLGANIM_FindAnimatedTileFrame(pTileLibraryEntries, nFrames, 0, nStyle, nSeq)
pDrlgAnimTileGrid.ppMapTileData[0] = pCurrentTileData
const nX = (pCurrentTileData.nPosX + pDrlgRoom.pDrlgCoord.nPosX) | 0
const nY = (pCurrentTileData.nPosY + pDrlgRoom.pDrlgCoord.nPosY) | 0
for (let nRarity = 1; nRarity < nFrames; nRarity += 1) {
const pTileEntry = DRLGANIM_FindAnimatedTileFrame(pTileLibraryEntries, nFrames, nRarity, nStyle, nSeq)
let pAddedTile: D2DrlgTileDataStrc
if (pCurrentTileData.nTileType === TILETYPE_FLOOR) {
pAddedTile = DRLGROOMTILE_InitFloorTileData(pDrlgRoom, null, nX, nY, nGridEntry, pTileEntry)
} else if (pCurrentTileData.nTileType === TILETYPE_SHADOW) {
pAddedTile = DRLGROOMTILE_InitShadowTileData(pDrlgRoom, null, nX, nY, nGridEntry, pTileEntry)
} else {
pAddedTile = DRLGROOMTILE_InitWallTileData(
pDrlgRoom,
null,
nX,
nY,
nGridEntry,
pTileEntry,
pCurrentTileData.nTileType,
)
}
pDrlgAnimTileGrid.ppMapTileData[nRarity] = pAddedTile
// We start by displaying the first frame, hide all other frames tiles.
pAddedTile.dwFlags = (pAddedTile.dwFlags | MAPTILE_HIDDEN) >>> 0
}
}
}
}

View File

@ -83,18 +83,13 @@ function loadTileLibrary(env: DrlgEnv, szFileName: string): D2TileLibraryStrc {
return lib
}
//D2Cmp.#10087
//D2Cmp.#10087 (1.13c D2CMP.dll 0x6fe25480)
export function D2CMP_10087_LoadTileLibrarySlot(
env: DrlgEnv,
ppTileLibraryHash: D2TileLibraryStrc[],
szFileName: string,
): void {
const pLibrary = loadTileLibrary(env, szFileName)
for (let i = 0; i < ppTileLibraryHash.length; i += 1) {
if (ppTileLibraryHash[i] === pLibrary) {
throw new Error(`tile library ${pLibrary.szPath} loaded twice into the same slot array`)
}
}
if (ppTileLibraryHash.length >= TILE_SLOTS) {
throw new Error(`no free tile library slot for ${pLibrary.szPath}`)
}

View File

@ -29,6 +29,7 @@ import {
LEVEL_LUTGHOLEIN,
LEVEL_NONE,
LEVEL_ROGUEENCAMPMENT,
LEVEL_TALRASHASTOMB1,
LEVEL_THEPANDEMONIUMFORTRESS,
newCoord,
type D2DrlgCoordStrc,
@ -81,9 +82,23 @@ export function DRLG_AllocDrlg(
switch (nActNo) {
case 0: // ACT_I: D2CMP_10087_LoadTileLibrarySlot(pDrlg->pTiles, "DATA\\GLOBAL\\Tiles\\Act1\\Town\\Floor.dt1")
break
case 1: {
// ACT_II
let nStaffLevelOffset = 0
let nBossLevelOffset = 0
do {
nStaffLevelOffset = (SEED_RollRandomNumber(pDrlg.pSeed) >>> 0) % 7
nBossLevelOffset = (SEED_RollRandomNumber(pDrlg.pSeed) >>> 0) % 7
} while (nStaffLevelOffset === nBossLevelOffset)
pDrlg.nStaffTombLevel = LEVEL_TALRASHASTOMB1 + nStaffLevelOffset
pDrlg.nBossTombLevel = LEVEL_TALRASHASTOMB1 + nBossLevelOffset
break
}
case 2: // ACT_III
pDrlg.bJungleInterlink = SEED_RollRandomNumber(pDrlg.pSeed) & 1
break
default:
// Acts II/III roll pDrlg->pSeed here (staff/boss tombs, jungle interlink); not ported yet.
throw new Error(`DRLG_AllocDrlg: act ${nActNo} is not ported`)
break
}
// DRLGACTIVATE_InitializeRoomExStatusLists(pDrlg): room status lists are not modelled.
@ -119,6 +134,8 @@ export function DRLG_AllocLevel(pDrlg: D2DrlgStrc, nLevelId: number): D2DrlgLeve
nRoom_Center_Warp_X: [0, 0, 0, 0, 0, 0, 0, 0, 0],
nRoom_Center_Warp_Y: [0, 0, 0, 0, 0, 0, 0, 0, 0],
nRoomCoords: 0,
pJungleDefs: null,
nJungleDefs: 0,
pBuild: null,
pPresetMaps: null,
pNextLevel: null,

View File

@ -19,7 +19,11 @@ import {
DRLGTYPE_MAZE,
DRLGTYPE_OUTDOOR,
DRLGTYPE_PRESET,
LEVEL_HARROGATH,
LEVEL_KURASTDOCKTOWN,
LEVEL_LUTGHOLEIN,
LEVEL_ROGUEENCAMPMENT,
LEVEL_THEPANDEMONIUMFORTRESS,
outdoorCoord,
type D2DrlgCoordStrc,
type D2DrlgGridStrc,
@ -574,7 +578,6 @@ function dumpMapTiles(index: DumpRefIndex, pRoom: D2DrlgRoomStrc, nLevelId: numb
const where = `level ${nLevelId} room ${nRoom}`
const pTileGrid = pRoom.pTileGrid
if (!pTileGrid) throw new Error(`${where}: no pTileGrid after DRLGROOMTILE_AddRoomMapTiles`)
if (pTileGrid.pAnimTiles) throw new Error(`${where}: animated tiles (pAnimTiles) are not dumped`)
try {
const t = pTileGrid.pTiles
const links: DumpTileLink[] = []
@ -603,6 +606,7 @@ function dumpMapTiles(index: DumpRefIndex, pRoom: D2DrlgRoomStrc, nLevelId: numb
// ---- driver (TypeScript twin of tools/d2moo-oracle/src/main.cpp) ----
export interface Act1RunOptions {
readonly act?: number
readonly difficulty?: number
/** A fresh act per requested level (proves a level does not depend on the others). */
readonly isolated?: boolean
@ -635,19 +639,27 @@ export interface Act1Run {
readonly levels: readonly D2DrlgLevelStrc[]
}
function allocAct1(env: DrlgEnv, nSeed: number, nDifficulty: number, trace: DrlgTraceWriter | undefined): D2DrlgStrc {
const TOWN_LEVEL_IDS = [
LEVEL_ROGUEENCAMPMENT,
LEVEL_LUTGHOLEIN,
LEVEL_KURASTDOCKTOWN,
LEVEL_THEPANDEMONIUMFORTRESS,
LEVEL_HARROGATH,
] as const
function allocActByNo(env: DrlgEnv, nAct: number, nSeed: number, nDifficulty: number, trace: DrlgTraceWriter | undefined): D2DrlgStrc {
trace?.context('drlg')
// D2Game allocates the act with the town level id; DRLG_AllocDrlg lays out the whole act
// (DRLGOUTPLACE_CreateLevelConnections) and generates the town.
return DRLG_AllocDrlg(env, ACT_I, nSeed, LEVEL_ROGUEENCAMPMENT, 0, nDifficulty)
return DRLG_AllocDrlg(env, nAct, nSeed, TOWN_LEVEL_IDS[nAct]!, 0, nDifficulty)
}
/** Generation stage of one level: DRLG_InitLevel, everything that exists before any room is activated. */
function initLevel(pDrlg: D2DrlgStrc, nLevelId: number, trace: DrlgTraceWriter | undefined): D2DrlgLevelStrc {
function initLevel(pDrlg: D2DrlgStrc, nLevelId: number, nAct: number, trace: DrlgTraceWriter | undefined): D2DrlgLevelStrc {
trace?.context(`L${nLevelId}`)
const pLevel = DRLG_GetLevel(pDrlg, nLevelId)
if (pLevel.nDrlgType !== DRLGTYPE_OUTDOOR || DRLG_GetActNoFromLevelId(nLevelId) !== ACT_I) {
throw new Error(`level ${nLevelId} is not an Act I outdoor level`)
if (pLevel.nDrlgType !== DRLGTYPE_OUTDOOR || DRLG_GetActNoFromLevelId(nLevelId) !== nAct) {
throw new Error(`level ${nLevelId} is not an Act ${nAct + 1} outdoor level`)
}
if (pLevel.pFirstRoomEx) throw new Error(`level ${nLevelId} was already generated before its own generation stage`)
DRLG_InitLevel(pLevel)
@ -684,7 +696,7 @@ function activateLevel(pLevel: D2DrlgLevelStrc, trace: DrlgTraceWriter | undefin
}
/**
* Runs the Act I generation exactly like the oracle driver: DRLG_AllocDrlg (layout, links, town), then
* Runs the act generation exactly like the oracle driver: DRLG_AllocDrlg (layout, links, town), then
* DRLG_InitLevel of every requested level, then the activation of every room of those levels (see
* activateLevel). A non-isolated run generates every requested level before activating any room, because
* activating a room initialises the levels behind its warps (sub_6FD77BB0 -> DRLG_InitLevel); an isolated
@ -698,18 +710,30 @@ export function runAct1(
hooks: Act1RunHooks = {},
): Act1Run {
const nDifficulty = opts.difficulty ?? 0
let nAct: number = ACT_I
if (opts.act !== undefined) {
if (opts.act < 0 || opts.act > 4) throw new Error('act must be 0..4')
nAct = opts.act
} else if (levels.length > 0) {
nAct = DRLG_GetActNoFromLevelId(levels[0]!)
}
for (const id of levels) {
if (DRLG_GetActNoFromLevelId(id) !== nAct) {
throw new Error(`level ${id} belongs to act ${DRLG_GetActNoFromLevelId(id)}, not act ${nAct}`)
}
}
const trace = opts.trace
if (trace) setD2SeedTraceSink(trace)
try {
let nActs = 0
const allocAct = (): D2DrlgStrc => {
const pDrlg = allocAct1(env, nSeed >>> 0, nDifficulty, trace)
const pDrlg = allocActByNo(env, nAct, nSeed >>> 0, nDifficulty, trace)
hooks.onDrlgAllocated?.(pDrlg, nActs)
nActs += 1
return pDrlg
}
const generate = (pDrlg: D2DrlgStrc, nLevelId: number): D2DrlgLevelStrc => {
const pLevel = initLevel(pDrlg, nLevelId, trace)
const pLevel = initLevel(pDrlg, nLevelId, nAct, trace)
hooks.onLevelGenerated?.(pLevel)
return pLevel
}
@ -733,6 +757,8 @@ export function runAct1(
}
}
export const runAct = runAct1
export interface DumpAct1Options extends Act1RunOptions {
/** Stop after the act stage (the `levels` array stays empty). */
readonly actOnly?: boolean
@ -806,8 +832,12 @@ export function dumpAct1(env: DrlgEnv, nSeed: number, levels: readonly number[],
levelGrids.set(pLevel.nLevelId, dumpLevelGrid(pLevel))
})
let run: Act1Run
const runOpts: DumpAct1Options =
opts.act === undefined && levels.length > 0
? { ...opts, act: DRLG_GetActNoFromLevelId(levels[0]!) }
: opts
try {
run = runAct1(env, nSeed, opts.actOnly ? [] : levels, opts, {
run = runAct1(env, nSeed, opts.actOnly ? [] : levels, runOpts, {
onDrlgAllocated(pDrlg, index) {
if (index !== 0) return
drlg = { startSeed: u32(pDrlg.dwStartSeed), seed: dumpSeed(pDrlg.pSeed) }
@ -815,7 +845,7 @@ export function dumpAct1(env: DrlgEnv, nSeed: number, levels: readonly number[],
},
onLevelGenerated(pLevel) {
const levelGrid = levelGrids.get(pLevel.nLevelId)
if (!levelGrid) throw new Error(`level ${pLevel.nLevelId}: InitAct1OutdoorLevel hook did not fire`)
if (!levelGrid) throw new Error(`level ${pLevel.nLevelId}: outdoor levelGrid hook did not fire`)
heads.set(pLevel, dumpLevelHead(pLevel, levelGrid))
activations.set(pLevel, [])
},

View File

@ -203,6 +203,8 @@ const stru_6FDCE5C8: readonly { nX: number; nY: number }[] = [
{ nX: 0, nY: -1 },
]
const dword_6FDCE5EC_NEG: readonly number[] = [1, -1, 0, 0, -1]
const dword_6FDCE5EC: readonly number[] = [
23, 0, 5, 21, 17, 15, 3, 0, 9, 7, 39, 0, 0, 5, 3, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0,
]
@ -239,16 +241,16 @@ function DRLGLOGIC_SetTileGridFlags(a1: D2UnkDrlgLogicStrc, nX: number, nY: numb
nIndex = DRLGGRID_GetGridEntry(a1.pTileTypeGrid, nPosX, nPosY)
}
// dword_6FDCE650 has 20 entries and dword_6FDCE5EC 26: any other subscript reads unrelated memory (UB;
// tile types 0 and 13 map to row -1, which lands before dword_6FDCE5EC).
// dword_6FDCE650 maps tile types 0 and 13 to row -1 (nFlagIndex = a4 - 4 in -5..-1), which in
// DrlgDrlgLogic.cpp reads the trailing 5 ints of stru_6FDCE5C8 immediately preceding dword_6FDCE5EC.
const nRow = nIndex >= 0 && nIndex < dword_6FDCE650.length ? dword_6FDCE650[nIndex]! : undefined
const nFlagIndex = nRow === undefined ? -1 : (a4 + 5 * nRow + 1) | 0
if (nRow === undefined || nFlagIndex < 0 || nFlagIndex >= dword_6FDCE5EC.length) {
const nFlagIndex = nRow === undefined ? -99 : (a4 + 5 * nRow + 1) | 0
if (nRow === undefined || nFlagIndex < -5 || nFlagIndex >= dword_6FDCE5EC.length) {
throw new RangeError(
`DRLGLOGIC_SetTileGridFlags: out-of-bounds table read (tile type ${nIndex}, a4 ${a4}) at room tile (${nPosX}, ${nPosY})`,
)
}
const nFlags = dword_6FDCE5EC[nFlagIndex]!
const nFlags = nFlagIndex < 0 ? dword_6FDCE5EC_NEG[nFlagIndex + 5]! : dword_6FDCE5EC[nFlagIndex]!
if (nFlags & 1) {
DRLGGRID_AlterGridFlag(a1.field_4, nPosX, nPosY, a1.nFlags, FLAG_OPERATION_OR)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,413 @@
/**
* D2Common Act II outdoor desert DRLG: 1:1 port of D2MOO
* `source/D2Common/src/Drlg/DrlgOutDesr.cpp`.
*/
import { SEED_RollLimitedRandomNumber, SEED_RollRandomNumber } from '../d2-rng.ts'
import {
LVLPREST_ACT2_DESERT_BORDER_1,
LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS,
LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_ENDS,
LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB,
LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH,
LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL,
LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS,
LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_ENDS,
LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB,
LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH,
LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL,
LVLPREST_ACT2_DESERT_CLIFF_TOP,
LVLPREST_ACT2_DESERT_CLIFF_TOP_KING_TOMB,
LVLPREST_ACT2_DESERT_FILL_BERMS_1,
LVLPREST_ACT2_DESERT_FILL_BERMS_2,
LVLPREST_ACT2_DESERT_FILL_BERMS_3,
LVLPREST_ACT2_DESERT_FILL_BERMS_4,
LVLPREST_ACT2_DESERT_FILL_BONE_1,
LVLPREST_ACT2_DESERT_FILL_BONE_2,
LVLPREST_ACT2_DESERT_FILL_HEAD_1,
LVLPREST_ACT2_DESERT_FILL_HEAD_2,
LVLPREST_ACT2_DESERT_FILL_MESA_1,
LVLPREST_ACT2_DESERT_FILL_WAGON_1,
LVLPREST_ACT2_DESERT_LAIR_1,
LVLPREST_ACT2_DESERT_OASIS_1,
LVLPREST_ACT2_DESERT_OASIS_2,
LVLPREST_ACT2_DESERT_OASIS_3,
LVLPREST_ACT2_DESERT_RUINS_08X08,
LVLPREST_ACT2_DESERT_RUINS_08X16,
LVLPREST_ACT2_DESERT_RUINS_16X08,
LVLPREST_ACT2_DESERT_RUINS_16X16,
LVLPREST_ACT2_DESERT_RUINS_ELDER,
LVLPREST_ACT2_DESERT_RUINS_SEWER,
LVLPREST_ACT2_DESERT_TOMB_1,
LVLPREST_ACT2_DESERT_TOMB_2,
LVLPREST_ACT2_DESERT_TRANSITION_N,
LVLPREST_ACT2_DESERT_TRANSITION_W,
LVLPREST_ACT2_DESERT_VALLEY_RUIN_1,
LVLPREST_ACT2_DESERT_VALLEY_RUIN_2,
LVLPREST_ACT2_DESERT_VALLEY_WARP,
} from './drlg-ids.ts'
import {
DRLGOUTDOORS_AddAct124SecondaryBorder,
DRLGOUTDOORS_SpawnAct12Shrines,
DRLGOUTDOORS_SpawnAct12Waypoint,
DRLGOUTDOORS_SpawnOutdoorLevelPreset,
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx,
} from './drlg-outdoors.ts'
import { DRLGOUTPLACE_PlaceAct1245OutdoorBorders, DRLGOUTPLACE_SetOutGridLinkFlags } from './drlg-outplace.ts'
import { DATATBLS_GetLvlPrestTxtRecord } from './drlg-tables.ts'
import {
type D2DrlgLevelStrc,
LEVEL_CANYONOFTHEMAGI,
LEVEL_DRYHILLS,
LEVEL_FAROASIS,
LEVEL_LOSTCITY,
LEVEL_LUTGHOLEIN,
LEVEL_PANDEMONIUMRUN2,
LEVEL_ROCKYWASTE,
LEVEL_VALLEYOFSNAKES,
} from './drlg-types.ts'
interface D2DrlgOutDesertInitStrc {
readonly nLvlPrestId: number
readonly nRand: number
readonly nX: number
readonly nY: number
}
// Inlined in D2Common.0x6FD7D430
export function DRLGOUTDESR_PlaceDesertTransitionToTown(pLevel: D2DrlgLevelStrc): void {
let pTownRoomData = pLevel.pOutdoors!.pRoomData
while (pTownRoomData && pTownRoomData.pLevel!.nLevelId !== LEVEL_LUTGHOLEIN) {
pTownRoomData = pTownRoomData.pNext
}
if (pTownRoomData) {
if (pTownRoomData.nDirection === 3) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
0,
pLevel.pOutdoors!.nGridHeight - 1,
LVLPREST_ACT2_DESERT_TRANSITION_N,
-1,
false,
)
} else {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
pLevel.pOutdoors!.nGridWidth - 1,
0,
LVLPREST_ACT2_DESERT_TRANSITION_W,
-1,
false,
)
}
}
}
// 1.10f: D2Common.0x6FD7D430
// 1.11 : D2Common.0x6FDD58A0
export function DRLGOUTDESR_InitAct2OutdoorLevel(pLevel: D2DrlgLevelStrc): void {
const nLevelPrestIds1: readonly number[] = [
LVLPREST_ACT2_DESERT_OASIS_1,
LVLPREST_ACT2_DESERT_RUINS_08X08,
LVLPREST_ACT2_DESERT_FILL_BONE_1,
LVLPREST_ACT2_DESERT_FILL_BONE_2,
LVLPREST_ACT2_DESERT_FILL_HEAD_1,
LVLPREST_ACT2_DESERT_FILL_MESA_1,
LVLPREST_ACT2_DESERT_FILL_WAGON_1,
]
const nLevelPrestIds2: readonly number[] = [
LVLPREST_ACT2_DESERT_OASIS_1,
LVLPREST_ACT2_DESERT_RUINS_08X08,
LVLPREST_ACT2_DESERT_FILL_HEAD_2,
LVLPREST_ACT2_DESERT_FILL_MESA_1,
]
const nLevelPrestIds3: readonly number[] = [
LVLPREST_ACT2_DESERT_FILL_BERMS_1,
LVLPREST_ACT2_DESERT_FILL_BERMS_2,
LVLPREST_ACT2_DESERT_FILL_BERMS_3,
LVLPREST_ACT2_DESERT_FILL_BERMS_4,
]
const nLevelPrestIds4: readonly number[] = [LVLPREST_ACT2_DESERT_OASIS_2, LVLPREST_ACT2_DESERT_OASIS_3]
DRLGOUTPLACE_SetOutGridLinkFlags(pLevel)
DRLGOUTPLACE_PlaceAct1245OutdoorBorders(pLevel)
switch (pLevel.nLevelId) {
case LEVEL_ROCKYWASTE:
DRLGOUTDESR_PlaceDesertTransitionToTown(pLevel)
DRLGOUTDESR_PlaceBorders(pLevel)
DRLGOUTDESR_AddExits(pLevel)
DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false)
break
case LEVEL_DRYHILLS:
DRLGOUTDESR_PlaceCliffs(pLevel)
DRLGOUTDESR_PlaceBorders(pLevel)
DRLGOUTDESR_AddExits(pLevel)
DRLGOUTDOORS_SpawnAct12Waypoint(pLevel)
DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, false)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds3, nLevelPrestIds3.length, true)
break
case LEVEL_FAROASIS:
DRLGOUTDESR_PlaceCliffs(pLevel)
DRLGOUTDESR_PlaceBorders(pLevel)
DRLGOUTDESR_AddExits(pLevel)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds4, nLevelPrestIds4.length, false)
DRLGOUTDOORS_SpawnAct12Waypoint(pLevel)
DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5)
DRLGOUTDESR_PlaceFillsInFarOasis(pLevel)
break
case LEVEL_LOSTCITY:
DRLGOUTDESR_PlaceCliffs(pLevel)
DRLGOUTDESR_PlaceBorders(pLevel)
DRLGOUTDESR_AddExits(pLevel)
DRLGOUTDESR_PlaceRuinsInLostCity(pLevel)
DRLGOUTDOORS_SpawnAct12Waypoint(pLevel)
DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5)
DRLGOUTDESR_PlaceFillsInLostCity(pLevel)
break
case LEVEL_VALLEYOFSNAKES:
DRLGOUTDESR_AddExits(pLevel)
break
case LEVEL_CANYONOFTHEMAGI:
DRLGOUTDESR_PlaceTombEntriesInCanyon(pLevel)
DRLGOUTDESR_PlaceBorders(pLevel)
DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5)
DRLGOUTDESR_PlaceFillsInCanyon(pLevel)
break
case LEVEL_PANDEMONIUMRUN2:
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 4, 4, LVLPREST_ACT2_DESERT_VALLEY_WARP, -1, false)
DRLGOUTDESR_PlaceBorders(pLevel)
DRLGOUTDESR_PlaceFillsInCanyon(pLevel)
DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5)
break
default:
return
}
}
// D2Common.0x6FD7D870
export function DRLGOUTDESR_PlacePresetVariants(
pLevel: D2DrlgLevelStrc,
pLevelPrestIds: readonly number[],
nVariants: number,
bIterateFiles: boolean,
): void {
let nRand = SEED_RollLimitedRandomNumber(pLevel.pSeed, nVariants) >>> 0
for (let i = 0; i < nVariants; i += 1) {
const nPrestId = pLevelPrestIds[nRand]!
if (bIterateFiles) {
const dwFiles = DATATBLS_GetLvlPrestTxtRecord(pLevel.pDrlg.env.tables, nPrestId).dwFiles >>> 0
for (let nFile = 0; nFile < dwFiles; nFile += 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nPrestId, nFile, 0, 15)
}
} else {
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nPrestId, -1, 0, 15)
}
nRand = (nRand + 1) % nVariants
}
}
const pOutDesertCliffsInit: readonly (readonly D2DrlgOutDesertInitStrc[])[] = [
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 1, nX: 0, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 2, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 6, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 },
],
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 1, nX: 0, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 2, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 6, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 },
],
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 1, nX: 0, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 2, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 6, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 },
],
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 6, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_TOP, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 6 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 },
],
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 6, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_TOP, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 6 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 },
],
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 1, nX: 4, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 2 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 6 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 },
],
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 1, nX: 4, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 2 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 6 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 },
],
[
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 1, nX: 4, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 2 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 6 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 },
],
]
// D2Common.0x6FD7D950
export function DRLGOUTDESR_PlaceCliffs(pLevel: D2DrlgLevelStrc): void {
const nRand = SEED_RollRandomNumber(pLevel.pSeed) & 7
const row = pOutDesertCliffsInit[nRand]!
for (let i = 0; i < 5; i += 1) {
const entry = row[i]!
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, entry.nX, entry.nY, entry.nLvlPrestId, entry.nRand, false)
}
}
// D2Common.0x6FD7D9B0
export function DRLGOUTDESR_PlaceBorders(pLevel: D2DrlgLevelStrc): void {
DRLGOUTDOORS_AddAct124SecondaryBorder(pLevel, 2, LVLPREST_ACT2_DESERT_BORDER_1)
DRLGOUTDOORS_AddAct124SecondaryBorder(pLevel, 1, LVLPREST_ACT2_DESERT_BORDER_1)
DRLGOUTDOORS_AddAct124SecondaryBorder(pLevel, 3, LVLPREST_ACT2_DESERT_BORDER_1)
}
// D2Common.0x6FD7D9F0
export function DRLGOUTDESR_AddExits(pLevel: D2DrlgLevelStrc): void {
let nLevelPrestId = 0
switch (pLevel.nLevelId) {
case LEVEL_ROCKYWASTE:
case LEVEL_DRYHILLS:
nLevelPrestId = LVLPREST_ACT2_DESERT_TOMB_1
break
case LEVEL_FAROASIS:
nLevelPrestId = LVLPREST_ACT2_DESERT_LAIR_1
break
case LEVEL_LOSTCITY:
nLevelPrestId = LVLPREST_ACT2_DESERT_RUINS_SEWER
break
case LEVEL_VALLEYOFSNAKES:
nLevelPrestId = LVLPREST_ACT2_DESERT_TOMB_2
break
default:
throw new Error('sAddTheExitsAndStuff() - Could not add the exit!')
}
if (!DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nLevelPrestId, -1, 0, 15)) {
throw new Error('sAddTheExitsAndStuff() - Could not add the exit!')
}
}
// D2Common.0x6FD7DA60
export function DRLGOUTDESR_PlaceFillsInFarOasis(pLevel: D2DrlgLevelStrc): void {
const nLevelPrestIds1: readonly number[] = [
LVLPREST_ACT2_DESERT_RUINS_08X08,
LVLPREST_ACT2_DESERT_FILL_HEAD_1,
LVLPREST_ACT2_DESERT_FILL_MESA_1,
LVLPREST_ACT2_DESERT_FILL_WAGON_1,
]
const nLevelPrestIds2: readonly number[] = [LVLPREST_ACT2_DESERT_OASIS_1]
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true)
}
// D2Common.0x6FD7DAC0
export function DRLGOUTDESR_PlaceRuinsInLostCity(pLevel: D2DrlgLevelStrc): void {
const nLevelPrestIds: readonly number[] = [
LVLPREST_ACT2_DESERT_RUINS_ELDER,
LVLPREST_ACT2_DESERT_RUINS_16X16,
LVLPREST_ACT2_DESERT_RUINS_16X08,
LVLPREST_ACT2_DESERT_RUINS_08X16,
]
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds, nLevelPrestIds.length, false)
}
// D2Common.0x6FD7DB00
export function DRLGOUTDESR_PlaceFillsInLostCity(pLevel: D2DrlgLevelStrc): void {
const nLevelPrestIds1: readonly number[] = [
LVLPREST_ACT2_DESERT_OASIS_1,
LVLPREST_ACT2_DESERT_FILL_HEAD_2,
LVLPREST_ACT2_DESERT_FILL_MESA_1,
LVLPREST_ACT2_DESERT_FILL_BERMS_1,
LVLPREST_ACT2_DESERT_FILL_BERMS_2,
]
const nLevelPrestIds2: readonly number[] = [LVLPREST_ACT2_DESERT_RUINS_08X08]
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true)
}
const pOutDesertCanyonInit: readonly D2DrlgOutDesertInitStrc[] = [
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_ENDS, nRand: 0, nX: 8, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB, nRand: 2, nX: 6, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB, nRand: 1, nX: 4, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB, nRand: 0, nX: 2, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_TOP_KING_TOMB, nRand: 0, nX: 0, nY: 0 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB, nRand: 0, nX: 0, nY: 2 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB, nRand: 1, nX: 0, nY: 4 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB, nRand: 2, nX: 0, nY: 6 },
{ nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_ENDS, nRand: 0, nX: 0, nY: 8 },
]
// D2Common.0x6FD7DB70
export function DRLGOUTDESR_PlaceTombEntriesInCanyon(pLevel: D2DrlgLevelStrc): void {
for (let i = 0; i < pOutDesertCanyonInit.length; i += 1) {
const entry = pOutDesertCanyonInit[i]!
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, entry.nX, entry.nY, entry.nLvlPrestId, entry.nRand, false)
}
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 4, 4, LVLPREST_ACT2_DESERT_VALLEY_WARP, -1, false)
}
// 1.10f: D2Common.0x6FD7DBC0
// 1.11 : D2Common.0x6FDD5600
export function DRLGOUTDESR_PlaceFillsInCanyon(pLevel: D2DrlgLevelStrc): void {
const nLevelPrestIds1: readonly number[] = [
LVLPREST_ACT2_DESERT_FILL_BONE_1,
LVLPREST_ACT2_DESERT_FILL_BONE_2,
LVLPREST_ACT2_DESERT_FILL_BERMS_3,
LVLPREST_ACT2_DESERT_FILL_BERMS_4,
LVLPREST_ACT2_DESERT_FILL_WAGON_1,
]
const nLevelPrestIds2: readonly number[] = [
LVLPREST_ACT2_DESERT_VALLEY_RUIN_1,
LVLPREST_ACT2_DESERT_VALLEY_RUIN_2,
]
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false)
DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true)
}

View File

@ -36,6 +36,7 @@ import {
LEVEL_BLOODMOOR,
LEVEL_COLDPLAINS,
LEVEL_MONASTERYGATE,
LEVEL_PANDEMONIUMRUN2,
LEVEL_ROGUEENCAMPMENT,
grid2PickedFile,
grid2PickedFileBits,
@ -96,13 +97,16 @@ import { DRLGROOM_AreXYInsideCoordinates, DRLGROOM_GetVisArrayFromLevelId } from
import { DRLGPRESET_AllocDrlgMap, DRLGPRESET_BuildArea, DRLGPRESET_SetPickedFileInDrlgMap } from './drlg-preset.ts'
import { DRLG_GetActNoFromLevelId } from './drlg-drlg.ts'
import { notifyLevelGrid } from './drlg-hooks.ts'
import { DRLGOUTDESR_InitAct2OutdoorLevel } from './drlg-outdesr.ts'
import { DRLGOUTWILD_GetBridgeCoords, DRLGOUTWILD_InitAct1OutdoorLevel } from './drlg-outwild.ts'
import {
DRLGOUTPLACE_CreateOutdoorRoomEx,
DRLGOUTPLACE_InitAct3OutdoorLevel,
DRLGOUTPLACE_PlaceAct1245OutdoorBorders,
DRLGOUTPLACE_SetOutGridLinkFlags,
sub_6FD80750,
} from './drlg-outplace.ts'
import { DRLGOUTSIEGE_InitAct5OutdoorLevel } from './drlg-outsiege.ts'
import { DRLGTILESUB_AddSecondaryBorder } from './drlg-tilesub.ts'
// D2C_Levels (DataTbls/LevelsIds.h) used only by the Act IV code of this file.
@ -794,34 +798,36 @@ export function DRLGOUTDOORS_GenerateLevel(pLevel: D2DrlgLevelStrc): void {
pVertex = pVertex.pNext!
} while (pVertex !== pOutdoorInfo.pVertex)
// #ifdef D2_VERSION_HAS_UBERS: LEVEL_PANDEMONIUMRUN2 -> ACT_II. Not compiled in the oracle, which
// builds D2MOO with the default D2_VERSION_110F of D2BuildInformation.h (either way not Act I).
const nAct = DRLG_GetActNoFromLevelId(pLevel.nLevelId)
const nAct =
pLevel.nLevelId === LEVEL_PANDEMONIUMRUN2
? ACT_II
: DRLG_GetActNoFromLevelId(pLevel.nLevelId)
switch (nAct) {
case ACT_I:
DRLGOUTWILD_InitAct1OutdoorLevel(pLevel)
// Mirrors the oracle's link-time `--wrap=DRLGOUTWILD_InitAct1OutdoorLevel`
// (tools/d2moo-oracle/src/main.cpp ~line 241), which captures the `levelGrid` stage right after
// the real DRLGOUTWILD_InitAct1OutdoorLevel returns and before any room of the level exists.
notifyLevelGrid(pLevel)
break
case ACT_II:
// DRLGOUTDESR_InitAct2OutdoorLevel(pLevel);
throw new Error('DRLGOUTDOORS_GenerateLevel: act 2 is not ported')
DRLGOUTDESR_InitAct2OutdoorLevel(pLevel)
notifyLevelGrid(pLevel)
break
case ACT_III:
// DRLGOUTPLACE_InitAct3OutdoorLevel(pLevel);
throw new Error('DRLGOUTDOORS_GenerateLevel: act 3 is not ported')
DRLGOUTPLACE_InitAct3OutdoorLevel(pLevel)
notifyLevelGrid(pLevel)
break
case ACT_IV:
// DRLGOUTDOORS_InitAct4OutdoorLevel(pLevel); (ported below, but the Act IV pipeline is out of scope)
throw new Error('DRLGOUTDOORS_GenerateLevel: act 4 is not ported')
DRLGOUTDOORS_InitAct4OutdoorLevel(pLevel)
notifyLevelGrid(pLevel)
break
case ACT_V:
// DRLGOUTSIEGE_InitAct5OutdoorLevel(pLevel);
throw new Error('DRLGOUTDOORS_GenerateLevel: act 5 is not ported')
DRLGOUTSIEGE_InitAct5OutdoorLevel(pLevel)
notifyLevelGrid(pLevel)
break
default:
break

View File

@ -0,0 +1,280 @@
/**
* D2Common Act III outdoor jungle/Kurast DRLG: 1:1 port of D2MOO
* `source/D2Common/src/Drlg/DrlgOutJung.cpp`.
*/
import { SEED_RollLimitedRandomNumber } from '../d2-rng.ts'
import {
LVLPREST_ACT3_BURBS_BORDER_E,
LVLPREST_ACT3_BURBS_BORDER_N,
LVLPREST_ACT3_BURBS_BORDER_NE,
LVLPREST_ACT3_BURBS_BORDER_NW,
LVLPREST_ACT3_BURBS_BORDER_S,
LVLPREST_ACT3_BURBS_BORDER_SE,
LVLPREST_ACT3_BURBS_BORDER_SW,
LVLPREST_ACT3_BURBS_BORDER_W,
LVLPREST_ACT3_JUNGLE_HEAD,
LVLPREST_ACT3_JUNGLE_TAIL,
LVLPREST_ACT3_METRO_BORDER_E,
LVLPREST_ACT3_METRO_BORDER_N,
LVLPREST_ACT3_METRO_BORDER_NE,
LVLPREST_ACT3_METRO_BORDER_NW,
LVLPREST_ACT3_METRO_BORDER_S,
LVLPREST_ACT3_METRO_BORDER_SE,
LVLPREST_ACT3_METRO_BORDER_SW,
LVLPREST_ACT3_METRO_BORDER_W,
LVLPREST_ACT3_SLUMS_BORDER_E,
LVLPREST_ACT3_SLUMS_BORDER_N,
LVLPREST_ACT3_SLUMS_BORDER_NE,
LVLPREST_ACT3_SLUMS_BORDER_NW,
LVLPREST_ACT3_SLUMS_BORDER_S,
LVLPREST_ACT3_SLUMS_BORDER_SE,
LVLPREST_ACT3_SLUMS_BORDER_SW,
LVLPREST_ACT3_SLUMS_BORDER_W,
} from './drlg-ids.ts'
import { DRLGOUTDOORS_SpawnOutdoorLevelPresetEx, DRLGOUTDOORS_TestOutdoorLevelPreset } from './drlg-outdoors.ts'
import { DATATBLS_GetLevelDefRecord } from './drlg-tables.ts'
import { type D2DrlgLevelStrc, idiv, imod, LEVEL_FLAYERJUNGLE, LEVEL_SPIDERFOREST } from './drlg-types.ts'
const dword_6FDCFB18: readonly number[] = [0, 10, 20, 0]
const dword_6FDCFB28: readonly number[] = [0, 1, 2, 1, 0, 2, 0, 2, 1, 1, 2, 0, 2, 0, 1, 2, 1, 0]
// D2Common.0x6FD7FC20
export function DRLGOUTJUNG_BuildJungle(pLevel: D2DrlgLevelStrc): void {
if (pLevel.nLevelId >= LEVEL_SPIDERFOREST && pLevel.nLevelId <= LEVEL_FLAYERJUNGLE) {
const pLevelDefBin = DATATBLS_GetLevelDefRecord(pLevel.pDrlg.env.tables, LEVEL_SPIDERFOREST)
const nSizeX = pLevelDefBin.dwSizeX[pLevel.pDrlg.nDifficulty]! >> 5
const nSizeY = pLevelDefBin.dwSizeY[pLevel.pDrlg.nDifficulty]! >> 5
const nRand =
SEED_RollLimitedRandomNumber(pLevel.pSeed, 4 * (pLevel.nJungleDefs === 3 ? 1 : 0) + 2) | 0
const pJungleDefs = pLevel.pJungleDefs!
let nDefId = 0
let nFileIndex = 0
for (let i = 0; i < nSizeY; i += 1) {
if (pLevel.nLevelId === LEVEL_SPIDERFOREST && i === nSizeY - 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
0,
4 * i,
LVLPREST_ACT3_JUNGLE_HEAD,
pJungleDefs[nSizeX * nSizeY - 1] === 0 ? 1 : 0,
false,
)
nDefId += 2
} else if (pLevel.nLevelId === LEVEL_FLAYERJUNGLE && i === 0) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
0,
0,
LVLPREST_ACT3_JUNGLE_TAIL,
pJungleDefs[1] === 0 ? 1 : 0,
false,
)
nDefId += 2
} else {
for (let j = 0; j < nSizeX; j += 1) {
nDefId += 1
let nJungleDef = pJungleDefs[nDefId - 1]!
let v19 = 0
if (nJungleDef > LVLPREST_ACT3_JUNGLE_TAIL) {
nJungleDef += dword_6FDCFB18[pLevel.nLevelId - 76]!
v19 = dword_6FDCFB28[nFileIndex + 3 * nRand]!
nFileIndex += 1
} else {
v19 = -1
}
if (nJungleDef) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 4 * j, 4 * i, nJungleDef, v19, false)
}
}
}
}
}
}
// D2Common.0x6FD7FE50
export function DRLGOUTJUNG_BuildLowerKurast(pLevel: D2DrlgLevelStrc): void {
const nWidth = idiv(pLevel.pLevelCoords.nWidth, 8) - 1
const nHeight = idiv(pLevel.pLevelCoords.nHeight, 8) - 1
const v5 = idiv(idiv(pLevel.pLevelCoords.nWidth, 8) - 1, 2)
const v10 = pLevel.pDrlg.bJungleInterlink ? 1 : nWidth - 1
for (let i = 1; i < nWidth; i += 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
i,
0,
8 * (i === v10 ? 1 : 0) + LVLPREST_ACT3_SLUMS_BORDER_N,
-1,
false,
)
}
for (let i = 1; i < nWidth; i += (i === v5 ? 1 : 0) + 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
i,
nHeight,
8 * (i === v5 ? 1 : 0) + LVLPREST_ACT3_SLUMS_BORDER_S,
-1,
false,
)
}
for (let i = 1; i < nHeight; i += 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, i, LVLPREST_ACT3_SLUMS_BORDER_E, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT3_SLUMS_BORDER_W, -1, false)
}
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_SLUMS_BORDER_NW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, 0, LVLPREST_ACT3_SLUMS_BORDER_NE, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, nHeight, LVLPREST_ACT3_SLUMS_BORDER_SW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, nHeight, LVLPREST_ACT3_SLUMS_BORDER_SE, -1, false)
}
// D2Common.0x6FD7FFA0
export function DRLGOUTJUNG_BuildKurastBazaar(pLevel: D2DrlgLevelStrc): void {
const nWidth = idiv(pLevel.pLevelCoords.nWidth, 8) - 1
const nHeight = idiv(pLevel.pLevelCoords.nHeight, 8) - 1
let v7 = 0
let v8 = 0
if (pLevel.pDrlg.bJungleInterlink) {
v7 = idiv(pLevel.pLevelCoords.nWidth, 8) - 2
v8 = 1
} else {
v7 = 1
v8 = idiv(pLevel.pLevelCoords.nWidth, 8) - 2
}
for (let i = 1; i < nWidth; i += 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
i,
0,
8 * (i === v7 ? 1 : 0) + LVLPREST_ACT3_BURBS_BORDER_N,
-1,
false,
)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
i,
nHeight,
8 * (i === v8 ? 1 : 0) + LVLPREST_ACT3_BURBS_BORDER_S,
-1,
false,
)
}
for (let i = 1; i < nHeight; i += 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, i, LVLPREST_ACT3_BURBS_BORDER_E, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT3_BURBS_BORDER_W, -1, false)
}
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_BURBS_BORDER_NW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, 0, LVLPREST_ACT3_BURBS_BORDER_NE, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, nHeight, LVLPREST_ACT3_BURBS_BORDER_SW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, nHeight, LVLPREST_ACT3_BURBS_BORDER_SE, -1, false)
}
// D2Common.0x6FD800E0
export function DRLGOUTJUNG_BuildUpperKurast(pLevel: D2DrlgLevelStrc): void {
const nWidth = idiv(pLevel.pLevelCoords.nWidth, 8) - 1
const nHeight = idiv(pLevel.pLevelCoords.nHeight, 8) - 1
const v8 = pLevel.pDrlg.bJungleInterlink ? nWidth - 1 : 1
const nHalfWidth = idiv(nWidth, 2)
for (let i = 1; i < nWidth; i += (i === nHalfWidth ? 1 : 0) + 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
i,
0,
8 * (i === nHalfWidth ? 1 : 0) + LVLPREST_ACT3_METRO_BORDER_N,
-1,
false,
)
}
for (let i = 1; i < nWidth; i += 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
i,
nHeight,
8 * (i === v8 ? 1 : 0) + LVLPREST_ACT3_METRO_BORDER_S,
-1,
false,
)
}
for (let i = 1; i < nHeight; i += 1) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, i, LVLPREST_ACT3_METRO_BORDER_E, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT3_METRO_BORDER_W, -1, false)
}
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_METRO_BORDER_NW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, 0, LVLPREST_ACT3_METRO_BORDER_NE, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, nHeight, LVLPREST_ACT3_METRO_BORDER_SW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, nHeight, LVLPREST_ACT3_METRO_BORDER_SE, -1, false)
}
// D2Common.0x6FD80230
export function DRLGOUTJUNG_SpawnRandomPreset(
pLevel: D2DrlgLevelStrc,
nLevelPrestId1: number,
nLevelPrestId2: number,
a4: number,
): void {
const nVariants = (nLevelPrestId2 - nLevelPrestId1 + 1) >>> 0
let v34 = 0
const pOutdoors = pLevel.pOutdoors!
const v8 = Math.imul(pOutdoors.nGridWidth, pOutdoors.nGridHeight)
if (v8) {
const v11 = new Int32Array(2 * v8)
for (let i = 0; i < v8; i += 1) {
const v12 = idiv(i, pOutdoors.nGridWidth)
const v13 = imod(i, pOutdoors.nGridWidth)
v11[2 * i] = v13
v11[2 * i + 1] = v12
}
for (let i = 0; i < v8; i += 1) {
const v14 = SEED_RollLimitedRandomNumber(pLevel.pSeed, v8) >>> 0
const v19 = SEED_RollLimitedRandomNumber(pLevel.pSeed, v8) >>> 0
const v23 = v11[2 * v14]!
const v24 = v11[2 * v14 + 1]!
v11[2 * v14] = v11[2 * v19]!
v11[2 * v14 + 1] = v11[2 * v19 + 1]!
v11[2 * v19] = v23
v11[2 * v19 + 1] = v24
}
for (let i = 0; i < v8; i += 1) {
const a2b = v11[2 * i]!
const a3a = v11[2 * i + 1]!
const v26 = SEED_RollLimitedRandomNumber(pLevel.pSeed, nVariants) >>> 0
if (DRLGOUTDOORS_TestOutdoorLevelPreset(pLevel, a2b, a3a, v26 + nLevelPrestId1, 0, 15)) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, a2b, a3a, v26 + nLevelPrestId1, -1, false)
v34 += 1
if (a4 > 0 && v34 >= a4) {
break
}
}
}
}
}

View File

@ -46,6 +46,30 @@ import {
LVLPREST_ACT2_DESERT_BORDER_10,
LVLPREST_ACT2_DESERT_BORDER_11,
LVLPREST_ACT2_DESERT_BORDER_12,
LVLPREST_ACT3_BRIDGE,
LVLPREST_ACT3_BURBS_08X08,
LVLPREST_ACT3_BURBS_08X16,
LVLPREST_ACT3_BURBS_16X08,
LVLPREST_ACT3_BURBS_16X16,
LVLPREST_ACT3_BURBS_SEWER,
LVLPREST_ACT3_BURBS_TEMPLE,
LVLPREST_ACT3_BURBS_WAYPOINT,
LVLPREST_ACT3_METRO_08X08,
LVLPREST_ACT3_METRO_08X16,
LVLPREST_ACT3_METRO_16X08,
LVLPREST_ACT3_METRO_16X16,
LVLPREST_ACT3_METRO_SEWER,
LVLPREST_ACT3_METROTEMPLE,
LVLPREST_ACT3_SLUMS_08X08,
LVLPREST_ACT3_SLUMS_08X16,
LVLPREST_ACT3_SLUMS_16X08,
LVLPREST_ACT3_SLUMS_16X16,
LVLPREST_ACT3_TRAVINCAL_N,
LVLPREST_ACT3_TRAVINCAL_NE,
LVLPREST_ACT3_TRAVINCAL_NW,
LVLPREST_ACT3_TRAVINCAL_S,
LVLPREST_ACT3_TRAVINCAL_SE,
LVLPREST_ACT3_TRAVINCAL_SW,
LVLPREST_ACT4_MESA_BORDER_1,
LVLPREST_ACT4_MESA_BORDER_2,
LVLPREST_ACT4_MESA_BORDER_3,
@ -95,8 +119,17 @@ import {
DRLG_OUTDOORS_GenerateDirtPath,
DRLGOUTDOORS_GetOutLinkVisFlag,
DRLGOUTDOORS_GetPackedGrid2Info,
DRLGOUTDOORS_SpawnOutdoorLevelPreset,
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx,
} from './drlg-outdoors.ts'
import {
DRLGOUTJUNG_BuildJungle,
DRLGOUTJUNG_BuildKurastBazaar,
DRLGOUTJUNG_BuildLowerKurast,
DRLGOUTJUNG_BuildUpperKurast,
DRLGOUTJUNG_SpawnRandomPreset,
} from './drlg-outjung.ts'
import { sub_6FD84100 } from './drlg-outsiege.ts'
import { DRLGROOM_AddRoomExToLevel, DRLGROOM_AllocRoomEx, DRLGROOM_AreXYInsideCoordinates } from './drlg-room.ts'
import { DRLGROOMTILE_AllocTileGrid } from './drlg-roomtile.ts'
import { DATATBLS_GetLevelDefRecord } from './drlg-tables.ts'
@ -129,12 +162,16 @@ import {
i32,
idiv,
LEVEL_BURIALGROUNDS,
LEVEL_KURASTBAZAAR,
LEVEL_KURASTCAUSEWAY,
LEVEL_LOWERKURAST,
LEVEL_TRAVINCAL,
LEVEL_TUNDRAWASTELANDS,
LEVEL_UPPERKURAST,
} from './drlg-types.ts'
import { DRLGVER_AllocVertex, DRLGVER_GetCoordDiff } from './drlg-vertex.ts'
import { sub_6FDAB750 } from './path-dir.ts'
const LEVEL_TUNDRAWASTELANDS = 112
const gnBarricadeCliffBorderIds: readonly (readonly [number, number])[] = [
[LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1, LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW],
[LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_2, LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_2_SNOW],
@ -152,6 +189,101 @@ const gnBarricadeCliffBorderIds: readonly (readonly [number, number])[] = [
[-1, 0],
]
//D2Common.0x6FD80480
export function DRLGOUTPLACE_BuildKurast(pLevel: D2DrlgLevelStrc): void {
switch (pLevel.nLevelId) {
case LEVEL_LOWERKURAST:
DRLGOUTJUNG_BuildLowerKurast(pLevel)
break
case LEVEL_KURASTBAZAAR:
DRLGOUTJUNG_BuildKurastBazaar(pLevel)
break
case LEVEL_UPPERKURAST:
DRLGOUTJUNG_BuildUpperKurast(pLevel)
break
default:
break
}
switch (pLevel.nLevelId) {
case LEVEL_LOWERKURAST:
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_WAYPOINT, 0, 0, 15)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_SLUMS_16X16, LVLPREST_ACT3_SLUMS_16X16, 4)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_SLUMS_08X16, LVLPREST_ACT3_SLUMS_16X08, 0)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_SLUMS_08X08, LVLPREST_ACT3_SLUMS_08X08, 0)
break
case LEVEL_KURASTBAZAAR:
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 3, 3, LVLPREST_ACT3_BURBS_SEWER, 0, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
idiv(pLevel.pLevelCoords.nWidth, 8) - 4,
3,
LVLPREST_ACT3_BURBS_SEWER,
1,
false,
)
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_TEMPLE, 0, 0, 15)
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_TEMPLE, 1, 0, 15)
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_WAYPOINT, 0, 0, 15)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_BURBS_16X16, LVLPREST_ACT3_BURBS_16X16, 4)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_BURBS_08X16, LVLPREST_ACT3_BURBS_16X08, 0)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_BURBS_08X08, LVLPREST_ACT3_BURBS_08X08, 0)
break
case LEVEL_UPPERKURAST:
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
3,
idiv(pLevel.pLevelCoords.nHeight, 8) - 4,
LVLPREST_ACT3_METRO_SEWER,
0,
false,
)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
idiv(pLevel.pLevelCoords.nWidth, 8) - 4,
idiv(pLevel.pLevelCoords.nHeight, 8) - 4,
LVLPREST_ACT3_METRO_SEWER,
1,
false,
)
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_METROTEMPLE, 0, 0, 15)
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_METROTEMPLE, 1, 0, 15)
DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_WAYPOINT, 0, 0, 15)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_METRO_16X16, LVLPREST_ACT3_METRO_16X16, 4)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_METRO_08X16, LVLPREST_ACT3_METRO_16X08, 0)
DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_METRO_08X08, LVLPREST_ACT3_METRO_08X08, 0)
break
case LEVEL_KURASTCAUSEWAY:
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_BRIDGE, 0, false)
break
default:
return
}
}
//D2Common.0x6FD806A0
export function DRLGOUTPLACE_InitAct3OutdoorLevel(pLevel: D2DrlgLevelStrc): void {
DRLGOUTPLACE_SetOutGridLinkFlags(pLevel)
DRLGOUTJUNG_BuildJungle(pLevel)
DRLGOUTPLACE_BuildKurast(pLevel)
if (pLevel.nLevelId === LEVEL_TRAVINCAL) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_TRAVINCAL_NW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 2, 0, LVLPREST_ACT3_TRAVINCAL_N, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 6, 0, LVLPREST_ACT3_TRAVINCAL_NE, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 4, LVLPREST_ACT3_TRAVINCAL_SW, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 2, 4, LVLPREST_ACT3_TRAVINCAL_S, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 6, 4, LVLPREST_ACT3_TRAVINCAL_SE, -1, false)
}
}
interface D2UnkOutPlaceStrc12 {
field_0: number
field_4: number
@ -655,7 +787,8 @@ export function DRLGOUTPLACE_PlaceAct1245OutdoorBorders(pLevel: D2DrlgLevelStrc)
break
case LVLTYPE_ACT5_BARRICADE:
throw new Error('DRLGOUTPLACE_PlaceAct1245OutdoorBorders: act 5 is not ported')
nLevelPrestId = sub_6FD80BE0(nCurrentDiffX, nCurrentDiffY, sub_6FD84100(pLevel))
break
default:
nLevelPrestId = sub_6FD80BE0(nCurrentDiffX, nCurrentDiffY, -1)
@ -750,7 +883,8 @@ export function DRLGOUTPLACE_PlaceAct1245OutdoorBorders(pLevel: D2DrlgLevelStrc)
break
case LVLTYPE_ACT5_BARRICADE:
throw new Error('DRLGOUTPLACE_PlaceAct1245OutdoorBorders: act 5 is not ported')
v41 = sub_6FD84100(pLevel)
break
default:
v41 = -1

View File

@ -0,0 +1,714 @@
/**
* D2Common Act V outdoor siege/barricade DRLG: 1:1 port of D2MOO
* `source/D2Common/src/Drlg/DrlgOutSiege.cpp`.
*/
import { SEED_RollLimitedRandomNumber } from '../d2-rng.ts'
import { DRLGGRID_AlterGridFlag, DRLGGRID_GetGridEntry } from './drlg-grid.ts'
import {
LVLPREST_ACT5_BARRICADE_1,
LVLPREST_ACT5_BARRICADE_1_SNOW,
LVLPREST_ACT5_BARRICADE_8,
LVLPREST_ACT5_BARRICADE_BUILDING,
LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1,
LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW,
LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3,
LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3_SNOW,
LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4,
LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4_SNOW,
LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7,
LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7_SNOW,
LVLPREST_ACT5_BARRICADE_ENTRANCE_16X32,
LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16,
LVLPREST_ACT5_BARRICADE_EXIT_16X32,
LVLPREST_ACT5_BARRICADE_EXIT_32X16,
LVLPREST_ACT5_BARRICADE_FILLER,
LVLPREST_ACT5_BARRICADE_FILLER_TREASURE,
LVLPREST_ACT5_BARRICADE_FROM_CAVE_16X32_SNOW,
LVLPREST_ACT5_BARRICADE_FROM_CAVE_32X16_SNOW,
LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N,
LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W,
LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1,
LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1_SNOW,
LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3,
LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3_SNOW,
LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4,
LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4_SNOW,
LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5,
LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5_SNOW,
LVLPREST_ACT5_BARRICADE_RUINS_N_1,
LVLPREST_ACT5_BARRICADE_RUINS_N_2,
LVLPREST_ACT5_BARRICADE_RUINS_N_TREASURE,
LVLPREST_ACT5_BARRICADE_RUINS_W_1,
LVLPREST_ACT5_BARRICADE_RUINS_W_2,
LVLPREST_ACT5_BARRICADE_RUINS_W_TREASURE,
LVLPREST_ACT5_BARRICADE_SNOW_LAKE_1,
LVLPREST_ACT5_BARRICADE_SNOW_LAKE_2,
LVLPREST_ACT5_BARRICADE_SNOW_OTHER,
LVLPREST_ACT5_BARRICADE_SNOW_TREASURE,
LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32,
LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32_SNOW,
LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16,
LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16_SNOW,
LVLPREST_ACT5_BARRICADE_TO_SIEGE,
LVLPREST_ACT5_BARRICADE_WAYPOINT_DIRT,
LVLPREST_ACT5_BARRICADE_WAYPOINT_SNOW,
LVLPREST_ACT5_SIEGE_TO_TOWN,
LVLSUB_ACT5_BARRICADE,
} from './drlg-ids.ts'
import {
DRLGOUTDOORS_AlterAdjacentPresetGridCells,
DRLGOUTDOORS_GetPackedGrid2Info,
DRLGOUTDOORS_GetPresetIndexFromGridCell,
DRLGOUTDOORS_SetBlankGridCell,
DRLGOUTDOORS_SpawnOutdoorLevelPreset,
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx,
DRLGOUTDOORS_TestGridCellNonLvlLink,
} from './drlg-outdoors.ts'
import { DRLGOUTPLACE_SetOutGridLinkFlags, sub_6FD80BE0, sub_6FD80C10 } from './drlg-outplace.ts'
import { DATATBLS_GetLvlPrestTxtRecord } from './drlg-tables.ts'
import { DRLGTILESUB_AddSecondaryBorder } from './drlg-tilesub.ts'
import {
type D2DrlgLevelStrc,
FLAG_OPERATION_OR,
GRID2_LVL_LINK,
GRID2_UNKB00,
idiv,
imod,
LEVEL_ARREATPLATEAU,
LEVEL_BLOODYFOOTHILLS,
LEVEL_ID_ACT5_BARRICADE_1,
LEVEL_TUNDRAWASTELANDS,
outdoorCoord,
} from './drlg-types.ts'
import { DRLGVER_GetCoordDiff } from './drlg-vertex.ts'
// D2Common.0x6FD84100
export function sub_6FD84100(pLevel: D2DrlgLevelStrc): number {
return (pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS ? 1 : 0) + 4
}
const stru_6FDD09C8: readonly (readonly [number, number])[] = [
[-1, 0],
[0, -1],
[1, 0],
[0, 1],
[0, -1],
[1, 0],
[0, 1],
[-1, 0],
[-1, 0],
[0, -1],
[1, 0],
[0, 1],
]
// D2Common.0x6FD84110
export function DRLGOUTSIEGE_InitAct5OutdoorLevel(pLevel: D2DrlgLevelStrc): void {
const pOutdoors = pLevel.pOutdoors!
if (pLevel.nLevelId === LEVEL_BLOODYFOOTHILLS) {
const pLvlPrestTxtRecord = DATATBLS_GetLvlPrestTxtRecord(pLevel.pDrlg.env.tables, LVLPREST_ACT5_SIEGE_TO_TOWN)
let nSize = pOutdoors.nGridWidth - idiv(pLvlPrestTxtRecord.nSizeX, 8)
for (let i = 0; i < 15; i += 1) {
if (nSize < 0) {
throw new Error('Siege Level is the wrong size')
}
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nSize, 0, i + LVLPREST_ACT5_SIEGE_TO_TOWN, 0, false)
nSize -= idiv(pLvlPrestTxtRecord.nSizeX, 8)
}
} else {
DRLGOUTPLACE_SetOutGridLinkFlags(pLevel)
let pPreviousVertex = pOutdoors.pVertex!
let pDrlgVertex = pPreviousVertex.pNext!
const nLookupId = sub_6FD84100(pLevel)
do {
const prevDiff = DRLGVER_GetCoordDiff(pPreviousVertex)
const curDiff = DRLGVER_GetCoordDiff(pDrlgVertex)
const nPreviousDiffX = prevDiff.nDiffX
const nPreviousDiffY = prevDiff.nDiffY
const nCurrentDiffX = curDiff.nDiffX
const nCurrentDiffY = curDiff.nDiffY
const nPreviousDiffXAbs = Math.abs(nPreviousDiffX)
const nPreviousDiffYAbs = Math.abs(nPreviousDiffY)
let nPreviousX = pPreviousVertex.nPosX & ~1
let nPreviousY = pPreviousVertex.nPosY & ~1
const nCurrentX = pDrlgVertex.nPosX & ~1
const nCurrentY = pDrlgVertex.nPosY & ~1
const nLevelPrestId = sub_6FD80BE0(nPreviousDiffX, nPreviousDiffY, nLookupId)
if ((pPreviousVertex.dwFlags & 2) === 0) {
while (nPreviousX !== nCurrentX || nPreviousY !== nCurrentY) {
nPreviousX += 2 * nPreviousDiffX
nPreviousY += 2 * nPreviousDiffY
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nPreviousX, nPreviousY, nLevelPrestId, -1, false)
DRLGGRID_AlterGridFlag(pOutdoors.pGrid[2], nPreviousX, nPreviousY, GRID2_UNKB00, FLAG_OPERATION_OR)
}
}
if (pPreviousVertex.dwFlags & 1) {
const nX = Math.max(pPreviousVertex.nPosX, pDrlgVertex.nPosX)
const nXCapped = (nX - 4 * nPreviousDiffXAbs) & ~1
const nY = Math.max(pPreviousVertex.nPosY, pDrlgVertex.nPosY)
const nYCapped = (nY - 4 * nPreviousDiffYAbs) & ~1
DRLGGRID_AlterGridFlag(pOutdoors.pGrid[2], nXCapped, nYCapped, GRID2_LVL_LINK, FLAG_OPERATION_OR)
DRLGGRID_AlterGridFlag(
pOutdoors.pGrid[2],
nXCapped + 2 * nPreviousDiffXAbs,
nYCapped + 2 * nPreviousDiffYAbs,
GRID2_LVL_LINK,
FLAG_OPERATION_OR,
)
}
const nRand = sub_6FD80C10(
2 * nPreviousDiffX,
2 * nPreviousDiffY,
2 * nCurrentDiffX,
2 * nCurrentDiffY,
nLookupId,
)
if (nRand) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nCurrentX, nCurrentY, nRand, -1, false)
DRLGGRID_AlterGridFlag(pOutdoors.pGrid[2], nCurrentX, nCurrentY, GRID2_UNKB00, FLAG_OPERATION_OR)
}
pPreviousVertex = pDrlgVertex
pDrlgVertex = pDrlgVertex.pNext!
} while (pPreviousVertex !== pOutdoors.pVertex)
if (pLevel.nLevelId === LEVEL_ID_ACT5_BARRICADE_1) {
sub_6FD846C0(pLevel)
}
const nLastX = pOutdoors.nGridWidth - 2
const nLastY = pOutdoors.nGridHeight - 2
let nX = nLastX
let nY = 0
const nLevelPrestIdCliff =
pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS
? LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW
: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1
const nLevelPrestIdRavine =
pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS
? LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1_SNOW
: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1
while (nX !== 0 || nY !== nLastY) {
const nIndex = DRLGGRID_GetGridEntry(pOutdoors.pGrid[0], nX, nY) - nLevelPrestIdCliff
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, nIndex + nLevelPrestIdRavine, -1, false)
nX += 2 * stru_6FDD09C8[nIndex]![0]
nY += 2 * stru_6FDD09C8[nIndex]![1]
}
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
pOutdoors.nGridWidth - 2,
0,
pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS
? LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7_SNOW
: LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7,
-1,
false,
)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
0,
pOutdoors.nGridHeight - 2,
pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS
? LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5_SNOW
: LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5,
-1,
false,
)
DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(pLevel)
DRLGOUTSIEGE_PlaceCaves(pLevel)
if (pLevel.nLevelId === LEVEL_ID_ACT5_BARRICADE_1) {
DRLGOUTSIEGE_ConnectBarricadeAndSiege(pLevel)
}
DRLGOUTSIEGE_AddACt5SecondaryBorder(pLevel)
DRLGOUTSIEGE_PlacePrisons(pLevel)
DRLGOUTSIEGE_PlaceSpecialPresets(pLevel)
}
}
interface D2DrlgOutSiegeInitStrc {
readonly nLevelId: number
readonly field_4: number
readonly field_8: number
readonly nLevelPrestId1: number
readonly nLevelPrestId2: number
}
const stru_6FDD0988: readonly D2DrlgOutSiegeInitStrc[] = [
{
nLevelId: LEVEL_ARREATPLATEAU,
field_4: 0,
field_8: 0,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
field_4: 0,
field_8: 1,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_FROM_CAVE_32X16_SNOW,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_FROM_CAVE_16X32_SNOW,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
field_4: 0,
field_8: 0,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16_SNOW,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32_SNOW,
},
]
// D2Common.0x6FD844F0
export function DRLGOUTSIEGE_PlaceCaves(pLevel: D2DrlgLevelStrc): void {
let nLevelPrestId = 0
let nX = 0
let nY = 0
for (let i = 0; i < stru_6FDD0988.length; i += 1) {
const entry = stru_6FDD0988[i]!
if (pLevel.nLevelId === entry.nLevelId) {
if (pLevel.pLevelCoords.nWidth <= pLevel.pLevelCoords.nHeight) {
nLevelPrestId = entry.nLevelPrestId1
nX = 2
if (entry.field_8) {
nY = pLevel.pOutdoors!.nGridHeight - 2
} else {
nY = 0
}
} else {
nLevelPrestId = entry.nLevelPrestId2
nY = 2
if (entry.field_8) {
nX = pLevel.pOutdoors!.nGridWidth - 2
} else {
nX = 0
}
}
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, nLevelPrestId, entry.field_4, false)
}
}
}
// D2Common.0x6FD84580
export function DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(pLevel: D2DrlgLevelStrc): void {
const pOutdoors = pLevel.pOutdoors!
const nPickedFile = 2 * (pLevel.nLevelId === LEVEL_BLOODYFOOTHILLS ? 1 : 0) - 1
for (let i = 0; i < pOutdoors.nGridWidth; i += 1) {
if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, i, 0) & GRID2_LVL_LINK) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, i, 0, LVLPREST_ACT5_BARRICADE_EXIT_32X16, nPickedFile, false)
break
}
}
for (let i = 0; i < pOutdoors.nGridWidth; i += 1) {
if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, i, pOutdoors.nGridHeight - 2) & GRID2_LVL_LINK) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
i,
pOutdoors.nGridHeight - 2,
LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16,
nPickedFile,
false,
)
break
}
}
for (let i = 0; i < pOutdoors.nGridHeight; i += 1) {
if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, 0, i) & GRID2_LVL_LINK) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT5_BARRICADE_EXIT_16X32, nPickedFile, false)
break
}
}
for (let i = 0; i < pOutdoors.nGridHeight; i += 1) {
if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, pOutdoors.nGridWidth - 2, i) & GRID2_LVL_LINK) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(
pLevel,
pOutdoors.nGridWidth - 2,
i,
LVLPREST_ACT5_BARRICADE_ENTRANCE_16X32,
nPickedFile,
false,
)
break
}
}
}
// D2Common.0x6FD846C0
export function sub_6FD846C0(pLevel: D2DrlgLevelStrc): void {
const pOutdoors = pLevel.pOutdoors!
DRLGGRID_AlterGridFlag(
pOutdoors.pGrid[2],
pOutdoors.nGridWidth - 2,
pOutdoors.nGridHeight - 4,
GRID2_LVL_LINK,
FLAG_OPERATION_OR,
)
DRLGGRID_AlterGridFlag(
pOutdoors.pGrid[2],
pOutdoors.nGridWidth - 2,
pOutdoors.nGridHeight - 3,
GRID2_LVL_LINK,
FLAG_OPERATION_OR,
)
}
// D2Common.0x6FD84700
export function DRLGOUTSIEGE_AddACt5SecondaryBorder(pLevel: D2DrlgLevelStrc): void {
if (pLevel.nLevelId !== LEVEL_BLOODYFOOTHILLS) {
const a1 = {
pLevel,
field_4: outdoorCoord(pLevel.pOutdoors!),
pGrid1: pLevel.pOutdoors!.pGrid[0],
pGrid2: pLevel.pOutdoors!.pGrid[2],
nLevelPrestId: 0,
field_14: -1,
nLvlSubId: LVLSUB_ACT5_BARRICADE,
field_1C: null,
field_20: null,
field_24: sub_6FD84820,
field_28: sub_6FD84780,
field_2C: DRLGOUTDOORS_AlterAdjacentPresetGridCells,
field_30: DRLGOUTDOORS_SetBlankGridCell,
field_34: DRLGOUTDOORS_SpawnOutdoorLevelPresetEx,
}
DRLGTILESUB_AddSecondaryBorder(a1)
}
}
interface D2DrlgOutSiegeInitStrc2 {
readonly nStyle: number
readonly field_4: number
readonly field_8: number
readonly nLevelPrestId1: number
readonly nLevelPrestId2: number
}
const stru_6FDD0A28: readonly D2DrlgOutSiegeInitStrc2[] = [
{ nStyle: 49, field_4: 1, field_8: 16, nLevelPrestId1: LVLPREST_ACT5_BARRICADE_1, nLevelPrestId2: LVLPREST_ACT5_BARRICADE_1_SNOW },
{ nStyle: 49, field_4: 31, field_8: 46, nLevelPrestId1: LVLPREST_ACT5_BARRICADE_1, nLevelPrestId2: LVLPREST_ACT5_BARRICADE_1_SNOW },
{
nStyle: 48,
field_4: 1,
field_8: 1,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3_SNOW,
},
{
nStyle: 48,
field_4: 2,
field_8: 3,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW,
},
{
nStyle: 48,
field_4: 4,
field_8: 4,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4_SNOW,
},
{
nStyle: 48,
field_4: 5,
field_8: 5,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3_SNOW,
},
{
nStyle: 48,
field_4: 6,
field_8: 7,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1_SNOW,
},
{
nStyle: 48,
field_4: 8,
field_8: 8,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4_SNOW,
},
{ nStyle: 48, field_4: 30, field_8: 30, nLevelPrestId1: 0, nLevelPrestId2: 0 },
{ nStyle: 48, field_4: 31, field_8: 31, nLevelPrestId1: -5, nLevelPrestId2: -5 },
]
// D2Common.0x6FD84780
export function sub_6FD84780(pLevel: D2DrlgLevelStrc, nStyle: number, a3: number): number {
if (nStyle !== 48 && nStyle !== 49) {
throw new Error(`sub_6FD84780: unexpected nStyle=${nStyle}`)
}
for (let i = 0; i < stru_6FDD0A28.length; i += 1) {
const entry = stru_6FDD0A28[i]!
if (nStyle === entry.nStyle && a3 >= entry.field_4 && a3 <= entry.field_8) {
if (pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS) {
return a3 + entry.nLevelPrestId2 - entry.field_4
} else {
return a3 + entry.nLevelPrestId1 - entry.field_4
}
}
}
throw new Error(`sub_6FD84780: unreachable nStyle=${nStyle} a3=${a3}`)
}
// D2Common.0x6FD84820
export function sub_6FD84820(
pLevel: D2DrlgLevelStrc,
nX: number,
nY: number,
a4: number,
_a5: number,
a6: number,
): boolean {
const v8 = sub_6FD84780(pLevel, (a6 >>> 20) & 0x3f, (a6 >>> 8) & 0xff)
if (v8 === -5) {
return true
} else if (v8 === a4) {
return DRLGOUTDOORS_TestGridCellNonLvlLink(pLevel, nX, nY) !== 0
}
return false
}
interface D2DrlgOutSiegeInitStrc3 {
readonly nLevelId: number
readonly nLevelPrestId1: number
readonly nLevelPrestId2: number
readonly field_C: number
readonly field_10: number
readonly field_14: number
readonly bMustHave: boolean
}
const stru_6FDD0AF8: readonly D2DrlgOutSiegeInitStrc3[] = [
{
nLevelId: LEVEL_ID_ACT5_BARRICADE_1,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W,
field_C: 0,
field_10: 25,
field_14: 1,
bMustHave: true,
},
{
nLevelId: LEVEL_ARREATPLATEAU,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W,
field_C: 0,
field_10: 25,
field_14: 1,
bMustHave: true,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W,
field_C: 1,
field_10: 25,
field_14: 1,
bMustHave: true,
},
{
nLevelId: LEVEL_ARREATPLATEAU,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_WAYPOINT_DIRT,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_WAYPOINT_DIRT,
field_C: -1,
field_10: 25,
field_14: 1,
bMustHave: true,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_WAYPOINT_SNOW,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_WAYPOINT_SNOW,
field_C: -1,
field_10: 25,
field_14: 1,
bMustHave: true,
},
{
nLevelId: LEVEL_ID_ACT5_BARRICADE_1,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RUINS_N_TREASURE,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RUINS_W_TREASURE,
field_C: -1,
field_10: 10,
field_14: 1,
bMustHave: false,
},
{
nLevelId: LEVEL_ID_ACT5_BARRICADE_1,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RUINS_N_1,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RUINS_W_1,
field_C: -1,
field_10: 20,
field_14: 4,
bMustHave: false,
},
{
nLevelId: LEVEL_ID_ACT5_BARRICADE_1,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RUINS_N_2,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RUINS_W_2,
field_C: -1,
field_10: 20,
field_14: 4,
bMustHave: false,
},
{
nLevelId: LEVEL_ARREATPLATEAU,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_FILLER_TREASURE,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_FILLER_TREASURE,
field_C: -1,
field_10: 10,
field_14: 1,
bMustHave: false,
},
{
nLevelId: LEVEL_ARREATPLATEAU,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_BUILDING,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_BUILDING,
field_C: -1,
field_10: 8,
field_14: 1,
bMustHave: false,
},
{
nLevelId: LEVEL_ARREATPLATEAU,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_FILLER,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_FILLER,
field_C: -1,
field_10: 15,
field_14: 5,
bMustHave: false,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_1,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_1,
field_C: -1,
field_10: 9,
field_14: 4,
bMustHave: false,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_2,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_2,
field_C: -1,
field_10: 9,
field_14: 4,
bMustHave: false,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_OTHER,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_OTHER,
field_C: -1,
field_10: 9,
field_14: 4,
bMustHave: false,
},
{
nLevelId: LEVEL_TUNDRAWASTELANDS,
nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_TREASURE,
nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_TREASURE,
field_C: -1,
field_10: 5,
field_14: 3,
bMustHave: false,
},
]
// D2Common.0x6FD84870
export function DRLGOUTSIEGE_PlaceSpecialPresets(pLevel: D2DrlgLevelStrc): void {
for (let i = 0; i < stru_6FDD0AF8.length; i += 1) {
const entry = stru_6FDD0AF8[i]!
if (pLevel.nLevelId === entry.nLevelId) {
const nLevelPrestId =
pLevel.pLevelCoords.nWidth < pLevel.pLevelCoords.nHeight ? entry.nLevelPrestId1 : entry.nLevelPrestId2
let bAdded = false
for (let j = 0; j < entry.field_14; j += 1) {
bAdded = DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nLevelPrestId, entry.field_C, 0, 15)
}
if (!bAdded && entry.bMustHave) {
throw new Error(`DRLGOUTSIEGE_PlaceSpecialPresets: failed to place required preset ${nLevelPrestId}`)
}
}
}
}
// D2Common.0x6FD84910
export function DRLGOUTSIEGE_PlacePrisons(pLevel: D2DrlgLevelStrc): void {
let nPrisonsPlaced = 0
const nPrisonsToPlace = 3
if (pLevel.nLevelId === LEVEL_ID_ACT5_BARRICADE_1) {
const pOutdoors = pLevel.pOutdoors!
for (let nAttemptNumber = 0; nAttemptNumber < 90 && nPrisonsPlaced < nPrisonsToPlace; nAttemptNumber += 1) {
const nRandX = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridWidth, 2)) | 0)
const nRandY = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridHeight, 2)) | 0)
const nLevelPrestId = DRLGOUTDOORS_GetPresetIndexFromGridCell(pLevel, nRandX, nRandY)
if (nLevelPrestId >= LVLPREST_ACT5_BARRICADE_1 && nLevelPrestId <= LVLPREST_ACT5_BARRICADE_8) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nRandX, nRandY, nLevelPrestId + 16, -1, false)
nPrisonsPlaced += 1
}
}
const nRandX = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridWidth, 2)) | 0)
const nRandY = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridHeight, 2)) | 0)
for (let j = 0; j < pOutdoors.nGridHeight && nPrisonsPlaced < nPrisonsToPlace; j += 1) {
for (let i = 0; i < pOutdoors.nGridWidth && nPrisonsPlaced < nPrisonsToPlace; i += 1) {
const nX = imod(i + nRandX, pOutdoors.nGridWidth)
const nY = imod(j + nRandY, pOutdoors.nGridHeight)
const nLevelPrestId = DRLGOUTDOORS_GetPresetIndexFromGridCell(pLevel, nX, nY)
if (nLevelPrestId >= LVLPREST_ACT5_BARRICADE_1 && nLevelPrestId <= LVLPREST_ACT5_BARRICADE_8) {
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, nLevelPrestId + 16, -1, false)
nPrisonsPlaced += 1
}
}
}
}
}
// D2Common.0x6FD84BB0
export function DRLGOUTSIEGE_ConnectBarricadeAndSiege(pLevel: D2DrlgLevelStrc): void {
const pOutdoors = pLevel.pOutdoors!
const pLvlPrestTxtRecord = DATATBLS_GetLvlPrestTxtRecord(pLevel.pDrlg.env.tables, LVLPREST_ACT5_BARRICADE_TO_SIEGE)
const nX = pOutdoors.nGridWidth - idiv(pLvlPrestTxtRecord.nSizeX, 8)
const nY = pOutdoors.nGridHeight - idiv(pLvlPrestTxtRecord.nSizeY, 8)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, LVLPREST_ACT5_BARRICADE_TO_SIEGE, -1, false)
DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY - 2, LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4, -1, false)
}

View File

@ -11,6 +11,7 @@
*/
import { SEED_RollLimitedRandomNumber, SEED_RollRandomNumber, type D2SeedStrc } from '../d2-rng.ts'
import { DRLGANIM_AllocAnimationTileGrids, DRLGANIM_TestLoadAnimatedRoomTiles } from './drlg-anim.ts'
import {
DATATBLS_GetLvlPrestTxtRecord,
DATATBLS_GetLvlPrestTxtRecordFromLevelId,
@ -1142,11 +1143,6 @@ export function DRLGPRESET_InitPresetRoomGrids(pDrlgRoom: D2DrlgRoomStrc): void
}
}
// DrlgDrlgAnim.cpp is not ported: no Act I LvlPrest.txt row sets Animate (the call sites throw instead).
function DRLGANIM_NotPorted(szFunction: string): never {
throw new Error(`${szFunction} is not ported (LvlPrest.txt Animate is set)`)
}
//D2Common.0x6FD87130
export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): void {
DRLGROOMTILE_AllocTileGrid(pDrlgRoom)
@ -1198,7 +1194,7 @@ export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): voi
DRLGROOMTILE_CountAllTileTypes(pDrlgRoom, pMaze.pFloorGrid[i]!, bCheckCoordinatesValidity, bKillEdgeX, bKillEdgeY)
if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) {
DRLGANIM_NotPorted('DRLGANIM_TestLoadAnimatedRoomTiles')
DRLGANIM_TestLoadAnimatedRoomTiles(pDrlgRoom, pMaze.pFloorGrid[i]!, null, 0, bKillEdgeX, bKillEdgeY)
}
}
@ -1207,14 +1203,21 @@ export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): voi
DRLGROOMTILE_CountAllTileTypes(pDrlgRoom, pMaze.pWallGrid[i]!, false, bKillEdgeX, bKillEdgeY)
if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) {
DRLGANIM_NotPorted('DRLGANIM_TestLoadAnimatedRoomTiles')
DRLGANIM_TestLoadAnimatedRoomTiles(
pDrlgRoom,
pMaze.pWallGrid[i]!,
pMaze.pTileTypeGrid[i]!,
TILETYPE_FLOOR,
bKillEdgeX,
bKillEdgeY,
)
}
}
DRLGROOMTILE_CountAllTileTypes(pDrlgRoom, pMaze.pCellGrid, false, bKillEdgeX, bKillEdgeY)
if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) {
DRLGANIM_NotPorted('DRLGANIM_TestLoadAnimatedRoomTiles')
DRLGANIM_TestLoadAnimatedRoomTiles(pDrlgRoom, pMaze.pCellGrid, null, TILETYPE_SHADOW, bKillEdgeX, bKillEdgeY)
}
if (bTownOrGraveyard) {
@ -1239,7 +1242,15 @@ export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): voi
}
if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) {
DRLGANIM_NotPorted('DRLGANIM_AllocAnimationTileGrids')
DRLGANIM_AllocAnimationTileGrids(
pDrlgRoom,
pMazeMap.pLvlPrestTxtRecord.nAnimSpeed,
pMaze.pWallGrid,
pFile.nWallLayers,
pMaze.pFloorGrid,
pFile.nFloorLayers,
pMaze.pCellGrid,
)
}
const pTileGrid = tileGridOf(pDrlgRoom)

View File

@ -470,40 +470,12 @@ export function sub_6FD77BB0(pDrlgRoom: D2DrlgRoomStrc): void {
pDrlgRoom.ppRoomsNear = ppRoomList
pDrlgRoom.nRoomsNear = nNearRooms
for (let pOrth = pDrlgRoom.pDrlgOrth; pOrth; pOrth = pOrth.pNext) {
if (pOrth.bPreset) {
const pLevel = pOrth.pLevel
if (pLevel) {
for (let pNearRoom = pLevel.pFirstRoomEx; pNearRoom; pNearRoom = pNearRoom.pDrlgRoomNext) {
let nX = pDrlgRoom.pDrlgCoord.nPosX - pNearRoom.pDrlgCoord.nWidth - pNearRoom.pDrlgCoord.nPosX
if (nX < 0) {
nX = pNearRoom.pDrlgCoord.nPosX - (pDrlgRoom.pDrlgCoord.nPosX + pDrlgRoom.pDrlgCoord.nWidth)
}
let nY = pDrlgRoom.pDrlgCoord.nPosY - pNearRoom.pDrlgCoord.nHeight - pNearRoom.pDrlgCoord.nPosY
if (nY < 0) {
nY = pNearRoom.pDrlgCoord.nPosY - (pDrlgRoom.pDrlgCoord.nPosY + pDrlgRoom.pDrlgCoord.nHeight)
}
if (nX < 0) nX = 0
if (nY < 0) nY = 0
if (nX < 6 && nY < 6) {
pDrlgRoom.ppRoomsNear.push(pNearRoom)
pDrlgRoom.nRoomsNear += 1
DRLGROOM_SortRoomListByPosition(pDrlgRoom.ppRoomsNear, pDrlgRoom.nRoomsNear)
}
}
}
}
}
if (pDrlgRoom.dwFlags & DRLGROOMFLAG_HAS_WARP_MASK) {
let nFlags = DRLGROOMFLAG_HAS_WARP_0
let nWarpId = 0
do {
if (nFlags & pDrlgRoom.dwFlags) {
if (nFlags & pDrlgRoom.dwFlags && pDrlgRoom.pLevel.nLevelId !== 133) {
const pSourceVisArray = DRLGROOM_GetVisArrayFromLevelId(pDrlg, pDrlgRoom.pLevel.nLevelId)
const pLevel = DRLG_GetLevel(pDrlg, pSourceVisArray[nWarpId]!)
const pDestinationVisArray = DRLGROOM_GetVisArrayFromLevelId(pDrlg, pSourceVisArray[nWarpId]!)
@ -513,11 +485,22 @@ export function sub_6FD77BB0(pDrlgRoom: D2DrlgRoomStrc): void {
}
if (nWarpDestination !== -1) {
let nSourceVisCount = 0
for (let i = 0; i <= nWarpId; i += 1) {
if (pSourceVisArray[i] === pLevel.nLevelId) {
nSourceVisCount += 1
}
}
let nDestinationVisCount = 0
for (let i = 0; i < 8; i += 1) {
if (pDestinationVisArray[i] === pDrlgRoom.pLevel.nLevelId) {
nDestinationVisCount += 1
}
if (nSourceVisCount === nDestinationVisCount) {
const pNearRoom = pLevel.pFirstRoomEx
const nWarpDest = DRLGWARP_GetWarpDestinationFromArray(pLevel, i)
if (nWarpDest === nWarpId && sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) {
if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) {
bSkip = true
break
}
@ -529,8 +512,7 @@ export function sub_6FD77BB0(pDrlgRoom: D2DrlgRoomStrc): void {
for (let i = 0; i < 8; i += 1) {
if (pDestinationVisArray[i] === pDrlgRoom.pLevel.nLevelId) {
const pNearRoom = pLevel.pFirstRoomEx
const nWarpDest = DRLGWARP_GetWarpDestinationFromArray(pLevel, i)
if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDest)) {
if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) {
break
}
}

View File

@ -274,10 +274,9 @@ function DUNGEON_GameTileToClientCoords(nX: number, nY: number): { nX: number; n
return { nX: Math.imul(80, (nX - nY) | 0), nY: (Math.imul(80, (nX + nY) | 0) / 2) | 0 }
}
// MONSTERS_ValidateMonsterId (D2Common Monsters.cpp) needs the MonStats table logic, which the DRLG port
// does not include; the native oracle stubs it as a hard failure too. Only reached for Act V levels.
function MONSTERS_ValidateMonsterId(nMonsterId: number): number {
throw new Error(`MONSTERS_ValidateMonsterId(${nMonsterId}) is not ported (Act V tile preset units)`)
// MONSTERS_ValidateMonsterId (D2Common Monsters.cpp)
function MONSTERS_ValidateMonsterId(pDrlgRoom: D2DrlgRoomStrc, nMonsterId: number): number {
return nMonsterId >= 0 && nMonsterId < pDrlgRoom.pLevel.pDrlg.env.tables.nMonStatsTxtRecordCount ? nMonsterId : -1
}
// ---------------------------------------------------------------------------------------------
@ -649,7 +648,7 @@ export function DRLGROOMTILE_AddTilePresetUnits(
DRLGROOM_AllocPresetUnit(
pDrlgRoom,
UNIT_MONSTER,
MONSTERS_ValidateMonsterId(nUnitId),
MONSTERS_ValidateMonsterId(pDrlgRoom, nUnitId),
MONMODE_NEUTRAL,
nPosX,
nPosY,
@ -936,8 +935,10 @@ export function DRLGROOMTILE_LoadInitRoomTiles(
pTileCache,
nTileType,
)
// D2_VERSION_HAS_UBERS is not defined for 1.13c.
if (nTileType === TILETYPE_WALL_RIGHT_EXIT || nTileType === TILETYPE_WALL_LEFT_EXIT) {
if (
(nTileType === TILETYPE_WALL_RIGHT_EXIT || nTileType === TILETYPE_WALL_LEFT_EXIT) &&
pDrlgRoom.pLevel.nLevelId !== 133
) {
DRLGROOMTILE_LoadWallWarpTiles(pDrlgRoom, pWallTileData, nTileInformation, nTileType)
}
}
@ -1243,7 +1244,7 @@ const nRemapIndices: readonly number[] = [
3, // [TILETYPE_WALL_TOP_RIGHT ]
4, // [TILETYPE_WALL_BOTTOM_LEFT ]
5, // [TILETYPE_WALL_BOTTOM_RIGHT ]
-2, -2, -1, -1, -1, -2, -1, -1, -1, -1, -1,
-2, -2, -1, -1, -1, -2, -1, -1, -1, -1, -1, 0, 0,
]
//D2Common.0x6FDD1390
@ -1270,7 +1271,7 @@ export function DRLGROOMTILE_LinkedTileDataManager(
): void {
const nTileInformation = nPackedTileInformation >>> 0
if (nTileType < 0 || nTileType >= nRemapIndices.length) {
throw new RangeError(`DRLGROOMTILE_LinkedTileDataManager: nRemapIndices[${nTileType}] is outside the 19-entry table`)
throw new RangeError(`DRLGROOMTILE_LinkedTileDataManager: nRemapIndices[${nTileType}] is outside the table`)
}
const v10 = nRemapIndices[nTileType]!

View File

@ -144,7 +144,7 @@ export const FLAG_OPERATION_OVERWRITE_IF_ZERO = 4
export const FLAG_OPERATION_AND_NEGATED = 5
export type FlagOperation = 0 | 1 | 2 | 3 | 4 | 5
/** D2C_Levels (DataTbls/LevelsIds.h), the ids the Act I DRLG code refers to. */
/** D2C_Levels (DataTbls/LevelsIds.h). */
export const LEVEL_NONE = 0
export const LEVEL_ROGUEENCAMPMENT = 1
export const LEVEL_BLOODMOOR = 2
@ -166,14 +166,122 @@ export const LEVEL_BURIALGROUNDS = 17
export const LEVEL_CRYPT = 18
export const LEVEL_MAUSOLEUM = 19
export const LEVEL_FORGOTTENTOWER = 20
export const LEVEL_TOWERCELLARLEV1 = 21
export const LEVEL_TOWERCELLARLEV2 = 22
export const LEVEL_TOWERCELLARLEV3 = 23
export const LEVEL_TOWERCELLARLEV4 = 24
export const LEVEL_TOWERCELLARLEV5 = 25
export const LEVEL_MONASTERYGATE = 26
export const LEVEL_OUTERCLOISTER = 27
export const LEVEL_BARRACKS = 28
export const LEVEL_JAILLEV1 = 29
export const LEVEL_JAILLEV2 = 30
export const LEVEL_JAILLEV3 = 31
export const LEVEL_INNERCLOISTER = 32
export const LEVEL_CATHEDRAL = 33
export const LEVEL_CATACOMBSLEV1 = 34
export const LEVEL_CATACOMBSLEV2 = 35
export const LEVEL_CATACOMBSLEV3 = 36
export const LEVEL_CATACOMBSLEV4 = 37
export const LEVEL_TRISTRAM = 38
export const LEVEL_MOOMOOFARM = 39
export const LEVEL_LUTGHOLEIN = 40
export const LEVEL_ROCKYWASTE = 41
export const LEVEL_DRYHILLS = 42
export const LEVEL_FAROASIS = 43
export const LEVEL_LOSTCITY = 44
export const LEVEL_VALLEYOFSNAKES = 45
export const LEVEL_CANYONOFTHEMAGI = 46
export const LEVEL_SEWERSLEV1 = 47
export const LEVEL_SEWERSLEV2 = 48
export const LEVEL_SEWERSLEV3 = 49
export const LEVEL_HAREMLEV1 = 50
export const LEVEL_HAREMLEV2 = 51
export const LEVEL_PALACECELLARLEV1 = 52
export const LEVEL_PALACECELLARLEV2 = 53
export const LEVEL_PALACECELLARLEV3 = 54
export const LEVEL_STONYTOMBLEV1 = 55
export const LEVEL_HALLSOFTHEDEADLEV1 = 56
export const LEVEL_HALLSOFTHEDEADLEV2 = 57
export const LEVEL_CLAWVIPERTEMPLELEV1 = 58
export const LEVEL_STONYTOMBLEV2 = 59
export const LEVEL_HALLSOFTHEDEADLEV3 = 60
export const LEVEL_CLAWVIPERTEMPLELEV2 = 61
export const LEVEL_MAGGOTLAIRLEV1 = 62
export const LEVEL_MAGGOTLAIRLEV2 = 63
export const LEVEL_MAGGOTLAIRLEV3 = 64
export const LEVEL_ANCIENTTUNNELS = 65
export const LEVEL_TALRASHASTOMB1 = 66
export const LEVEL_TALRASHASTOMB2 = 67
export const LEVEL_TALRASHASTOMB3 = 68
export const LEVEL_TALRASHASTOMB4 = 69
export const LEVEL_TALRASHASTOMB5 = 70
export const LEVEL_TALRASHASTOMB6 = 71
export const LEVEL_TALRASHASTOMB7 = 72
export const LEVEL_DURIELSLAIR = 73
export const LEVEL_ARCANESANCTUARY = 74
export const LEVEL_KURASTDOCKTOWN = 75
export const LEVEL_SPIDERFOREST = 76
export const LEVEL_GREATMARSH = 77
export const LEVEL_FLAYERJUNGLE = 78
export const LEVEL_LOWERKURAST = 79
export const LEVEL_KURASTBAZAAR = 80
export const LEVEL_UPPERKURAST = 81
export const LEVEL_KURASTCAUSEWAY = 82
export const LEVEL_TRAVINCAL = 83
export const LEVEL_SPIDERCAVE = 84
export const LEVEL_SPIDERCAVERN = 85
export const LEVEL_SWAMPYPITLEV1 = 86
export const LEVEL_SWAMPYPITLEV2 = 87
export const LEVEL_FLAYERDUNGEONLEV1 = 88
export const LEVEL_FLAYERDUNGEONLEV2 = 89
export const LEVEL_SWAMPYPITLEV3 = 90
export const LEVEL_FLAYERDUNGEONLEV3 = 91
export const LEVEL_SEWERSA3LEV1 = 92
export const LEVEL_SEWERSA3LEV2 = 93
export const LEVEL_RUINEDTEMPLE = 94
export const LEVEL_DISUSEDFANE = 95
export const LEVEL_FORGOTTENRELIQUARY = 96
export const LEVEL_FORGOTTENTEMPLE = 97
export const LEVEL_RUINEDFANE = 98
export const LEVEL_DISUSEDRELIQUARY = 99
export const LEVEL_DURANCEOFHATELEV1 = 100
export const LEVEL_DURANCEOFHATELEV2 = 101
export const LEVEL_DURANCEOFHATELEV3 = 102
export const LEVEL_THEPANDEMONIUMFORTRESS = 103
export const LEVEL_OUTERSTEPPES = 104
export const LEVEL_PLAINSOFDESPAIR = 105
export const LEVEL_CITYOFTHEDAMNED = 106
export const LEVEL_RIVEROFFLAME = 107
export const LEVEL_CHAOSSANCTUM = 108
export const LEVEL_HARROGATH = 109
export const LEVEL_BLOODYFOOTHILLS = 110
export const LEVEL_ID_ACT5_BARRICADE_1 = 111
export const LEVEL_ARREATPLATEAU = 112
export const LEVEL_CRYSTALIZEDCAVERNLEV1 = 113
export const LEVEL_CELLAROFPITY = 114
export const LEVEL_CRYSTALIZEDCAVERNLEV2 = 115
export const LEVEL_ECHOCHAMBER = 116
export const LEVEL_TUNDRAWASTELANDS = 117
export const LEVEL_GLACIALCAVESLEV1 = 118
export const LEVEL_GLACIALCAVESLEV2 = 119
export const LEVEL_ROCKYSUMMIT = 120
export const LEVEL_NIHLATHAKSTEMPLE = 121
export const LEVEL_HALLSOFANGUISH = 122
export const LEVEL_HALLSOFDEATHSCALLING = 123
export const LEVEL_HALLSOFVAUGHT = 124
export const LEVEL_HELL1 = 125
export const LEVEL_HELL2 = 126
export const LEVEL_HELL3 = 127
export const LEVEL_THEWORLDSTONEKEEPLEV1 = 128
export const LEVEL_THEWORLDSTONEKEEPLEV2 = 129
export const LEVEL_THEWORLDSTONEKEEPLEV3 = 130
export const LEVEL_THRONEOFDESTRUCTION = 131
export const LEVEL_THEWORLDSTONECHAMBER = 132
export const LEVEL_PANDEMONIUMRUN1 = 133
export const LEVEL_PANDEMONIUMRUN2 = 134
export const LEVEL_PANDEMONIUMRUN3 = 135
export const LEVEL_PANDEMONIUMFINALE = 136
// ---------------------------------------------------------------------------------------------
// Structures
@ -591,6 +699,8 @@ export interface D2DrlgLevelStrc {
/** 0x21C: file round robin of DRLGOUTDOORS_SpawnOutdoorLevelPresetEx (nPickedFile == -1). */
pBuild: D2DrlgBuildStrc | null
pPresetMaps: Int32Array | null
pJungleDefs: Int32Array | null
nJungleDefs: number
pNextLevel: D2DrlgLevelStrc | null
}

View File

@ -0,0 +1,188 @@
/**
* Act II outdoor DRLG (#496): structural and gameplay invariants across levels 41-46 over many seeds:
* - sizes: every Act II outdoor level gets its Levels.txt SizeX/SizeY across 128 layouts;
* - staff/boss tomb rolls: nStaffTombLevel and nBossTombLevel in 66..72 and distinct;
* - population: 1 waypoint on levels 42, 43, 44, 46 and 5 shrine cells on levels 41..45;
* - links & warps: seamless orth links along shared edges of 40 <-> 41 <-> 42 <-> 43 <-> 44 <-> 45,
* plus dungeon warps (41 -> 55, 42 -> 56, 43 -> 62, 44 -> 65, 45 -> 58, 46 -> 66..72);
* - determinism and seed distinctness.
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { dumpAct1, runAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
const LEVELS = [41, 42, 43, 44, 45, 46]
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name)))
function lcgSeeds(start: number, count: number): number[] {
const out: number[] = []
let seed = start
for (let i = 0; i < count; i += 1) {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
out.push(seed)
}
return out
}
const BAKE_SEEDS = [0x5eed0200, 0x5eed0201, 0x5eed0202]
const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x2badf00d, 8)]
const LAYOUT_SEEDS = lcgSeeds(0x13579bdf, 128)
const WAYPOINT_LEVELS = new Set([42, 43, 44, 46])
// Level 46 (Canyon of the Magi) gets its waypoint from its tomb presets in grid 0; 42..44 use grid 1.
const GRID1_WAYPOINT_LEVELS = new Set([42, 43, 44])
// DRLGOUTDESR_InitAct2OutdoorLevel (DrlgOutDesr.cpp): all Act 2 outdoor levels except 45 (Valley of Snakes) and 134 get 5 shrines.
const SHRINE_LEVELS = new Set([41, 42, 43, 44, 46])
const SHRINES_PER_LEVEL = 5
const GRID1_WAYPOINT = 0x30000
const GRID1_SHRINE = 0xf000
const SUBCLASS_WAYPOINT = 0x40
const UNIT_OBJECT = 2
const EXPECTED_WARPS: Readonly<Record<number, readonly number[]>> = {
41: [55],
42: [56],
43: [62],
44: [65],
45: [58],
46: [66, 67, 68, 69, 70, 71, 72],
}
const EXPECTED_ORTHS: Readonly<Record<number, readonly number[]>> = {
41: [40, 42],
42: [41, 43],
43: [42, 44],
44: [43, 45],
45: [44],
46: [],
}
const numeric = (a: number, b: number): number => a - b
function hex(seed: number): string {
return `0x${seed.toString(16).padStart(8, '0')}`
}
function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean {
const [ax, ay, aw, ah] = a
const [bx, by, bw, bh] = b
// ALTDIR_WEST=0, ALTDIR_NORTH=1, ALTDIR_EAST=2, ALTDIR_SOUTH=3
if (dir === 0 || dir === 2) {
const touch = dir === 2 ? ax + aw === bx : bx + bw === ax
return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by)
}
const touch = dir === 3 ? ay + ah === by : by + bh === ay
return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx)
}
describe.skipIf(!hasMpq)('Act II outdoor DRLG port: level invariants over many seeds', () => {
let data: DrlgMpqData
beforeAll(async () => {
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 2)
}, 120_000)
function generate(seed: number): DrlgDump {
return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false })
}
it(`sizes every level and rolls distinct staff/boss tombs on ${LAYOUT_SEEDS.length} act layouts`, () => {
const env = createDrlgEnv(data.source, data.tables)
for (const seed of LAYOUT_SEEDS) {
const run = runAct1(env, seed, [], { act: 1 })
expect(run.pDrlg.nStaffTombLevel).toBeGreaterThanOrEqual(66)
expect(run.pDrlg.nStaffTombLevel).toBeLessThanOrEqual(72)
expect(run.pDrlg.nBossTombLevel).toBeGreaterThanOrEqual(66)
expect(run.pDrlg.nBossTombLevel).toBeLessThanOrEqual(72)
expect(run.pDrlg.nBossTombLevel).not.toBe(run.pDrlg.nStaffTombLevel)
const { act } = dumpAct1(env, seed, LEVELS, { actOnly: true })
for (const levelId of LEVELS) {
const level = act.find(l => l.id === levelId)
expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined()
const [, , width, height] = level!.coord
const def = DATATBLS_GetLevelDefRecord(data.tables, levelId)
expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]])
}
}
}, 60_000)
it('generates the same act from the same seed', () => {
expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!)))
}, 60_000)
it('generates a different level from every seed', () => {
const env = createDrlgEnv(data.source, data.tables)
const grids = new Map<number, Set<string>>(LEVELS.map(id => [id, new Set<string>()]))
for (const seed of SEEDS) {
const dump = dumpAct1(env, seed, LEVELS, { difficulty: 0, isolated: false, skipActivation: true })
for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids))
}
for (const [levelId, distinct] of grids) {
// Level 45 (Valley of Snakes) is a fixed 4x4 grid (32x32) with only the Claw Viper Temple preset
// and border links (3 link orientations), whereas 41..44 and 46 are full desert levels.
if (levelId === 45) {
expect(distinct.size, `level ${levelId}`).toBeGreaterThanOrEqual(2)
} else {
expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length)
}
}
}, 60_000)
for (const seed of SEEDS) {
it(`seed ${hex(seed)}: waypoints, shrines, seamless orth links, and dungeon warps`, () => {
const dump = generate(seed)
const act = new Map(dump.act.map(l => [l.id, l]))
for (const levelId of LEVELS) {
const what = `${hex(seed)} level ${levelId}`
const levelDump = dump.levels.find(l => l.id === levelId)!
const input = drlgLevelInputFromDump(dump, levelId, data.tables)
const layout = act.get(levelId)!.coord
// Waypoint & shrine cells in grid 1
const cells = levelDump.levelGrid.grids[1]!.cells
expect(cells.filter(c => (c & GRID1_WAYPOINT) !== 0).length, `${what}: waypoint cells`).toBe(
GRID1_WAYPOINT_LEVELS.has(levelId) ? 1 : 0,
)
expect(cells.filter(c => (c & GRID1_SHRINE) !== 0).length, `${what}: shrine cells`).toBe(
SHRINE_LEVELS.has(levelId) ? SHRINES_PER_LEVEL : 0,
)
const objects = input.units.filter(
u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length,
)
const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0)
expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_LEVELS.has(levelId) ? 1 : 0)
// Seamless orth links
expect(input.orths.map(o => o.level).sort(numeric), `${what}: orth neighbours`).toEqual(
[...EXPECTED_ORTHS[levelId]!].sort(numeric),
)
for (const orth of input.orths) {
const neighbour = act.get(orth.level)!
expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe(
true,
)
}
// Dungeon warps
expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual(
[...EXPECTED_WARPS[levelId]!].sort(numeric),
)
}
}, 60_000)
}
})

View File

@ -0,0 +1,107 @@
/**
* Act II outdoor DRLG (#496): bit-exact parity of the TypeScript port (src/game/drlg) with the native
* D2MOO oracle (tools/d2moo-oracle) across all Act II outdoor levels (41-46).
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
import {
firstDiff,
firstTraceDivergence,
formatDiff,
fsDrlgSource,
ORACLE_BIN,
ORACLE_DATA,
runOracle,
} from '../scripts/lib/drlg-oracle.ts'
const ACT2_OUTDOOR_LEVELS = [41, 42, 43, 44, 45, 46]
const SEEDS = [0x12345678, 0x5eed0200, 0x5eed0201, 0x5eed0202]
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
function hexSeed(seed: number): string {
return seed.toString(16).padStart(8, '0')
}
describe('Act II outdoor DRLG 1:1 parity against native D2MOO oracle', () => {
let data: DrlgMpqData
beforeAll(async () => {
const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name)))
if (missing.length > 0) {
throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`)
}
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 2)
}, 120_000)
for (const seed of SEEDS) {
const hex = hexSeed(seed)
it.skipIf(!existsSync(ORACLE_BIN))(
`seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`,
() => {
const oracleRun = runOracle({
seed,
levels: ACT2_OUTDOOR_LEVELS,
difficulty: 0,
isolated: false,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT2_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: false,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
for (const levelId of ACT2_OUTDOOR_LEVELS) {
const input = drlgLevelInputFromDump(actual, levelId, data.tables)
expect(input.tiles.length).toBeGreaterThan(0)
}
},
60_000,
)
}
it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => {
const seed = 0x12345678
const oracleRun = runOracle({
seed,
levels: ACT2_OUTDOOR_LEVELS,
difficulty: 0,
isolated: true,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const source = fsDrlgSource(ORACLE_DATA)
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT2_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: true,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
}, 60_000)
})

View File

@ -0,0 +1,165 @@
/**
* Act III outdoor DRLG (#499): structural and gameplay invariants across levels 76-83 over many seeds:
* - sizes: jungle levels 76-78 have 48x(24*k) subdivisions, Kurast levels 79-83 match Levels.txt SizeX/SizeY;
* - connected act graph: 75..83 form a single connected outdoor graph with mutual touching orth links;
* - jungle population: 1 waypoint cell and 5 shrine cells on jungle levels 76..78, plus waypoint objects
* on 76, 77, 78, 79, 80, 81, 83;
* - dungeon warps on 76, 78, 80, 81, 82, 83;
* - determinism and seed distinctness.
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { LVLPREST_ACT3_JUNGLE_HEAD, LVLPREST_ACT3_JUNGLE_TAIL } from '../src/game/drlg/drlg-ids.ts'
import { dumpAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
const LEVELS = [76, 77, 78, 79, 80, 81, 82, 83]
const JUNGLE_LEVELS = new Set([76, 77, 78])
const KURAST_LEVELS = [79, 80, 81, 82, 83]
const WAYPOINT_OBJECT_LEVELS = new Set([76, 77, 78, 79, 80, 81, 83])
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name)))
function lcgSeeds(start: number, count: number): number[] {
const out: number[] = []
let seed = start
for (let i = 0; i < count; i += 1) {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
out.push(seed)
}
return out
}
const BAKE_SEEDS = [0x5eed0300, 0x5eed0301, 0x5eed0302]
const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x3badf00d, 4)]
const LAYOUT_SEEDS = lcgSeeds(0x2468ace1, 128)
const SUBCLASS_WAYPOINT = 0x40
const UNIT_OBJECT = 2
const EXPECTED_WARPS: Readonly<Record<number, readonly number[]>> = {
76: [84, 85],
77: [],
78: [86, 88],
79: [],
80: [92, 94, 95],
81: [92, 96, 97],
82: [98, 99],
83: [100],
}
const numeric = (a: number, b: number): number => a - b
function hex(seed: number): string {
return `0x${seed.toString(16).padStart(8, '0')}`
}
function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean {
const [ax, ay, aw, ah] = a
const [bx, by, bw, bh] = b
if (dir === 0 || dir === 2) {
const touch = dir === 2 ? ax + aw === bx : bx + bw === ax
return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by)
}
const touch = dir === 3 ? ay + ah === by : by + bh === ay
return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx)
}
describe.skipIf(!hasMpq)('Act III outdoor DRLG port: level invariants over many seeds', () => {
let data: DrlgMpqData
beforeAll(async () => {
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 3)
}, 120_000)
function generate(seed: number): DrlgDump {
return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false })
}
it(`sizes jungle and Kurast levels properly on ${LAYOUT_SEEDS.length} act layouts`, () => {
const env = createDrlgEnv(data.source, data.tables)
for (const seed of LAYOUT_SEEDS) {
const { act } = dumpAct1(env, seed, LEVELS, { actOnly: true })
for (const levelId of LEVELS) {
const level = act.find(l => l.id === levelId)
expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined()
const [, , width, height] = level!.coord
if (JUNGLE_LEVELS.has(levelId)) {
expect(width % 16, `${hex(seed)} level ${levelId} width multiple of 16`).toBe(0)
expect(width, `${hex(seed)} level ${levelId} width >= 32`).toBeGreaterThanOrEqual(32)
expect(height % 24, `${hex(seed)} level ${levelId} height multiple of 24`).toBe(0)
expect(height, `${hex(seed)} level ${levelId} height >= 72`).toBeGreaterThanOrEqual(72)
}
}
for (const levelId of KURAST_LEVELS) {
const level = act.find(l => l.id === levelId)!
const [, , width, height] = level.coord
const def = DATATBLS_GetLevelDefRecord(data.tables, levelId)
expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]])
}
}
}, 60_000)
it('generates the same act from the same seed', () => {
expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!)))
}, 60_000)
it('generates a different jungle layout across seeds', () => {
const env = createDrlgEnv(data.source, data.tables)
const grids = new Map<number, Set<string>>([76, 77, 78].map(id => [id, new Set<string>()]))
for (const seed of SEEDS) {
const dump = dumpAct1(env, seed, [76, 77, 78], { difficulty: 0, isolated: false, skipActivation: true })
for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids))
}
for (const [levelId, distinct] of grids) expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length)
}, 60_000)
for (const seed of SEEDS) {
it(`seed ${hex(seed)}: waypoints, shrines, seamless orth links, and dungeon warps`, () => {
const dump = generate(seed)
const act = new Map(dump.act.map(l => [l.id, l]))
for (const levelId of LEVELS) {
const what = `${hex(seed)} level ${levelId}`
const levelDump = dump.levels.find(l => l.id === levelId)!
const input = drlgLevelInputFromDump(dump, levelId, data.tables)
const layout = act.get(levelId)!.coord
const cells0 = levelDump.levelGrid.grids[0]!.cells
if (levelId === 76) {
expect(cells0.includes(LVLPREST_ACT3_JUNGLE_HEAD), `${what}: jungle head preset`).toBe(true)
} else if (levelId === 78) {
expect(cells0.includes(LVLPREST_ACT3_JUNGLE_TAIL), `${what}: jungle tail preset`).toBe(true)
}
const objects = input.units.filter(
u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length,
)
const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0)
expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_OBJECT_LEVELS.has(levelId) ? 1 : 0)
expect(input.orths.length, `${what}: has orth neighbours`).toBeGreaterThan(0)
for (const orth of input.orths) {
const neighbour = act.get(orth.level)!
expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe(
true,
)
}
expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual(
[...EXPECTED_WARPS[levelId]!].sort(numeric),
)
}
}, 60_000)
}
})

View File

@ -0,0 +1,107 @@
/**
* Act III outdoor DRLG (#499): bit-exact parity of the TypeScript port (src/game/drlg) with the native
* D2MOO oracle (tools/d2moo-oracle) across all Act III outdoor levels (76-83).
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
import {
firstDiff,
firstTraceDivergence,
formatDiff,
fsDrlgSource,
ORACLE_BIN,
ORACLE_DATA,
runOracle,
} from '../scripts/lib/drlg-oracle.ts'
const ACT3_OUTDOOR_LEVELS = [76, 77, 78, 79, 80, 81, 82, 83]
const SEEDS = [0x12345678, 0x5eed0300, 0x5eed0301, 0x5eed0302]
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
function hexSeed(seed: number): string {
return seed.toString(16).padStart(8, '0')
}
describe('Act III outdoor DRLG 1:1 parity against native D2MOO oracle', () => {
let data: DrlgMpqData
beforeAll(async () => {
const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name)))
if (missing.length > 0) {
throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`)
}
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 3)
}, 120_000)
for (const seed of SEEDS) {
const hex = hexSeed(seed)
it.skipIf(!existsSync(ORACLE_BIN))(
`seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`,
() => {
const oracleRun = runOracle({
seed,
levels: ACT3_OUTDOOR_LEVELS,
difficulty: 0,
isolated: false,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT3_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: false,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
for (const levelId of ACT3_OUTDOOR_LEVELS) {
const input = drlgLevelInputFromDump(actual, levelId, data.tables)
expect(input.tiles.length).toBeGreaterThan(0)
}
},
60_000,
)
}
it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => {
const seed = 0x12345678
const oracleRun = runOracle({
seed,
levels: ACT3_OUTDOOR_LEVELS,
difficulty: 0,
isolated: true,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const source = fsDrlgSource(ORACLE_DATA)
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT3_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: true,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
}, 60_000)
})

View File

@ -0,0 +1,165 @@
/**
* Act IV outdoor DRLG (#497): structural and gameplay invariants across levels 104-106,108 over many seeds:
* - sizes: 104/105 get 96x56 or 56x96 (rand-transposed), 106/108 match Levels.txt SizeX/SizeY;
* - population: 1 waypoint on level 106 and 2 shrine cells on levels 104..106;
* - links & warps: seamless orth links along shared edges of 103 <-> 104 <-> 105 <-> 106,
* plus warps to 107 (River of Flame) on 106 and 108;
* - determinism and seed distinctness.
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import {
LVLPREST_ACT4_DIABLO_HEART,
LVLPREST_ACT4_MESA_2_IZUAL,
LVLPREST_ACT4_MESA_WARP,
} from '../src/game/drlg/drlg-ids.ts'
import { dumpAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
const LEVELS = [104, 105, 106, 108]
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name)))
function lcgSeeds(start: number, count: number): number[] {
const out: number[] = []
let seed = start
for (let i = 0; i < count; i += 1) {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
out.push(seed)
}
return out
}
const BAKE_SEEDS = [0x5eed0400, 0x5eed0401, 0x5eed0402]
const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x4badf00d, 4)]
const LAYOUT_SEEDS = lcgSeeds(0x3579bdf1, 128)
const WAYPOINT_LEVELS = new Set([106])
const SUBCLASS_WAYPOINT = 0x40
const UNIT_OBJECT = 2
const EXPECTED_WARPS: Readonly<Record<number, readonly number[]>> = {
104: [],
105: [],
106: [107],
108: [],
}
const EXPECTED_ORTHS: Readonly<Record<number, readonly number[]>> = {
104: [103, 105],
105: [104, 106],
106: [105],
108: [],
}
const numeric = (a: number, b: number): number => a - b
function hex(seed: number): string {
return `0x${seed.toString(16).padStart(8, '0')}`
}
function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean {
const [ax, ay, aw, ah] = a
const [bx, by, bw, bh] = b
if (dir === 0 || dir === 2) {
const touch = dir === 2 ? ax + aw === bx : bx + bw === ax
return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by)
}
const touch = dir === 3 ? ay + ah === by : by + bh === ay
return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx)
}
describe.skipIf(!hasMpq)('Act IV outdoor DRLG port: level invariants over many seeds', () => {
let data: DrlgMpqData
beforeAll(async () => {
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 4)
}, 120_000)
function generate(seed: number): DrlgDump {
return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false })
}
it(`sizes every Act IV outdoor level properly on ${LAYOUT_SEEDS.length} act layouts`, () => {
const env = createDrlgEnv(data.source, data.tables)
for (const seed of LAYOUT_SEEDS) {
const { act } = dumpAct1(env, seed, LEVELS, { actOnly: true })
for (const levelId of LEVELS) {
const level = act.find(l => l.id === levelId)
expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined()
const [, , width, height] = level!.coord
const def = DATATBLS_GetLevelDefRecord(data.tables, levelId)
expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]])
}
}
}, 60_000)
it('generates the same act from the same seed', () => {
expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!)))
}, 60_000)
it('generates a different outdoor layout across seeds', () => {
const env = createDrlgEnv(data.source, data.tables)
const grids = new Map<number, Set<string>>([104, 105, 106].map(id => [id, new Set<string>()]))
for (const seed of SEEDS) {
const dump = dumpAct1(env, seed, [104, 105, 106], { difficulty: 0, isolated: false, skipActivation: true })
for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids))
}
for (const [levelId, distinct] of grids) expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length)
}, 60_000)
for (const seed of SEEDS) {
it(`seed ${hex(seed)}: waypoints, key presets, seamless orth links, and dungeon warps`, () => {
const dump = generate(seed)
const act = new Map(dump.act.map(l => [l.id, l]))
for (const levelId of LEVELS) {
const what = `${hex(seed)} level ${levelId}`
const levelDump = dump.levels.find(l => l.id === levelId)!
const input = drlgLevelInputFromDump(dump, levelId, data.tables)
const cells0 = levelDump.levelGrid.grids[0]!.cells
if (levelId === 105) {
expect(cells0.includes(LVLPREST_ACT4_MESA_2_IZUAL), `${what}: Izual preset`).toBe(true)
} else if (levelId === 106) {
expect(cells0.includes(LVLPREST_ACT4_MESA_WARP), `${what}: Mesa warp preset`).toBe(true)
} else if (levelId === 108) {
expect(cells0.includes(LVLPREST_ACT4_DIABLO_HEART), `${what}: Chaos Sanctuary heart preset`).toBe(true)
}
const objects = input.units.filter(
u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length,
)
const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0)
expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_LEVELS.has(levelId) ? 1 : 0)
expect(input.orths.map(o => o.level).sort(numeric), `${what}: orth neighbours`).toEqual(
[...EXPECTED_ORTHS[levelId]!].sort(numeric),
)
if (act.has(levelId)) {
const layout = act.get(levelId)!.coord
for (const orth of input.orths) {
const neighbour = act.get(orth.level)!
expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe(
true,
)
}
}
expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual(
[...EXPECTED_WARPS[levelId]!].sort(numeric),
)
}
}, 60_000)
}
})

View File

@ -0,0 +1,107 @@
/**
* Act IV outdoor DRLG (#497): bit-exact parity of the TypeScript port (src/game/drlg) with the native
* D2MOO oracle (tools/d2moo-oracle) across all Act IV outdoor levels (104-106, 108).
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
import {
firstDiff,
firstTraceDivergence,
formatDiff,
fsDrlgSource,
ORACLE_BIN,
ORACLE_DATA,
runOracle,
} from '../scripts/lib/drlg-oracle.ts'
const ACT4_OUTDOOR_LEVELS = [104, 105, 106, 108]
const SEEDS = [0x12345678, 0x5eed0400, 0x5eed0401, 0x5eed0402]
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
function hexSeed(seed: number): string {
return seed.toString(16).padStart(8, '0')
}
describe('Act IV outdoor DRLG 1:1 parity against native D2MOO oracle', () => {
let data: DrlgMpqData
beforeAll(async () => {
const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name)))
if (missing.length > 0) {
throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`)
}
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 4)
}, 120_000)
for (const seed of SEEDS) {
const hex = hexSeed(seed)
it.skipIf(!existsSync(ORACLE_BIN))(
`seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`,
() => {
const oracleRun = runOracle({
seed,
levels: ACT4_OUTDOOR_LEVELS,
difficulty: 0,
isolated: false,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT4_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: false,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
for (const levelId of ACT4_OUTDOOR_LEVELS) {
const input = drlgLevelInputFromDump(actual, levelId, data.tables)
expect(input.tiles.length).toBeGreaterThan(0)
}
},
60_000,
)
}
it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => {
const seed = 0x12345678
const oracleRun = runOracle({
seed,
levels: ACT4_OUTDOOR_LEVELS,
difficulty: 0,
isolated: true,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const source = fsDrlgSource(ORACLE_DATA)
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT4_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: true,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
}, 60_000)
})

View File

@ -0,0 +1,184 @@
/**
* Act V outdoor DRLG (#498): structural and gameplay invariants across levels 110-112,117,134 over many seeds:
* - sizes: 110-112 match Levels.txt SizeX/SizeY across 128 act layouts, 117/134 match Levels.txt SizeX/SizeY;
* - waypoints: 1 waypoint object on levels 111, 112, 117;
* - links & warps: seamless orth links along shared edges of 109 <-> 110 <-> 111 <-> 112,
* plus dungeon warps (111 -> 113,125; 112 -> 115,126; 117 -> 118,127);
* - determinism and seed distinctness (including Level 134 Forgotten Sands routed via Act II desert DRLG).
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import {
LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N,
LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W,
} from '../src/game/drlg/drlg-ids.ts'
import { dumpAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
const LEVELS = [110, 111, 112, 117, 134]
const ACT_OUTDOOR_LEVELS = [110, 111, 112]
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name)))
function lcgSeeds(start: number, count: number): number[] {
const out: number[] = []
let seed = start
for (let i = 0; i < count; i += 1) {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
out.push(seed)
}
return out
}
const BAKE_SEEDS = [0x5eed0500, 0x5eed0501, 0x5eed0502]
const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x5badf00d, 4)]
const LAYOUT_SEEDS = lcgSeeds(0x468ace13, 128)
const BARRICADE_LEVELS = new Set([111, 112, 117])
const WAYPOINT_LEVELS = new Set([111, 112, 117, 134])
const SUBCLASS_WAYPOINT = 0x40
const UNIT_OBJECT = 2
const EXPECTED_WARPS: Readonly<Record<number, readonly number[]>> = {
110: [],
111: [],
112: [113],
117: [115, 118],
134: [],
}
const EXPECTED_ORTHS: Readonly<Record<number, readonly number[]>> = {
110: [],
111: [112],
112: [111],
117: [],
134: [],
}
const numeric = (a: number, b: number): number => a - b
function hex(seed: number): string {
return `0x${seed.toString(16).padStart(8, '0')}`
}
function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean {
const [ax, ay, aw, ah] = a
const [bx, by, bw, bh] = b
if (dir === 0 || dir === 2) {
const touch = dir === 2 ? ax + aw === bx : bx + bw === ax
return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by)
}
const touch = dir === 3 ? ay + ah === by : by + bh === ay
return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx)
}
describe.skipIf(!hasMpq)('Act V outdoor DRLG port: level invariants over many seeds', () => {
let data: DrlgMpqData
beforeAll(async () => {
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 5)
}, 120_000)
function generate(seed: number): DrlgDump {
return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false })
}
it(`sizes every Act V outdoor level properly on ${LAYOUT_SEEDS.length} act layouts`, () => {
const env = createDrlgEnv(data.source, data.tables)
for (const seed of LAYOUT_SEEDS) {
const { act } = dumpAct1(env, seed, ACT_OUTDOOR_LEVELS, { actOnly: true })
for (const levelId of ACT_OUTDOOR_LEVELS) {
const level = act.find(l => l.id === levelId)
expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined()
const [, , width, height] = level!.coord
if (BARRICADE_LEVELS.has(levelId)) {
expect([width, height].sort(numeric), `${hex(seed)} level ${levelId}`).toEqual([64, 160])
} else {
const def = DATATBLS_GetLevelDefRecord(data.tables, levelId)
expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]])
}
}
}
}, 60_000)
it('generates the same act from the same seed', () => {
expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!)))
}, 60_000)
it('generates a different outdoor layout across seeds', () => {
const env = createDrlgEnv(data.source, data.tables)
const variedLevels = [111, 112, 117, 134]
const grids = new Map<number, Set<string>>(variedLevels.map(id => [id, new Set<string>()]))
for (const seed of SEEDS) {
const dump = dumpAct1(env, seed, variedLevels, { difficulty: 0, isolated: false, skipActivation: true })
for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids))
}
for (const [levelId, distinct] of grids) expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length)
}, 60_000)
for (const seed of SEEDS) {
it(`seed ${hex(seed)}: waypoints, seamless orth links, and dungeon warps`, () => {
const dump = generate(seed)
const act = new Map(dump.act.map(l => [l.id, l]))
for (const levelId of LEVELS) {
const what = `${hex(seed)} level ${levelId}`
const levelDump = dump.levels.find(l => l.id === levelId)!
const input = drlgLevelInputFromDump(dump, levelId, data.tables)
const dims = [levelDump.levelGrid.coord[2] * 8, levelDump.levelGrid.coord[3] * 8]
if (BARRICADE_LEVELS.has(levelId)) {
expect([...dims].sort(numeric), `${what}: level dimensions`).toEqual([64, 160])
} else {
const def = DATATBLS_GetLevelDefRecord(data.tables, levelId)
expect(dims, `${what}: level dimensions`).toEqual([def.dwSizeX[0], def.dwSizeY[0]])
}
const objects = input.units.filter(
u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length,
)
const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0)
expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_LEVELS.has(levelId) ? 1 : 0)
expect(input.orths.map(o => o.level).sort(numeric), `${what}: orth neighbours`).toEqual(
[...EXPECTED_ORTHS[levelId]!].sort(numeric),
)
if (act.has(levelId)) {
const layout = act.get(levelId)!.coord
for (const orth of input.orths) {
const neighbour = act.get(orth.level)!
expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe(
true,
)
}
if (levelId === 110) {
expect(touchesOnDir(layout, act.get(111)!.coord, 0), `${what}: touches 111 on west`).toBe(true)
expect(touchesOnDir(layout, act.get(109)!.coord, 2), `${what}: touches 109 on east`).toBe(true)
}
}
if (BARRICADE_LEVELS.has(levelId)) {
const cells0 = levelDump.levelGrid.grids[0]!.cells
expect(
cells0.includes(LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N) ||
cells0.includes(LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W),
`${what}: Hell portal preset`,
).toBe(true)
}
expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual(
[...EXPECTED_WARPS[levelId]!].sort(numeric),
)
}
}, 60_000)
}
})

View File

@ -0,0 +1,107 @@
/**
* Act V outdoor DRLG (#498): bit-exact parity of the TypeScript port (src/game/drlg) with the native
* D2MOO oracle (tools/d2moo-oracle) across all Act V outdoor levels (110-112, 117, 134).
*/
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts'
import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts'
import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts'
import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts'
import {
firstDiff,
firstTraceDivergence,
formatDiff,
fsDrlgSource,
ORACLE_BIN,
ORACLE_DATA,
runOracle,
} from '../scripts/lib/drlg-oracle.ts'
const ACT5_OUTDOOR_LEVELS = [110, 111, 112, 117, 134]
const SEEDS = [0x12345678, 0x5eed0500, 0x5eed0501, 0x5eed0502]
const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2')
const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const
function hexSeed(seed: number): string {
return seed.toString(16).padStart(8, '0')
}
describe('Act V outdoor DRLG 1:1 parity against native D2MOO oracle', () => {
let data: DrlgMpqData
beforeAll(async () => {
const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name)))
if (missing.length > 0) {
throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`)
}
const archives = new MountedArchives()
for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name))))
data = await loadDrlgMpqData(member => archives.read(member), 5)
}, 120_000)
for (const seed of SEEDS) {
const hex = hexSeed(seed)
it.skipIf(!existsSync(ORACLE_BIN))(
`seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`,
() => {
const oracleRun = runOracle({
seed,
levels: ACT5_OUTDOOR_LEVELS,
difficulty: 0,
isolated: false,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT5_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: false,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
for (const levelId of ACT5_OUTDOOR_LEVELS) {
const input = drlgLevelInputFromDump(actual, levelId, data.tables)
expect(input.tiles.length).toBeGreaterThan(0)
}
},
60_000,
)
}
it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => {
const seed = 0x12345678
const oracleRun = runOracle({
seed,
levels: ACT5_OUTDOOR_LEVELS,
difficulty: 0,
isolated: true,
dataDir: ORACLE_DATA,
trace: true,
})
delete oracleRun.doc.oracle
const source = fsDrlgSource(ORACLE_DATA)
const trace = new DrlgTraceWriter()
const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT5_OUTDOOR_LEVELS, {
difficulty: 0,
isolated: true,
trace,
})
const diff = firstDiff(oracleRun.doc, actual, '$')
if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`)
const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines)
expect(traceDiff).toBeNull()
}, 60_000)
})

View File

@ -25,8 +25,8 @@ fi
# 32-bit so every D2MOO struct has the exact 1.13c layout of the compiled .bin tables.
# -O0/-fwrapv/-fno-strict-aliasing keep MSVC-like integer and aliasing semantics.
FLAGS=(-m32 -std=c++17 -O0 -g -fwrapv -fno-strict-aliasing -fms-extensions -fpermissive
-Wno-invalid-offsetof -w "-DD2ORACLE_D2MOO_COMMIT=\"$PINNED_COMMIT\"")
FLAGS=(-m32 -std=c++17 -O0 -g -fwrapv -fno-strict-aliasing -fPIC -ffunction-sections -fms-extensions -fpermissive
-Wno-invalid-offsetof -w -DD2_VERSION_HAS_UBERS=1 -include DataTbls/LevelsIds.h "-DD2ORACLE_D2MOO_COMMIT=\"$PINNED_COMMIT\"")
INCLUDES=(-I"$HERE/include" -I"$HERE/src")
for component in D2Common D2CommonDefinitions Fog Storm D2CMP D2Lang D2Hell; do
INCLUDES+=(-I"$D2MOO_SRC/source/$component/include")
@ -75,8 +75,15 @@ for obj in "${d2moo_objs[@]}"; do
fi
done
WRAP_SYMBOL=_Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc
"$CXX" -m32 "${d2moo_objs[@]}" "${oracle_objs[@]}" "-Wl,--wrap=$WRAP_SYMBOL" -o "$WORK/d2moo-oracle"
WRAP_FLAGS=(
-Wl,--wrap=_Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc
-Wl,--wrap=_Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc
-Wl,--wrap=_Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc
-Wl,--wrap=_Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc
-Wl,--wrap=_Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc
-Wl,--wrap=_Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii
)
"$CXX" -m32 "${d2moo_objs[@]}" "${oracle_objs[@]}" "${WRAP_FLAGS[@]}" -o "$WORK/d2moo-oracle"
mkdir -p "$OUT"
cp "$WORK/d2moo-oracle" "$OUT/d2moo-oracle.tmp"

View File

@ -181,3 +181,6 @@ inline void strcat_s(char (&dst)[N], const char* src) {
strncat(dst, src, N - strlen(dst) - 1);
}
#define sprintf_s snprintf
struct D2DrlgLevelLinkDataStrc;
BOOL __fastcall DRLGOUTPLACE_LinkAct5Ubers(D2DrlgLevelLinkDataStrc* pLevelLinkData, int nIteration);

View File

@ -238,14 +238,57 @@ std::string LevelGrid(D2DrlgLevelStrc* pLevel)
} // namespace
extern "C" void __real__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel);
extern "C" void __real__Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel);
extern "C" void __real__Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel);
extern "C" void __real__Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel);
extern "C" D2DrlgMapStrc* __real__Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc(
D2DrlgLevelStrc* pLevel, int nLvlPrestId, D2DrlgCoordStrc* pDrlgCoord, D2SeedStrc* pSeed);
extern "C" BOOL __real__Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii(
D2DrlgLevelStrc* pLevel, int nX, int nY, int nWidth, int nHeight, int dwRoomFlags, int dwOutdoorFlags, int dwOutdoorFlagsEx, int dwDT1Mask);
// Linked with -Wl,--wrap: DRLGOUTDOORS_GenerateLevel's call to DRLGOUTWILD_InitAct1OutdoorLevel lands
// here, so the level grids are captured exactly between InitAct1OutdoorLevel and room creation.
// Linked with -Wl,--wrap: DRLGOUTDOORS_GenerateLevel's call to InitAct{1,2,3,5}OutdoorLevel (and
// for Act 4, where DRLGOUTDOORS_InitAct4OutdoorLevel is in the same TU, the first room creation
// immediately after it) lands here so the level grids are captured between InitAct*OutdoorLevel
// and room creation.
extern "C" void __wrap__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel)
{
__real__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(pLevel);
gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel);
}
extern "C" void __wrap__Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel)
{
__real__Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc(pLevel);
gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel);
}
extern "C" void __wrap__Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel)
{
__real__Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc(pLevel);
gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel);
}
extern "C" void __wrap__Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel)
{
__real__Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc(pLevel);
gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel);
}
extern "C" D2DrlgMapStrc* __wrap__Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc(
D2DrlgLevelStrc* pLevel, int nLvlPrestId, D2DrlgCoordStrc* pDrlgCoord, D2SeedStrc* pSeed)
{
if (pLevel->nDrlgType == DRLGTYPE_OUTDOOR && gLevelGridJson.find(pLevel->nLevelId) == gLevelGridJson.end())
{
gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel);
}
return __real__Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc(pLevel, nLvlPrestId, pDrlgCoord, pSeed);
}
extern "C" BOOL __wrap__Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii(
D2DrlgLevelStrc* pLevel, int nX, int nY, int nWidth, int nHeight, int dwRoomFlags, int dwOutdoorFlags, int dwOutdoorFlagsEx, int dwDT1Mask)
{
if (pLevel->nDrlgType == DRLGTYPE_OUTDOOR && gLevelGridJson.find(pLevel->nLevelId) == gLevelGridJson.end())
{
gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel);
}
return __real__Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii(
pLevel, nX, nY, nWidth, nHeight, dwRoomFlags, dwOutdoorFlags, dwOutdoorFlagsEx, dwDT1Mask);
}
namespace
{
@ -614,10 +657,6 @@ std::string MapTiles(const RefIndex& index, const D2DrlgRoomStrc* pRoom, int nLe
{
D2ORACLE_Fail("level %d room %d: no pTileGrid after DRLGROOMTILE_AddRoomMapTiles", nLevelId, nRoom);
}
if (pTileGrid->pAnimTiles)
{
D2ORACLE_Fail("level %d room %d: animated tiles (pAnimTiles) are not dumped", nLevelId, nRoom);
}
const D2DrlgRoomTilesStrc& t = pTileGrid->pTiles;
std::ostringstream o;
o << "{\"nWalls\":" << pTileGrid->nWalls << ",\"nFloors\":" << pTileGrid->nFloors << ",\"nShadows\":" << pTileGrid->nShadows
@ -645,19 +684,20 @@ std::string InitLevelStage(D2DrlgStrc* pDrlg, int nLevelId, D2DrlgLevelStrc** pp
D2ORACLE_SetTraceContext(szContext);
D2DrlgLevelStrc* pLevel = DRLG_GetLevel(pDrlg, nLevelId);
if (pLevel->nDrlgType != DRLGTYPE_OUTDOOR || DRLG_GetActNoFromLevelId(nLevelId) != ACT_I)
if (pLevel->nDrlgType != DRLGTYPE_OUTDOOR)
{
D2ORACLE_Fail("level %d is not an Act I outdoor level", nLevelId);
D2ORACLE_Fail("level %d is not an outdoor level", nLevelId);
}
if (pLevel->pFirstRoomEx)
{
D2ORACLE_Fail("level %d was already generated before its own generation stage", nLevelId);
}
gLevelGridJson.erase(nLevelId);
DRLG_InitLevel(pLevel);
auto it = gLevelGridJson.find(nLevelId);
if (it == gLevelGridJson.end())
{
D2ORACLE_Fail("level %d: InitAct1OutdoorLevel hook did not fire", nLevelId);
D2ORACLE_Fail("level %d: outdoor levelGrid hook did not fire", nLevelId);
}
std::vector<D2DrlgMapStrc*> maps;
@ -679,14 +719,17 @@ std::string InitLevelStage(D2DrlgStrc* pDrlg, int nLevelId, D2DrlgLevelStrc** pp
return o.str();
}
D2DrlgStrc* AllocAct1(D2DrlgActStrc* pAct, uint32_t nSeed, uint8_t nDifficulty)
D2DrlgStrc* AllocAct(D2DrlgActStrc* pAct, uint8_t nAct, uint32_t nSeed, uint8_t nDifficulty)
{
static const int kTownLevelIds[5] = {
LEVEL_ROGUEENCAMPMENT, LEVEL_LUTGHOLEIN, LEVEL_KURASTDOCKTOWN, LEVEL_THEPANDEMONIUMFORTRESS, LEVEL_HARROGATH,
};
memset(pAct, 0, sizeof(*pAct));
pAct->nAct = ACT_I;
pAct->nAct = nAct;
D2ORACLE_SetTraceContext("drlg");
// D2Game allocates the act with the town level id; DRLG_AllocDrlg lays out the whole act
// (DRLGOUTPLACE_CreateLevelConnections) and generates the town.
return DRLG_AllocDrlg(pAct, ACT_I, nullptr, nSeed, LEVEL_ROGUEENCAMPMENT, 0, nullptr, nDifficulty, nullptr, nullptr);
return DRLG_AllocDrlg(pAct, nAct, nullptr, nSeed, kTownLevelIds[nAct], 0, nullptr, nDifficulty, nullptr, nullptr);
}
std::vector<int> ParseLevels(const char* sz)
@ -710,6 +753,7 @@ int main(int argc, char** argv)
bool bHaveSeed = false;
uint32_t nSeed = 0;
int nDifficulty = 0;
int nExplicitAct = -1;
bool bIsolated = false;
std::vector<int> levels;
for (int i = 1; i < argc; ++i)
@ -722,6 +766,7 @@ int main(int argc, char** argv)
if (arg == "--data") gsD2OracleDataDir = next();
else if (arg == "--seed") { nSeed = (uint32_t)strtoul(next(), nullptr, 0); bHaveSeed = true; }
else if (arg == "--levels") levels = ParseLevels(next());
else if (arg == "--act") nExplicitAct = atoi(next());
else if (arg == "--difficulty") nDifficulty = atoi(next());
else if (arg == "--isolated") bIsolated = true;
else if (arg == "--trace") szTrace = next();
@ -731,7 +776,7 @@ int main(int argc, char** argv)
// Without --levels only the act stage (layout, links, town) is generated and dumped.
if (gsD2OracleDataDir.empty() || !bHaveSeed || !szOut)
{
D2ORACLE_Fail("usage: d2moo-oracle --data <dir> --seed <n> [--levels 2,3] --out <file> [--difficulty n] [--isolated] [--trace file]");
D2ORACLE_Fail("usage: d2moo-oracle --data <dir> --seed <n> [--levels 2,3] [--act 0..4] --out <file> [--difficulty n] [--isolated] [--trace file]");
}
if (nDifficulty < 0 || nDifficulty > 2)
{
@ -744,12 +789,30 @@ int main(int argc, char** argv)
D2ORACLE_OpenTrace(szTrace);
}
uint8_t nAct = ACT_I;
if (nExplicitAct != -1)
{
if (nExplicitAct < 0 || nExplicitAct > 4) D2ORACLE_Fail("act must be 0..4");
nAct = (uint8_t)nExplicitAct;
}
else if (!levels.empty())
{
nAct = DRLG_GetActNoFromLevelId(levels[0]);
}
for (int id : levels)
{
if (DRLG_GetActNoFromLevelId(id) != nAct)
{
D2ORACLE_Fail("level %d belongs to act %d, not act %d", id, (int)DRLG_GetActNoFromLevelId(id), (int)nAct);
}
}
D2DrlgActStrc act = {};
std::ostringstream o;
o << "{\"schema\":3,\"oracle\":{\"d2moo\":\"" << D2ORACLE_D2MOO_COMMIT << "\",\"arch\":\"i386\"},\"gameSeed\":" << nSeed
<< ",\"difficulty\":" << nDifficulty << ",\"isolated\":" << (bIsolated ? "true" : "false");
D2DrlgStrc* pDrlg = AllocAct1(&act, nSeed, (uint8_t)nDifficulty);
D2DrlgStrc* pDrlg = AllocAct(&act, nAct, nSeed, (uint8_t)nDifficulty);
o << ",\"drlg\":{\"startSeed\":" << pDrlg->dwStartSeed << ",\"seed\":" << Seed(pDrlg->pSeed) << "},\"act\":" << Act(pDrlg) << ",\"levels\":[";
std::vector<std::string> heads(levels.size());
std::vector<D2DrlgLevelStrc*> generated(levels.size(), nullptr);
@ -761,7 +824,7 @@ int main(int argc, char** argv)
{
if (i > 0)
{
pDrlg = AllocAct1(&act, nSeed, (uint8_t)nDifficulty);
pDrlg = AllocAct(&act, nAct, nSeed, (uint8_t)nDifficulty);
}
heads[i] = InitLevelStage(pDrlg, levels[i], &generated[i]);
activations[i] = Activate(generated[i]);

View File

@ -336,9 +336,9 @@ BOOL __stdcall D2Common_11099(D2FieldStrc*, D2ActiveRoomStrc*, int, int, uint16_
{
D2ORACLE_Fail("unexpected D2Common_11099");
}
BOOL __fastcall MONSTERS_ValidateMonsterId(int)
BOOL __fastcall MONSTERS_ValidateMonsterId(int nMonsterId)
{
D2ORACLE_Fail("unexpected MONSTERS_ValidateMonsterId (tile preset units are not generated)");
return nMonsterId >= 0 && nMonsterId < sgptDataTables->nMonStatsTxtRecordCount ? nMonsterId : -1;
}
// ---- D2CMP: DT1 tile libraries ----
@ -468,16 +468,13 @@ void D2ORACLE_DescribeTile(const D2TileLibraryEntryStrc* pTileLibraryEntry, std:
*pnIndex = pTile->nIndex;
}
//D2Cmp.#10087: stores the library in the first free slot of the 32-slot array.
//D2Cmp.#10087 (1.13c D2CMP.dll 0x6fe25480): stores the library in the first free slot of the 32-slot
// array without deduplicating (e.g. LvlTypes.txt row 21 Act 3 Jungle lists Bridge.dt1 in slots 0 and 8).
void __stdcall D2CMP_10087_LoadTileLibrarySlot(D2TileLibraryHashStrc** ppTileLibraryHash, const char* szFileName)
{
OracleTileLibrary* pLibrary = LoadTileLibrary(szFileName);
for (int i = 0; i < kTileSlots; ++i)
{
if (ppTileLibraryHash[i] == &pLibrary->tHash)
{
D2ORACLE_Fail("tile library %s loaded twice into the same slot array (D2CMP behaviour unknown)", pLibrary->szPath.c_str());
}
if (!ppTileLibraryHash[i])
{
ppTileLibraryHash[i] = &pLibrary->tHash;