206 lines
7.3 KiB
TypeScript
206 lines
7.3 KiB
TypeScript
/**
|
|
* Unified Offline Asset & Table Baker CLI (`src/baker/cli.ts`).
|
|
*
|
|
* Usage:
|
|
* npm run bake -- <target>
|
|
* npm run bake -- --verify
|
|
*
|
|
* Supported targets:
|
|
* tiles, acts, entities, missiles, overlays, ui, ui-assets, frontend-ui,
|
|
* animdata, char-so, characters, cursor, skills, drop-data, tbl, all
|
|
*/
|
|
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { spawnSync } from 'node:child_process'
|
|
import {
|
|
UNIVERSAL_DT1_PATHS,
|
|
validatePackManifest,
|
|
validateUniversalDt1Libraries,
|
|
} from '../common/pack-contract/index.ts'
|
|
import { cell, loadActTables, resolveLevelLibraries } from '../common/world/acts.ts'
|
|
import { mountNodeArchives } from './mpq-archive-source.ts'
|
|
import { auditDropParity } from './packers/pack-canonical-drop-data.ts'
|
|
|
|
const TARGET_SCRIPTS: Readonly<Record<string, string>> = {
|
|
tiles: 'src/baker/packers/pack-tiles.ts',
|
|
acts: 'src/baker/packers/pack-act-assets.ts',
|
|
entities: 'src/baker/packers/pack-entity-assets.ts',
|
|
missiles: 'src/baker/packers/pack-missiles.ts',
|
|
overlays: 'src/baker/packers/pack-overlays.ts',
|
|
ui: 'src/baker/packers/pack-ui.ts',
|
|
'ui-assets': 'src/baker/packers/pack-ui-assets.ts',
|
|
'frontend-ui': 'src/baker/packers/pack-frontend-ui.ts',
|
|
animdata: 'src/baker/packers/pack-animdata.ts',
|
|
'char-so': 'src/baker/packers/pack-char-so-layers.ts',
|
|
characters: 'src/baker/packers/pack-characters.ts',
|
|
cursor: 'src/baker/packers/pack-cursor.ts',
|
|
skills: 'src/baker/packers/pack-skill-assets.ts',
|
|
'drop-data': 'src/baker/packers/pack-canonical-drop-data.ts',
|
|
tbl: 'src/baker/packers/pack-canonical-tbl.ts',
|
|
manifest: 'src/baker/io/asset-manifest.ts',
|
|
}
|
|
|
|
export interface BakerVerifySummary {
|
|
readonly tileManifestVerified: boolean
|
|
readonly levelsVerified: number
|
|
readonly universalDt1Count: number
|
|
readonly dropParityVerified: boolean
|
|
readonly entityPacksVerified: boolean
|
|
}
|
|
|
|
/**
|
|
* Runs offline contract, universal DT1, 136-level library, drop table, and entity pack verification.
|
|
*/
|
|
export async function runBakerVerify(rootDir: string = process.cwd()): Promise<BakerVerifySummary> {
|
|
console.log('=== Diablo II v1.13c Baker Offline Contract & Pack Verification ===')
|
|
|
|
// 1. Validate tile catalog manifest (`public/act-packs/tiles/index.json`)
|
|
const tileIndexCandidates = [
|
|
join(rootDir, 'public', 'act-packs', 'tiles', 'index.json'),
|
|
join(rootDir, 'samples', 'd2-packs', 'tiles', 'index.json'),
|
|
]
|
|
const tileIndexPath = tileIndexCandidates.find(p => existsSync(p))
|
|
if (!tileIndexPath) {
|
|
throw new Error(`Tile catalog index.json not found in ${tileIndexCandidates.join(' or ')}`)
|
|
}
|
|
const rawTileIndex = JSON.parse(readFileSync(tileIndexPath, 'utf-8')) as import('../common/pack-contract/index.ts').PackedTileCatalogIndex
|
|
const tileCatalog = validatePackManifest(rawTileIndex)
|
|
console.log(
|
|
`✓ Tile catalog verified (${tileIndexPath}): ${tileCatalog.totalLibraries} libraries, ${tileCatalog.totalTiles} tiles, missingFiles=${tileCatalog.missingFiles.length}, invisWalFloorVerified=${String(tileCatalog.invisWalFloorVerified)}`,
|
|
)
|
|
|
|
// 2. Verify all 136 levels resolve universal DT1s (`Blank.dt1`, `InvisWal.dt1`, `Warp.dt1`)
|
|
const archiveDir = existsSync(join(rootDir, 'samples', 'd2', 'd2data.mpq'))
|
|
? join(rootDir, 'samples', 'd2')
|
|
: '/usr/local/google/home/taodao/d2-data'
|
|
const archives = await mountNodeArchives(archiveDir)
|
|
const tables = await loadActTables(archives)
|
|
let levelsVerified = 0
|
|
for (let levelId = 1; levelId <= 136; levelId += 1) {
|
|
const row = tables.levels.rows.find(r => Number(cell(tables.levels, r, 'Id')) === levelId)
|
|
if (!row) {
|
|
throw new Error(`Missing level Id=${levelId} in Levels.txt`)
|
|
}
|
|
const levelTypeId = Number(cell(tables.levels, row, 'LevelType'))
|
|
const libs = resolveLevelLibraries(tables, levelTypeId)
|
|
validateUniversalDt1Libraries(libs, `levelId=${levelId} (levelType=${levelTypeId})`)
|
|
levelsVerified += 1
|
|
}
|
|
console.log(
|
|
`✓ All ${levelsVerified}/136 levels verified to include the ${UNIVERSAL_DT1_PATHS.length} universal DT1s (${UNIVERSAL_DT1_PATHS.join(', ')})`,
|
|
)
|
|
|
|
// 3. Verify 1.13c canonical drop data parity (MonStats, SuperUniques, DifficultyLevels)
|
|
const dropAudit = await auditDropParity(
|
|
archiveDir,
|
|
join(rootDir, 'src', 'common', 'data', 'canonical-drop-data.ts'),
|
|
)
|
|
if (
|
|
!dropAudit.monstatsMatch ||
|
|
!dropAudit.superUniquesMatch ||
|
|
!dropAudit.difficultyLevelsMatch ||
|
|
dropAudit.monsterKindsCount !== 734 ||
|
|
dropAudit.superUniquesCount !== 66 ||
|
|
dropAudit.difficultyLevelsCount !== 3
|
|
) {
|
|
throw new Error(`Drop data parity check failed: ${JSON.stringify(dropAudit)}`)
|
|
}
|
|
console.log(
|
|
`✓ Canonical drop data parity verified: ${dropAudit.monsterKindsCount} MonsterKinds, ${dropAudit.superUniquesCount} SuperUniques, ${dropAudit.difficultyLevelsCount} DifficultyLevels`,
|
|
)
|
|
|
|
// 4. Verify Schema 2 entity packs if present
|
|
let entityPacksVerified = false
|
|
if (existsSync(join(rootDir, 'samples', 'd2-packs', 'entities'))) {
|
|
const res = spawnSync(
|
|
process.execPath,
|
|
['--import', 'tsx', join(rootDir, 'src', 'baker', 'audit', 'verify-entity-packs.ts')],
|
|
{ cwd: rootDir, stdio: 'inherit' },
|
|
)
|
|
if (res.status !== 0) {
|
|
throw new Error(`verify-entity-packs.ts failed with status ${String(res.status)}`)
|
|
}
|
|
entityPacksVerified = true
|
|
}
|
|
|
|
console.log('=== All Baker Offline Verifications Passed (0 errors) ===')
|
|
return {
|
|
tileManifestVerified: true,
|
|
levelsVerified,
|
|
universalDt1Count: UNIVERSAL_DT1_PATHS.length,
|
|
dropParityVerified: true,
|
|
entityPacksVerified,
|
|
}
|
|
}
|
|
|
|
function runSubscript(scriptRelPath: string, extraArgs: readonly string[], rootDir: string): void {
|
|
const scriptPath = join(rootDir, scriptRelPath)
|
|
const res = spawnSync(
|
|
process.execPath,
|
|
['--import', 'tsx', scriptPath, ...extraArgs],
|
|
{ cwd: rootDir, stdio: 'inherit' },
|
|
)
|
|
if (res.status !== 0) {
|
|
throw new Error(`Baker target ${scriptRelPath} exited with code ${String(res.status)}`)
|
|
}
|
|
}
|
|
|
|
export async function runBakerCli(argv: readonly string[] = process.argv.slice(2)): Promise<void> {
|
|
const rootDir = process.cwd()
|
|
if (argv.includes('--verify') || argv[0] === 'verify') {
|
|
await runBakerVerify(rootDir)
|
|
return
|
|
}
|
|
|
|
const target = argv[0]
|
|
const restArgs = argv.slice(1)
|
|
|
|
if (!target || target === '--help' || target === '-h') {
|
|
console.log(
|
|
`Usage: npm run bake -- <target|--verify>\nTargets: ${Object.keys(TARGET_SCRIPTS).join(', ')}, all`,
|
|
)
|
|
return
|
|
}
|
|
|
|
if (target === 'all') {
|
|
for (const key of [
|
|
'animdata',
|
|
'tiles',
|
|
'characters',
|
|
'entities',
|
|
'missiles',
|
|
'overlays',
|
|
'ui',
|
|
'frontend-ui',
|
|
'skills',
|
|
'drop-data',
|
|
'tbl',
|
|
'manifest',
|
|
]) {
|
|
runSubscript(TARGET_SCRIPTS[key]!, restArgs, rootDir)
|
|
}
|
|
return
|
|
}
|
|
|
|
const scriptRel = TARGET_SCRIPTS[target]
|
|
if (!scriptRel) {
|
|
throw new Error(
|
|
`Unknown bake target "${target}". Expected one of: ${Object.keys(TARGET_SCRIPTS).join(', ')}, all, --verify`,
|
|
)
|
|
}
|
|
runSubscript(scriptRel, restArgs, rootDir)
|
|
}
|
|
|
|
const isMain =
|
|
process.argv[1] !== undefined &&
|
|
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
|
|
|
if (isMain) {
|
|
runBakerCli().catch(err => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|
|
}
|