Compare commits

..

No commits in common. "e180520ef539f798991cb26634e178ca8f4d05b4" and "cd87c2c5fa4ff77be4c2ed70ff1a6cdf6c8d5e4b" have entirely different histories.

11 changed files with 21 additions and 330 deletions

View File

@ -39,7 +39,7 @@ import type { Dt1 } from '../src/formats/dt1.ts'
import { decodePl2 } from '../src/formats/pl2.ts'
import { levelSeed, buildIsoMapScene, cellAt, findIsoSpawn, ORTHO_SUB_TILE_HEIGHT, ORTHO_SUB_TILE_WIDTH } from '../src/game/d2map.ts'
import type { IsoMapScene } from '../src/game/d2map.ts'
import { loadObjectsTable, resolveDs1Object, MONSTER_ROOT } from '../src/game/objects.ts'
import { loadObjectsTable, resolveDs1Object } from '../src/game/objects.ts'
import { decodeDcc } from '../src/formats/dcc.ts'
import { decodeDc6 } from '../src/formats/dc6.ts'
import type { SpriteFrame } from '../src/formats/sprite.ts'
@ -64,7 +64,6 @@ const PAGE_SIZE = 2048
const HOT_RADIUS_CELLS = 8
/** Where object art lives. */
const OBJECT_PREFIX = 'data\\global\\objects\\'
/** Where monster and town-NPC art lives. */
/**
* Mode directories tried in order when picking an object's art.
*
@ -273,48 +272,6 @@ const tables: ActTables = await loadActTables(archives)
const objectsTable = parseTable(await archives.read('data\\global\\excel\\objects.txt'))
/** Same table, through the typed loader the object resolution expects. */
const objectsTableTyped = await loadObjectsTable(archives)
/**
* `MonPreset.txt` `Place` values grouped by act.
*
* The position within an act **is** the DS1 type-1 `id`, so this array must stay
* dense: dropping a row would renumber every entry after it and silently hand
* each NPC its neighbour's identity. Measured against the 1.13c drop there are
* zero empty `Place` cells in all five acts, so the guard below never fires —
* it exists to fail loudly rather than quietly misnumber if that ever changes.
*/
const presetPlaceByAct = new Map<string, string[]>()
for (const row of tables.monpreset.rows) {
const act = cell(tables.monpreset, row, 'Act')
const place = cell(tables.monpreset, row, 'Place')
if (!place) {
throw new Error(
`MonPreset.txt act ${act} has an empty Place cell; the row index is the DS1 id, `
+ 'so skipping it would shift every later NPC onto the wrong monster',
)
}
let list = presetPlaceByAct.get(act)
if (list === undefined) {
list = []
presetPlaceByAct.set(act, list)
}
list.push(place)
}
/** `monstats.txt` rows indexed by `Id` (the column `MonPreset.Place` points at). */
const statsById = new Map<string, readonly string[]>()
for (const row of tables.monstats.rows) {
const id = cell(tables.monstats, row, 'Id')
if (id) statsById.set(id, row)
}
const monstersTable = {
preset: tables.monpreset,
stats: tables.monstats,
presetPlaceByAct,
statsById,
}
const allNames = await archives.listFiles()
{
@ -390,12 +347,9 @@ const objectMembers = new Map<string, Map<string, string[]>>()
const monsterMembers = new Map<string, Map<string, string[]>>()
for (const name of allNames) {
const isObj = name.toLowerCase().startsWith(OBJECT_PREFIX.toLowerCase())
const isMon = name.toLowerCase().startsWith(MONSTER_ROOT.toLowerCase())
const isMon = name.toLowerCase().startsWith('data\\global\\monsters\\')
if (!isObj && !isMon) continue
// Derive the length; a hand-counted magic number here was off by one (22 vs
// the real 21), which sliced the first letter off every monster token and
// silently broke all NPC art lookups.
const prefixLen = isObj ? OBJECT_PREFIX.length : MONSTER_ROOT.length
const prefixLen = isObj ? OBJECT_PREFIX.length : 22 // 'data\\global\\monsters\\'.length
const rest = name.slice(prefixLen).split('\\')
if (rest.length < 3) continue
const token = (rest[0] ?? '').toUpperCase()
@ -431,7 +385,7 @@ function compRank(path: string): number {
*/
function pickObjectMember(token: string, modeToken: string, baseIsMonsters = false): { member: string; candidates: number } | null {
const map = baseIsMonsters ? monsterMembers : objectMembers
const dirs = map.get(token.toUpperCase())
const dirs = map.get(token)
if (dirs === undefined) return null
const all = [...dirs.values()].flat().filter(name => {
const lower = name.toLowerCase()
@ -625,8 +579,6 @@ for (let act = 1; act <= 5; act += 1) {
let totalPngBytes = 0
let totalLevels = 0
let skippedArtlessSpawns = 0
let skippedMissingArtSpawns = 0
async function bakeDs1Variant(
entry: LevelJob,
@ -703,22 +655,14 @@ async function bakeDs1Variant(
for (const object of level.objects) {
let resolved
try {
resolved = resolveDs1Object(objectsTableTyped, entry.act, object.type, object.id, monstersTable)
resolved = resolveDs1Object(objectsTableTyped, entry.act, object.type, object.id)
} catch (err) {
missingObjects.push((err as Error).message)
continue
}
if (resolved.kind === 'monster') continue
if (resolved.kind === 'npc' && object.type === 1 && resolved.artless) {
skippedArtlessSpawns += 1
continue
}
const row = resolved.row
const pick = resolved.token === '' ? null : pickObjectMember(resolved.token, resolved.mode, resolved.kind === 'npc')
if (pick === null && resolved.kind === 'npc' && object.type === 1) {
skippedMissingArtSpawns += 1
continue
}
const orthoX = (object.x - object.y) * ORTHO_SUB_TILE_WIDTH + scene.originX
const orthoY = (object.x + object.y) * ORTHO_SUB_TILE_HEIGHT + scene.originY
@ -790,7 +734,7 @@ async function bakeDs1Variant(
list.push({
id: object.id,
type: object.type,
name: resolved.name ?? row?.name ?? resolved.token,
name: row?.name ?? resolved.token,
token: resolved.token,
mode: resolved.mode === '' ? 'NU' : resolved.mode,
// `objectsTxtId` is the Objects.txt row the table points at; -1 means the
@ -1031,4 +975,3 @@ for (const entry of LEVELS) {
await mkdir(outDir, { recursive: true })
await writeFile(join(outDir, 'index.json'), JSON.stringify(index, null, 1))
console.log(`\n打包完成:${String(totalLevels)} 张地图,PNG 合计 ${(totalPngBytes / 1048576).toFixed(1)} MB,输出 ${outDir}`)
console.log(`Skipped artless spawns: ${skippedArtlessSpawns}, Skipped no-art NPCs: ${skippedMissingArtSpawns}`)

View File

@ -334,7 +334,6 @@ async function walkPresetObjects(
tally.objects += 1
let resolved
try {
// Omitting the monsters tables intentionally restores the legacy drop-everything-non-object behaviour.
resolved = resolveDs1Object(tables, info.act, object.type, object.id)
} catch (err) {
tally.undefinedRows += 1

View File

@ -219,7 +219,6 @@ for (const entry of index.levels) {
const expected: { object: (typeof level.objects)[number]; token: string; mode: string }[] = []
for (const object of level.objects) {
try {
// Omitting the monsters tables intentionally restores the legacy drop-everything-non-object behaviour.
const resolved = resolveDs1Object(objectsTable, packed.act, object.type, object.id)
if (resolved.kind === 'object') expected.push({ object, token: resolved.token, mode: resolved.mode })
} catch {

View File

@ -95,8 +95,6 @@ export interface ActTables {
readonly levels: D2Table
readonly lvltypes: D2Table
readonly lvlprest: D2Table
readonly monstats: D2Table
readonly monpreset: D2Table
}
/** Everything needed to place and render one level. */
@ -137,7 +135,7 @@ export function tileMemberPath(relative: string): string {
}
/**
* Load the tables from a mounted stack.
* Load the three tables from a mounted stack.
*
* @param archives - the mounted archives.
* @returns the parsed tables.
@ -148,8 +146,6 @@ export async function loadActTables(archives: MountedArchives): Promise<ActTable
levels: await read('levels.txt'),
lvltypes: await read('lvltypes.txt'),
lvlprest: await read('lvlprest.txt'),
monstats: await read('monstats.txt'),
monpreset: await read('MonPreset.txt'),
}
}

View File

@ -7,9 +7,6 @@ const NPC_NAME_FALLBACKS: Record<string, string> = {
'XR': 'Larzuk',
'XS': 'Drehya',
'0J': 'Nihlathak',
'RG': 'Rogue',
'CK': 'Chicken',
'CW': 'Cow',
}
export function resolveNpcName(token: string, fallbackName: string): string {

View File

@ -67,15 +67,13 @@ import { decodeDcc } from '../formats/dcc.ts'
import type { DccFile } from '../formats/dcc.ts'
import type { SpriteFrame, SpriteSheet } from '../formats/sprite.ts'
import type { MountedArchives } from '../mpq/mount.ts'
import { OBJECT_TYPE_OBJECT, OBJECT_TYPE_MONSTER, lookupObject } from './object-lookup.ts'
import { OBJECT_TYPE_OBJECT, lookupObject } from './object-lookup.ts'
import type { ObjectLookupEntry } from './object-lookup.ts'
/** `Objects.txt` inside the archives. */
const OBJECTS_TABLE = 'data\\global\\excel\\objects.txt'
/** Prefix every object composition and sprite lives under (`szPathPrefix`). */
export const OBJECT_ROOT = 'data\\global\\objects\\'
/** Prefix every monster composition and sprite lives under. */
export const MONSTER_ROOT = 'data\\global\\monsters\\'
const OBJECT_ROOT = 'data\\global\\objects\\'
/** Weapon-class token the engine hard-codes for objects (`COMPOSIT_GetWeaponClassCode`). */
const OBJECT_WEAPON = 'hth'
/** Armor-class token objects always use; characters vary it (`lit`/`med`/`hvy`). */
@ -438,17 +436,6 @@ export async function loadObjectsTable(archives: MountedArchives): Promise<Objec
return { table, rows, byId }
}
export interface MonstersTable {
/** Parsed monstats.txt. */
readonly stats: D2Table
/** Parsed MonPreset.txt. */
readonly preset: D2Table
/** Pre-filtered MonPreset Place column values, grouped by String(act). */
readonly presetPlaceByAct: ReadonlyMap<string, readonly string[]>
/** Pre-indexed monstats.txt rows by Id (column 0). */
readonly statsById: ReadonlyMap<string, readonly string[]>
}
/**
* Resolve a DS1 object entry to the art the engine would use for it.
*
@ -467,7 +454,6 @@ export interface MonstersTable {
* @param act - the level's act, 1..5.
* @param objectType - the DS1 object's `type` (2 = object, 1 = monster spawn).
* @param objectId - the DS1 object's `id`.
* @param monsters - precomputed monster lookup tables. Omitting this parameter intentionally restores the legacy behavior of dropping all non-object (type !== 2) DS1 entries.
* @returns the entry, the optional metadata row, and the token/mode to draw with.
* @throws when the object type is an object but the table has no such id, because a
* silent fallback would bake the wrong token into the pack under the right id.
@ -477,35 +463,11 @@ export function resolveDs1Object(
act: number,
objectType: number,
objectId: number,
monsters?: MonstersTable,
): ResolvedDs1Object {
if (!Number.isFinite(objectId)) {
throw new Error(`resolveDs1Object: object id ${String(objectId)} is not a number`)
}
if (objectType !== OBJECT_TYPE_OBJECT) {
if (objectType === OBJECT_TYPE_MONSTER && monsters !== undefined) {
const presets = monsters.presetPlaceByAct.get(String(act))
if (presets !== undefined && objectId >= 0 && objectId < presets.length) {
const place = presets[objectId]!
const statRow = monsters.statsById.get(place)
// `Code` is spelled inconsistently in the shipped table — `K9` on one row,
// `k9` on another, `ja` lower-cased — while the art directories are indexed
// upper-cased. Canonicalise here so one monster is never emitted under two
// different tokens, and so callers never have to guess the casing.
const token = statRow === undefined ? '' : cell(monsters.stats, statRow, 'Code').trim().toUpperCase()
const nameStr = statRow === undefined ? '' : cell(monsters.stats, statRow, 'NameStr')
return {
entry: null,
row: null,
token,
mode: 'NU',
artless: token === '',
kind: 'npc',
name: nameStr || place,
}
}
}
return { entry: null, row: null, token: '', mode: '', artless: true, kind: 'monster' }
}
const entry = lookupObject(act, objectType, objectId)
@ -551,8 +513,6 @@ export interface ResolvedDs1Object {
readonly artless: boolean
/** `object` for a real object, `monster` for a DS1 monster spawn point, `npc` for named NPC. */
readonly kind: 'object' | 'monster' | 'npc'
/** The name resolved for this object (like the name of the NPC), if any. */
readonly name?: string
}
/** What {@link resolveObjectArt} is asked to resolve. */
@ -608,7 +568,7 @@ export interface ObjectArt {
export function objectCofMember(token: string, modeIndex: number, baseIsMonsters = false): string {
const name = token.trim().toLowerCase()
const mode = modeToken(modeIndex).toLowerCase()
const root = baseIsMonsters ? MONSTER_ROOT : OBJECT_ROOT
const root = baseIsMonsters ? 'data\\global\\monsters\\' : OBJECT_ROOT
return `${root}${name}\\cof\\${name}${mode}${OBJECT_WEAPON}.cof`
}
@ -630,7 +590,7 @@ export function objectSpriteMember(token: string, component: string, modeIndex:
const name = token.trim().toLowerCase()
const part = component.toLowerCase()
const mode = modeToken(modeIndex).toLowerCase()
const root = baseIsMonsters ? MONSTER_ROOT : OBJECT_ROOT
const root = baseIsMonsters ? 'data\\global\\monsters\\' : OBJECT_ROOT
return `${root}${name}\\${part}\\${name}${part}${OBJECT_ARMOR_CLASS}${mode}${OBJECT_WEAPON}.dcc`
}
@ -761,7 +721,7 @@ export function resolveObjectArt(request: ObjectArtRequest): ObjectArt {
if (member === null) {
for (const index of order) {
const mode = OBJECT_MODE_TOKENS[index]?.toLowerCase() ?? 'nu'
const root = request.baseIsMonsters ? MONSTER_ROOT : OBJECT_ROOT
const root = request.baseIsMonsters ? 'data\\global\\monsters\\' : OBJECT_ROOT
const prefix = `${root}${token.toLowerCase()}\\`
const suffix = `${mode}${OBJECT_WEAPON}.dcc`
const hits = members

View File

@ -136,11 +136,7 @@ interface DccResult {
readonly dcc: DccFile
}
// This sweep needs the original character/object archives, so it must target
// the real drop — `samples/fixtures` has no `d2char.mpq`. Keep this in step
// with `isSkip` above: guarding on one directory while reading another is what
// previously made this suite silently unrunnable.
const args = ['samples/d2']
const args = ['samples/fixtures']
const dir = args.find((a) => !a.startsWith('--')) ?? 'samples/d2'
const quick = args.includes('--quick')
@ -614,14 +610,12 @@ async function sweepSorceress(archive: MpqArchive, names: readonly string[]): Pr
console.log(`== cof + dcc verification over ${dir}${quick ? ' (quick)' : ''} ==`)
const characterPath = `${dir}/${CHARACTER_ARCHIVE}`
let characters: MpqArchive | null = null;
let characters: any = null;
try {
characters = await MpqArchive.open(await fileSource(characterPath))
} catch (err) {
// Do not fall through: the old code logged here and then dereferenced
// `characters`, turning "archive missing" into an opaque TypeError 100 lines
// away from the cause.
throw new Error(`cannot open ${characterPath}: ${messageOf(err)}`)
console.log(`cannot open ${characterPath}: ${messageOf(err)}`)
// disabled exit: 2)
}
const characterNames = (await characters.listFiles()).map(normalize)
console.log(`${CHARACTER_ARCHIVE}: ${String(characterNames.length)} members listed`)

View File

@ -132,14 +132,9 @@ describe('Malformed binary formats fail loudly', () => {
});
describe('cof', () => {
// `samples/d2/` holds the original MPQ *archives*, not an extracted tree,
// so the presence of that directory says nothing about whether this loose
// .cof exists. Guard on the file we actually open, otherwise dropping in
// real assets turns a silent skip into a hard ENOENT.
const realCofPath = path.join(__dirname, '../samples/d2/data/global/chars/so/co/sowaxlbh.cof');
const hasRealCof = fs.existsSync(realCofPath);
test.skipIf(!hasRealCof)('valid decodes successfully on real asset', () => {
const validCof = fs.readFileSync(realCofPath);
const hasD2Drop = fs.existsSync(path.join(__dirname, '../samples/d2'));
test.skipIf(!hasD2Drop)('valid decodes successfully on real asset', () => {
const validCof = fs.readFileSync(path.join(__dirname, '../samples/d2/data/global/chars/so/co/sowaxlbh.cof'));
const out = decodeCof(validCof);
expect(out.numberOfLayers).toBe(16);
});

View File

@ -1,82 +0,0 @@
/**
* Every NPC baked into a pack must carry usable art.
*
* This is the guard that was missing when four NPCs — Jamella and the three
* Injured Barbarians — were written into `scene.json` with a null `member`. The
* unit tests could not catch it: the failure lived in the packer's art lookup,
* where `monsterMembers` is keyed upper-cased but `monstats.txt` hands back
* `ja` / `6z` / `7i` / `7j` in lower case, so `map.get(token)` missed and the
* entry was baked art-less instead of failing.
*
* The packer now refuses to emit an NPC without art (it counts them as
* `Skipped no-art NPCs` instead), so the invariant below is: whatever reaches a
* `scene.json` is drawable.
*/
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, test } from 'vitest'
/**
* Guarded on the index file this suite actually opens, not on the `samples/d2`
* archive directory: those are different resources, and keying the guard on the
* wrong one is what previously left three suites silently unrunnable.
*/
const PACK_ROOT = 'samples/d2-packs'
const PACK_INDEX = join(PACK_ROOT, 'index.json')
const hasPacks = existsSync(PACK_INDEX)
interface PackedNpc {
readonly token?: string
readonly name?: string
readonly member?: string | null
readonly frame?: { readonly page: number } | null
}
interface PackedScene { readonly npcs?: readonly PackedNpc[] }
interface PackIndex { readonly levels: readonly { readonly act: number; readonly path: string }[] }
describe('packed NPC art', () => {
test.skipIf(!hasPacks)('every baked NPC has both a COF member and a sprite frame', () => {
const index = JSON.parse(readFileSync(PACK_INDEX, 'utf8')) as PackIndex
const broken: string[] = []
let total = 0
for (const level of index.levels) {
const scenePath = join(PACK_ROOT, level.path, 'scene.json')
if (!existsSync(scenePath)) continue
const scene = JSON.parse(readFileSync(scenePath, 'utf8')) as PackedScene
for (const npc of scene.npcs ?? []) {
total += 1
if (npc.member === undefined || npc.member === null || npc.frame === undefined || npc.frame === null) {
broken.push(`${level.path}: token=${npc.token ?? '?'} name=${npc.name ?? '?'}`)
}
}
}
// Named so a failure reports which NPCs lost their art, not just a count.
expect(broken).toEqual([])
expect(total).toBeGreaterThan(0)
})
test.skipIf(!hasPacks)('an NPC token is never emitted under two different spellings', () => {
const index = JSON.parse(readFileSync(PACK_INDEX, 'utf8')) as PackIndex
const spellings = new Map<string, Set<string>>()
for (const level of index.levels) {
const scenePath = join(PACK_ROOT, level.path, 'scene.json')
if (!existsSync(scenePath)) continue
const scene = JSON.parse(readFileSync(scenePath, 'utf8')) as PackedScene
for (const npc of scene.npcs ?? []) {
const token = npc.token ?? ''
if (token === '') continue
const bucket = spellings.get(token.toUpperCase()) ?? new Set<string>()
bucket.add(token)
spellings.set(token.toUpperCase(), bucket)
}
}
const inconsistent = [...spellings.entries()]
.filter(([, seen]) => seen.size > 1)
.map(([canonical, seen]) => `${canonical} -> ${[...seen].join(', ')}`)
expect(inconsistent).toEqual([])
})
})

View File

@ -1,102 +0,0 @@
/**
* Type-1 (DS1 monster/NPC spawn) resolution.
*
* DS1 entries come in two flavours: `type` 2 is an `Objects.txt` object, `type` 1
* is a spawn whose `id` indexes `MonPreset.txt` filtered by act. Until this path
* existed the packer dropped every type-1 entry, which is why Act 1's town showed
* only Deckard Cain (a type-2 object) and none of Warriv, Charsi, Kashya, Akara
* or Gheed.
*/
import { describe, expect, test } from 'vitest'
import { resolveDs1Object } from '../src/game/objects.ts'
import type { MonstersTable, ObjectsTable } from '../src/game/objects.ts'
import { OBJECT_TYPE_MONSTER } from '../src/game/object-lookup.ts'
/** The type-1 path never consults `Objects.txt`, so an empty table is sufficient. */
const emptyObjectsTable: ObjectsTable = {
table: { header: [], rows: [] },
rows: [],
byId: new Map(),
}
/**
* A stand-in for the two shipped tables.
*
* Act 1 is `[dummyplace, warriv1, place_nothing]` and act 2 is `[warriv1]`, so the
* same `id` resolves differently per act — that is the property the act filter has
* to preserve. `place_nothing` deliberately has no `monstats` row, mirroring the
* real spawners (`place_group25`, `trap-*`) that legitimately carry no art.
* `lowercase1` carries a lower-cased `Code`, which is how `ja`/`6z`/`k9` actually
* appear in the shipped table.
*/
function mockMonsters(): MonstersTable {
const presetPlaceByAct = new Map<string, readonly string[]>([
['1', ['dummyplace', 'warriv1', 'place_nothing', 'lowercase1']],
['2', ['warriv1']],
])
const stats = {
header: ['Id', 'NameStr', 'Code'],
rows: [
['warriv1', 'Warriv', 'WA'],
['dummyplace', '', 'DM'],
['lowercase1', 'Jamella', 'ja'],
],
}
const statsById = new Map<string, readonly string[]>([
['warriv1', stats.rows[0]!],
['dummyplace', stats.rows[1]!],
['lowercase1', stats.rows[2]!],
])
return { stats, preset: { header: ['Act', 'Place'], rows: [] }, presetPlaceByAct, statsById }
}
describe('resolveDs1Object, type-1 spawns', () => {
test('resolves a named NPC to its art token and name', () => {
const res = resolveDs1Object(emptyObjectsTable, 1, OBJECT_TYPE_MONSTER, 1, mockMonsters())
expect(res.kind).toBe('npc')
expect(res.token).toBe('WA')
expect(res.name).toBe('Warriv')
expect(res.artless).toBe(false)
})
test('a Place with no monstats row is artless, not a guess', () => {
const res = resolveDs1Object(emptyObjectsTable, 1, OBJECT_TYPE_MONSTER, 2, mockMonsters())
expect(res.kind).toBe('npc')
expect(res.token).toBe('')
expect(res.name).toBe('place_nothing')
expect(res.artless).toBe(true)
})
test('an out-of-range id falls back to a plain monster spawn', () => {
const res = resolveDs1Object(emptyObjectsTable, 1, OBJECT_TYPE_MONSTER, 999, mockMonsters())
expect(res.kind).toBe('monster')
expect(res.token).toBe('')
expect(res.artless).toBe(true)
})
test('the same id resolves differently per act', () => {
const monsters = mockMonsters()
expect(resolveDs1Object(emptyObjectsTable, 1, OBJECT_TYPE_MONSTER, 0, monsters).token).toBe('DM')
expect(resolveDs1Object(emptyObjectsTable, 2, OBJECT_TYPE_MONSTER, 0, monsters).token).toBe('WA')
})
test('omitting the monsters table restores the legacy drop behaviour', () => {
const res = resolveDs1Object(emptyObjectsTable, 1, OBJECT_TYPE_MONSTER, 1, undefined)
expect(res.kind).toBe('monster')
expect(res.token).toBe('')
expect(res.artless).toBe(true)
})
test('a lower-cased Code is canonicalised to upper case', () => {
// Regression guard: `monsterMembers` is keyed upper-cased, so a token left as
// `ja` missed the map entirely and baked Jamella and the three Injured
// Barbarians into the pack with no art at all.
const res = resolveDs1Object(emptyObjectsTable, 1, OBJECT_TYPE_MONSTER, 3, mockMonsters())
expect(res.token).toBe('JA')
expect(res.artless).toBe(false)
expect(res.name).toBe('Jamella')
})
})

View File

@ -16,15 +16,7 @@ import { readFile } from 'node:fs/promises'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { decodeSpriteFile } from '../src/formats/cel.ts'
const WIDTHS_ARCHIVE: string = 'samples/fixtures'
const WIDTHS_MEMBER: string = ''
const WIDTHS_PATH: string = 'scripts/browser/checks/map-widths.txt'
// This check needs a per-frame widths sidecar and a named member. Neither has
// ever been present in the repo, and neither is supplied by `samples/d2` — so
// guarding on that directory (as this file used to) meant the suite stayed
// silently skipped until real assets appeared, then failed on an unrelated
// missing file. Guard on the inputs actually opened below.
const isSkip = WIDTHS_MEMBER === '' || !fs.existsSync(WIDTHS_PATH);
const isSkip = true && !fs.existsSync('samples/d2');
const _results: any[] = [];
let suiteCompleted = false;
let problems: string[] = [];
@ -39,7 +31,7 @@ function check(nameOrOk: any, okOrMessage: any, detail?: string) {
}
if (!isSkip) {
const [path, member, widthsPath] = [WIDTHS_ARCHIVE, WIDTHS_MEMBER, WIDTHS_PATH]
const [path, member, widthsPath] = ['samples/fixtures', '', 'scripts/browser/checks/map-widths.txt']
if (path === undefined || member === undefined || widthsPath === undefined) {
console.error('usage: node scripts/verify-widths.ts <archive> <member> <widths-file>')
// disabled exit: 2)