import { describe, expect, it } from 'vitest' import { getMissileTxtData } from '../src/server/skills/skills.ts' import { loadAutoMagic, parseAutoMagicTable } from '../src/common/items/automagic.ts' import { filterByDt1Mask } from '../src/common/world/d2map.ts' import { loadDt1sForLevel, resolveLevel, resolveLevelLibraries, type ActTables, type D2Table } from '../src/common/world/acts.ts' import { discoverSkillModules, listRegisteredSkillModules } from '../src/server/skills/registry.ts' import type { SkillModule } from '../src/server/skills/types.ts' import { MONSTER_TABLE, monsterStatsOf } from '../src/common/units/monsters-data.ts' import { parseNetMonsterStats } from '../src/server/engine/state-hash.ts' import { RAW_MONSTATS } from '../src/common/data/canonical-drop-data.ts' import { Rng } from '../src/common/rng/d2-rng.ts' import type { MountedArchives } from '../src/common/mpq/mount.ts' import { getSharedMountedArchives, getSharedDataRegistry, resetSharedDataRegistryCache, } from '../src/baker/mpq-archive-source.ts' describe('P0 #524 — 1.13c Parity & Fail-Fast Invariants', () => { it('throws an explicit Error when getMissileTxtData is called with an unknown missile name', () => { expect(() => getMissileTxtData('nonexistent_missile_xyz')).toThrow( /Unknown missile "nonexistent_missile_xyz" in Missiles\.txt/, ) const icebolt = getMissileTxtData('icebolt') expect(icebolt.vel).toBeGreaterThan(0) }) it('throws an explicit Error when AutoMagic.txt fails to load or has malformed header', async () => { expect(() => parseAutoMagicTable('not\ta\tvalid\theader\n1\t2\t3\t4')).toThrow( /Invalid AutoMagic\.txt header/, ) const brokenArchives = { has: () => true, read: async () => { throw new Error('corrupt MPQ sector') }, } as unknown as MountedArchives await expect(loadAutoMagic(brokenArchives)).rejects.toThrow(/corrupt MPQ sector/) }) it('filterByDt1Mask returns an empty array instead of falling back to unmasked candidates', () => { const candidates = [ { library: 0, tileIndex: 0 } as any, { library: 1, tileIndex: 1 } as any, ] // Mask bit 2 (0b100 = 4) excludes library 0 and library 1 const filtered = filterByDt1Mask(candidates, 0b100) expect(filtered).toEqual([]) }) it('loadDt1sForLevel and resolveLevel record the actual loaded DT1 bitmask in Level.dt1Mask', () => { const emptyTable: D2Table = { header: [], rows: [] } const tables: ActTables = { levels: { header: ['Id', 'Name', 'Pal', 'LevelType', 'DrlgType', 'SizeX', 'SizeY', 'Act', 'Waypoint', 'Position', 'Portal'], rows: [['1', 'Act 1 - Town', '0', '1', '2', '56', '40', '0', '255', '0', '0']], }, lvltypes: { header: ['Id', 'Name', 'File 1', 'File 2', 'File 3', 'File 4'], // File 1 (bit 0) and File 3 (bit 2) are populated; File 2 is '0' and File 4 is '' rows: [['1', 'Act 1 - Town', 'Act1/Town/Floor.dt1', '0', 'Act1/Town/Objects.dt1', '']], }, lvlprest: { header: ['Def', 'LevelId', 'File1', 'Dt1Mask', 'Animate'], // Preset mask has bits 0, 1, 2, 3 set (15), but only slots 1 and 3 exist in LvlTypes.txt -> loaded mask = 0b0101 = 5 rows: [['1', '1', 'TownN1.ds1', '15', '80']], }, monstats: emptyTable, monpreset: emptyTable, monstats2: emptyTable, monlvl: emptyTable, montype: emptyTable, monumod: emptyTable, superuniques: emptyTable, lvlwarp: emptyTable, } const loaded = loadDt1sForLevel(tables, 1) expect(loaded.dt1Mask).toBe(0b0101) const level = resolveLevel(tables, 1) expect(level.dt1Mask).toBe(0b0101) const libs = resolveLevelLibraries(tables, 1) expect(libs.dt1Mask).toBe(0b0101) }) it('discoverSkillModules fails fast when 0 skill modules are discovered', () => { expect(listRegisteredSkillModules().length).toBeGreaterThanOrEqual(221) const emptyMap = new Map() expect(() => discoverSkillModules(() => ({}), emptyMap)).toThrow( /zero skill modules discovered/, ) }) it('MONSTER_TABLE and parseNetMonsterStats read 255-column MonStats.txt velocity and stats accurately', () => { const fallen = MONSTER_TABLE.get('fallen1') ?? MONSTER_TABLE.fallen1 expect(fallen).toBeDefined() expect(fallen!.velocity).toBe(5) expect(fallen!.runVelocity).toBe(5) const zombie = MONSTER_TABLE.get('zombie1') ?? MONSTER_TABLE.zombie1 expect(zombie).toBeDefined() expect(zombie!.velocity).toBe(1) expect(zombie!.runVelocity).toBe(3) const stats = monsterStatsOf(fallen!, new Rng(12345), 180) expect(stats.speed).toBeGreaterThan(0) const netStats = parseNetMonsterStats(RAW_MONSTATS) expect(netStats.length).toBeGreaterThan(100) const netFallen = netStats.find(m => m.id === 'fallen1') expect(netFallen).toBeDefined() expect(netFallen!.hp).toBeGreaterThanOrEqual(21) }) it('resets cachedMountedPromise and cachedRegistryPromise on rejection in data-registry.ts', async () => { resetSharedDataRegistryCache() await expect(getSharedMountedArchives('/nonexistent/d2/mpq/path/xyz')).rejects.toThrow() await expect(getSharedDataRegistry('/nonexistent/d2/mpq/path/xyz')).rejects.toThrow() // Verify a subsequent call is not stuck on a stale rejected promise with a different path await expect(getSharedMountedArchives('/another/nonexistent/path/abc')).rejects.toThrow(/abc/) resetSharedDataRegistryCache() }) })