Skip to content

Commit

Permalink
fix(map): override length values appropriately when reading map chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
fallenoak committed Dec 31, 2023
1 parent 2fe4e5b commit a292ed9
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 66 deletions.
Binary file added fixture/map/pvpzone.wdt
Binary file not shown.
Binary file added fixture/map/pvpzone3430.adt
Binary file not shown.
2 changes: 1 addition & 1 deletion src/lib/map/MapArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class MapArea {

// Normalize all MCAL splats into 8-bit depth splats
let splat: Uint8Array;
if (!alpha) {
if (!alpha || alpha.length === 0) {
splat = null;
} else if (this.#layerSplatDepth === 4) {
const rawSplat = new Uint8Array(
Expand Down
73 changes: 73 additions & 0 deletions src/lib/map/io/McnkIo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { IoContext, IoSource, IoType, IoMode, IoStream, openStream } from '@wowserhq/io';
import * as mcnkIo from './mcnk.js';

/**
* An IoType to read MCNK data from ADT files.
*
* While MCNK is nominally a headered TLV format, a number of presumed bugs in Blizzard's authoring
* tools lead to multiple tags with invalid lengths. The game client uses alternative length values
* specified in the header for these tags.
*/
class McnkIo implements IoType {
getSize() {
return 0;
}

#readChunk(stream: IoStream, context: IoContext, header: Record<string, any>) {
const tagValue = mcnkIo.chunkTag.read(stream);

let lengthValue = stream.readUint32Le();
if (tagValue === 'MCAL') {
lengthValue = header.mcalSize - 8;
} else if (tagValue === 'MCLQ') {
lengthValue = header.mclqSize - 8;
} else if (tagValue === 'MCSH') {
lengthValue = header.mcshSize;
}

const valueType = mcnkIo.chunks[tagValue];
const valueBytes = stream.readBytes(lengthValue);

let valueValue = valueBytes;
if (valueType && valueType.read) {
const valueStream = openStream(valueBytes, IoMode.Read);
valueValue = valueType.read(valueStream, context);
}

// If tag is padded, adjust offset accordingly
const padding = mcnkIo.padding[tagValue] ?? 0;
stream.offset += padding;

return {
tag: tagValue,
length: lengthValue,
value: valueValue,
};
}

#readChunks(stream: IoStream, context: IoContext, header: Record<string, any>) {
const chunks = [];

while (!stream.eof) {
chunks.push(this.#readChunk(stream, context, header));
}

return chunks;
}

read(source: IoSource, context: IoContext = {}) {
const stream = openStream(source, IoMode.Read);
const value: Record<string, any> = {};

context.local = null;
context.root = context.root ?? null;

const header = mcnkIo.header.read(source);
value.header = header;
value.data = this.#readChunks(stream, context, header);

return value;
}
}

export default McnkIo;
68 changes: 3 additions & 65 deletions src/lib/map/io/adt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as io from '@wowserhq/io';
import { mver } from './common.js';
import { MAP_CHUNK_VERTEX_COUNT } from '../const.js';
import { IoType } from '@wowserhq/io';
import { mver } from './common.js';
import McnkIo from './McnkIo.js';

const mapChunkInfo = io.struct({
offset: io.uint32le,
Expand All @@ -10,69 +10,7 @@ const mapChunkInfo = io.struct({
padding: io.uint32le,
});

const mapChunkHeader = io.struct({
flags: io.uint32le,
indexX: io.uint32le,
indexY: io.uint32le,
layerCount: io.uint32le,
doodadRefCount: io.uint32le,
mcvtOffset: io.uint32le,
mcnrOffset: io.uint32le,
mclyOffset: io.uint32le,
mcrfOffset: io.uint32le,
mcalOffset: io.uint32le,
mcalSize: io.uint32le,
mcshOffset: io.uint32le,
mcshSize: io.uint32le,
areaId: io.uint32le,
mapObjRefCount: io.uint32le,
holes: io.uint16le,
padding: io.uint16le,
predTex: io.array(io.uint16le, { size: 8 }),
noEffectDoodad: io.array(io.uint8, { size: 8 }),
mcseOffset: io.uint32le,
soundEmitterCount: io.uint32le,
mclqOffset: io.uint32le,
mclqSize: io.uint32le,
position: io.array(io.float32le, { size: 3 }),
mccvOffset: io.uint32le,
padding2: io.array(io.uint32le, { size: 2 }),
});

const mapLayer = io.struct({
textureId: io.uint32le,
properties: io.uint32le,
alphaOffset: io.uint32le,
effectId: io.uint32le,
});

const mcvt = io.typedArray(io.float32le, { size: MAP_CHUNK_VERTEX_COUNT });

const mcly = io.array(mapLayer);

const mcnr = io.typedArray(io.int8, { size: 3 * MAP_CHUNK_VERTEX_COUNT });

const mcnkChunks = {
MCVT: mcvt,
MCLY: mcly,
MCNR: mcnr,
};

const mcnkPadding = {
MCNR: 13,
};

const mcnkChunk = io.tlv(
io.string({ size: 4, reverse: true, terminate: false }),
io.uint32le,
mcnkChunks,
{ padding: mcnkPadding },
);

const mcnk = io.struct({
header: mapChunkHeader,
data: io.array(mcnkChunk),
});
const mcnk = new McnkIo();

const mhdr = io.struct({
flags: io.uint32le,
Expand Down
59 changes: 59 additions & 0 deletions src/lib/map/io/mcnk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as io from '@wowserhq/io';
import { IoType } from '@wowserhq/io';
import { MAP_CHUNK_VERTEX_COUNT } from '../const.js';

const header: IoType = io.struct({
flags: io.uint32le,
indexX: io.uint32le,
indexY: io.uint32le,
layerCount: io.uint32le,
doodadRefCount: io.uint32le,
mcvtOffset: io.uint32le,
mcnrOffset: io.uint32le,
mclyOffset: io.uint32le,
mcrfOffset: io.uint32le,
mcalOffset: io.uint32le,
mcalSize: io.uint32le,
mcshOffset: io.uint32le,
mcshSize: io.uint32le,
areaId: io.uint32le,
mapObjRefCount: io.uint32le,
holes: io.uint16le,
padding: io.uint16le,
predTex: io.array(io.uint16le, { size: 8 }),
noEffectDoodad: io.array(io.uint8, { size: 8 }),
mcseOffset: io.uint32le,
soundEmitterCount: io.uint32le,
mclqOffset: io.uint32le,
mclqSize: io.uint32le,
position: io.array(io.float32le, { size: 3 }),
mccvOffset: io.uint32le,
padding2: io.array(io.uint32le, { size: 2 }),
});

const layer = io.struct({
textureId: io.uint32le,
properties: io.uint32le,
alphaOffset: io.uint32le,
effectId: io.uint32le,
});

const chunkTag: IoType = io.string({ size: 4, reverse: true, terminate: false });

const mcvt = io.typedArray(io.float32le, { size: MAP_CHUNK_VERTEX_COUNT });

const mcly = io.array(layer);

const mcnr = io.typedArray(io.int8, { size: 3 * MAP_CHUNK_VERTEX_COUNT });

const chunks: Record<string, IoType> = {
MCVT: mcvt,
MCLY: mcly,
MCNR: mcnr,
};

const padding = {
MCNR: 13,
};

export { header, chunkTag, chunks, padding };
10 changes: 10 additions & 0 deletions src/spec/map/MapArea.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,15 @@ describe('MapArea', () => {
expect(mapArea.chunks.length).toBe(16 * 16);
});
});

describe('pvpzone', () => {
test('should load map area from valid file', () => {
const map = new Map().load('./fixture/map/pvpzone.wdt');
const mapArea = new MapArea(map.layerSplatDepth).load('./fixture/map/pvpzone3430.adt');

expect(mapArea.version).toBe(18);
expect(mapArea.chunks.length).toBe(16 * 16);
});
});
});
});

0 comments on commit a292ed9

Please sign in to comment.