fix(tiles): 参照 1.13c DRLGROOM_LoadDt1Files 补充常驻全局通用 DT1 (InvisWal / Blank / Warp) (Issue #112)
参照 1.13c D2Common.dll (DRLGROOM_LoadDt1Files @ 0x6fdb8400),在加载完 LvlTypes.txt 后无条件追加 3 个全局常驻通用 DT1: - data\global\tiles\Act1\Outdoors\Blank.dt1 - data\global\tiles\Act1\Barracks\InvisWal.dt1 - data\global\tiles\Act1\Barracks\Warp.dt1 其中 InvisWal.dt1 包含 style=49 sequence=7 的地面阻挡瓦片 (walk-blocking collision invisible wall),解决了 Act 5 Hell 1 (125) 和 Pandemonium 3 (135) 等全地图漫游中报 missingTile: floor style=49 sequence=7:0 的问题。 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
370dcc05b1
commit
ed344259a1
|
|
@ -290,6 +290,20 @@ export function tileMemberPath(relative: string): string {
|
|||
return `${TILES}${relative.replaceAll('/', '\\')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal DT1 files unconditionally appended by D2Common.dll
|
||||
* (DRLGROOM_LoadDt1Files @ 0x6fdb8400) after loading LvlTypes.txt.
|
||||
*
|
||||
* Blank.dt1: blank placeholder tiles
|
||||
* InvisWal.dt1: invisible walls, including style=49 sequence=7 floor tile (walk-blocking collision)
|
||||
* Warp.dt1: generic warp tiles
|
||||
*/
|
||||
export const UNIVERSAL_DT1S: readonly string[] = [
|
||||
tileMemberPath('Act1/Outdoors/Blank.dt1'),
|
||||
tileMemberPath('Act1/Barracks/InvisWal.dt1'),
|
||||
tileMemberPath('Act1/Barracks/Warp.dt1'),
|
||||
]
|
||||
|
||||
/**
|
||||
* Load the tables from a mounted stack.
|
||||
*
|
||||
|
|
@ -359,15 +373,7 @@ export function resolveLevel(tables: ActTables, levelId: number, act?: number):
|
|||
const dt1Mask = Number(cell(tables.lvlprest, preset[0]!, 'Dt1Mask'))
|
||||
const animSpeedRaw = parseInt(cell(tables.lvlprest, preset[0]!, 'Animate'), 10) || 0
|
||||
const animSpeed = animSpeedRaw > 0 ? animSpeedRaw : undefined
|
||||
const dt1Names: string[] = []
|
||||
for (let slot = 1; slot <= 32; slot += 1) {
|
||||
const value = cell(tables.lvltypes, typeRow, `File ${String(slot)}`)
|
||||
if (value === '' || value === '0') continue
|
||||
// The mask is indexed by the slot's position in the file list, not by the
|
||||
// position of the surviving entries.
|
||||
if ((dt1Mask & (1 << (slot - 1))) === 0) continue
|
||||
dt1Names.push(tileMemberPath(value))
|
||||
}
|
||||
const { dt1Names } = resolveLevelLibraries(tables, levelId)
|
||||
|
||||
const ds1Names: string[] = []
|
||||
for (const presetRow of preset) {
|
||||
|
|
@ -479,6 +485,11 @@ export function resolveLevelLibraries(tables: ActTables, levelId: number): Level
|
|||
// when assembling Act 5 siege DS1 files which reference temptile.dt1.
|
||||
dt1Names.push(tileMemberPath('Expansion/Siege/temptile.dt1'))
|
||||
}
|
||||
for (const universal of UNIVERSAL_DT1S) {
|
||||
if (!dt1Names.some(existing => existing.toLowerCase() === universal.toLowerCase())) {
|
||||
dt1Names.push(universal)
|
||||
}
|
||||
}
|
||||
return { levelTypeName: cell(tables.lvltypes, typeRow, 'Name'), dt1Names, dt1Mask }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import * as fs from 'node:fs'
|
||||
import {
|
||||
loadActTables,
|
||||
resolveLevel,
|
||||
resolveLevelLibraries,
|
||||
UNIVERSAL_DT1S,
|
||||
} from '../src/game/acts.ts'
|
||||
import type { ActTables, D2Table } from '../src/game/acts.ts'
|
||||
import { buildIsoMapScene } from '../src/game/d2map.ts'
|
||||
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||
import { decodeDt1 } from '../src/formats/dt1.ts'
|
||||
import type { Dt1, Dt1Tile } from '../src/formats/dt1.ts'
|
||||
import { MpqArchive } from '../src/mpq/archive.ts'
|
||||
import { fileSource } from '../src/mpq/file-source.ts'
|
||||
import { MountedArchives } from '../src/mpq/mount.ts'
|
||||
|
||||
function createMockTables(): ActTables {
|
||||
const emptyTable: D2Table = { header: [], rows: [] }
|
||||
const levels: D2Table = {
|
||||
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'],
|
||||
['79', 'Act 3 - Kurast 1', '2', '22', '3', '100', '100', '2', '79', '0', '0'],
|
||||
['125', 'Act 5 - Hell 1', '4', '35', '1', '80', '80', '4', '255', '0', '0'],
|
||||
['135', 'Act 5 - Pandemonium 3', '4', '35', '1', '80', '80', '4', '255', '0', '0'],
|
||||
],
|
||||
}
|
||||
const lvltypes: D2Table = {
|
||||
header: ['Id', 'Name', 'File 1', 'File 2'],
|
||||
rows: [
|
||||
['1', 'Act 1 - Town', 'Act1/Town/Floor.dt1', 'Act1/Town/Walls.dt1'],
|
||||
['22', 'Act 3 - Jungle', 'Act3/Jungle/Floor.dt1', 'Act3/Jungle/Tree.dt1'],
|
||||
['35', 'Act 5 - Lava', 'Act4/Lava/Floor.dt1', 'Act4/Lava/Walls.dt1'],
|
||||
],
|
||||
}
|
||||
const lvlprest: D2Table = {
|
||||
header: ['Def', 'LevelId', 'File1', 'Dt1Mask', 'Animate'],
|
||||
rows: [
|
||||
['1', '1', 'TownN1.ds1', '3', '80'],
|
||||
],
|
||||
}
|
||||
return {
|
||||
levels,
|
||||
lvltypes,
|
||||
lvlprest,
|
||||
monstats: emptyTable,
|
||||
monpreset: emptyTable,
|
||||
monstats2: emptyTable,
|
||||
monlvl: emptyTable,
|
||||
montype: emptyTable,
|
||||
monumod: emptyTable,
|
||||
superuniques: emptyTable,
|
||||
lvlwarp: emptyTable,
|
||||
}
|
||||
}
|
||||
|
||||
function makeSingleTileDs1(style: number, sequence: number, prop1 = 1): Ds1 {
|
||||
const width = 35
|
||||
const height = 25
|
||||
const cells: Ds1Cell[][] = []
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const row: Ds1Cell[] = []
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
if (x === 30 && y === 18) {
|
||||
row.push({
|
||||
walls: [],
|
||||
floors: [{ prop1, style, sequence, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
shadows: [],
|
||||
substitutions: [],
|
||||
})
|
||||
} else {
|
||||
row.push({ walls: [], floors: [], shadows: [], substitutions: [] })
|
||||
}
|
||||
}
|
||||
cells.push(row)
|
||||
}
|
||||
return {
|
||||
version: 18,
|
||||
width,
|
||||
height,
|
||||
act: 5,
|
||||
substitutionType: 0,
|
||||
wallLayers: 0,
|
||||
floorLayers: 1,
|
||||
cells,
|
||||
objects: [],
|
||||
npcPathOffset: null,
|
||||
}
|
||||
}
|
||||
|
||||
function createMockInvisWalDt1(): Dt1 {
|
||||
const dummyFlags = Array.from({ length: 25 }, () => ({
|
||||
blockWalk: true,
|
||||
blockLos: false,
|
||||
blockJump: false,
|
||||
blockPlayerWalk: false,
|
||||
blockLight: false,
|
||||
raw: 1,
|
||||
}))
|
||||
const mockTile: Dt1Tile = {
|
||||
direction: 0,
|
||||
height: -80,
|
||||
width: 160,
|
||||
type: 0, // Floor
|
||||
style: 49,
|
||||
sequence: 7,
|
||||
materialFlags: 0,
|
||||
roofHeight: 0,
|
||||
animated: 0,
|
||||
rarityFrameIndex: 0,
|
||||
subTileFlags: dummyFlags,
|
||||
minBlockY: 0,
|
||||
bitmapHeight: 80,
|
||||
blocks: [],
|
||||
}
|
||||
return {
|
||||
versionMajor: 7,
|
||||
versionMinor: 6,
|
||||
warnings: [],
|
||||
tiles: [mockTile],
|
||||
}
|
||||
}
|
||||
|
||||
describe('Blizzard 1.13c DRLGROOM_LoadDt1Files Universal DT1s (Issue #112)', () => {
|
||||
it('exports UNIVERSAL_DT1S with exact Blizzard paths and casing', () => {
|
||||
expect(UNIVERSAL_DT1S).toEqual([
|
||||
'data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1',
|
||||
'data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1',
|
||||
'data\\global\\tiles\\Act1\\Barracks\\Warp.dt1',
|
||||
])
|
||||
})
|
||||
|
||||
it('unconditionally appends universal DT1s in resolveLevelLibraries for all test levels', () => {
|
||||
const mockTables = createMockTables()
|
||||
for (const levelId of [1, 79, 125, 135]) {
|
||||
const { dt1Names } = resolveLevelLibraries(mockTables, levelId)
|
||||
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1')
|
||||
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
||||
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\Warp.dt1')
|
||||
}
|
||||
})
|
||||
|
||||
it('unconditionally appends universal DT1s in resolveLevel', () => {
|
||||
const mockTables = createMockTables()
|
||||
const levelInfo = resolveLevel(mockTables, 1)
|
||||
expect(levelInfo.dt1Names).toContain('data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1')
|
||||
expect(levelInfo.dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
||||
expect(levelInfo.dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\Warp.dt1')
|
||||
})
|
||||
|
||||
it('recognizes floor style=49 sequence=7 when InvisWal.dt1 is in the pool and flags missing without it', () => {
|
||||
const ds1 = makeSingleTileDs1(49, 7)
|
||||
const sceneWithout = buildIsoMapScene(ds1, [])
|
||||
expect(sceneWithout.missingTiles).toBe(1)
|
||||
expect(sceneWithout.missingRefs[0]).toContain('floor style=49 sequence=7:0 @cell(30,18)')
|
||||
|
||||
const mockDt1 = createMockInvisWalDt1()
|
||||
const sceneWith = buildIsoMapScene(ds1, [mockDt1])
|
||||
expect(sceneWith.missingTiles).toBe(0)
|
||||
expect(sceneWith.floors).toHaveLength(1)
|
||||
expect(sceneWith.floors[0]!.cellX).toBe(30)
|
||||
expect(sceneWith.floors[0]!.cellY).toBe(18)
|
||||
})
|
||||
|
||||
const hasRealData = fs.existsSync('samples/d2/d2data.mpq')
|
||||
it.runIf(hasRealData)('verifies real MPQ tables and InvisWal.dt1 decoding on levels 1, 79, 125, 135', async () => {
|
||||
const archives = new MountedArchives()
|
||||
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
||||
try {
|
||||
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
|
||||
} catch {}
|
||||
}
|
||||
const realTables = await loadActTables(archives)
|
||||
|
||||
for (const levelId of [1, 79, 125, 135]) {
|
||||
const { dt1Names } = resolveLevelLibraries(realTables, levelId)
|
||||
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1')
|
||||
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
||||
expect(dt1Names).toContain('data\\global\\tiles\\Act1\\Barracks\\Warp.dt1')
|
||||
}
|
||||
|
||||
// Decode real InvisWal.dt1 from MPQ and ensure style=49 sequence=7 floor tile is present
|
||||
const invisWalBytes = await archives.read('data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1')
|
||||
const realInvisWal = decodeDt1(invisWalBytes)
|
||||
const match = realInvisWal.tiles.find(t => t.type === 0 && t.style === 49 && t.sequence === 7)
|
||||
expect(match).toBeDefined()
|
||||
expect(match?.style).toBe(49)
|
||||
expect(match?.sequence).toBe(7)
|
||||
|
||||
// Build scene with real InvisWal.dt1
|
||||
const ds1 = makeSingleTileDs1(49, 7)
|
||||
const scene = buildIsoMapScene(ds1, [realInvisWal])
|
||||
expect(scene.missingTiles).toBe(0)
|
||||
expect(scene.floors).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue