patchwork-mapconverter
Version:
Executable wrapper for https://github.com/ChiefOfGxBxL/WC3MapTranslator
282 lines (244 loc) • 12 kB
text/typescript
import { HexBuffer } from '../HexBuffer';
import { W3Buffer } from '../W3Buffer';
import { type WarResult, type JsonResult } from '../CommonInterfaces';
import { Translator } from './Translator';
import { Terrain } from '../data/Terrain';
function splitLargeArrayIntoWidthArrays(array: unknown[], width: number) {
const rows: unknown[][] = [];
for (let i = 0; i < array.length / width; i++) {
rows.push(array.slice(i * width, (i + 1) * width));
}
return rows;
}
export class TerrainTranslator implements Translator<Terrain> {
private static instance: TerrainTranslator;
private constructor() {}
public static getInstance() {
if (!this.instance) {
this.instance = new this();
}
return this.instance;
}
public static jsonToWar(terrain: Terrain): WarResult {
return this.getInstance().jsonToWar(terrain);
}
public static warToJson(buffer: Buffer): JsonResult<Terrain> {
return this.getInstance().warToJson(buffer);
}
public jsonToWar(terrainJson: Terrain): WarResult {
const outBufferToWar = new HexBuffer();
/*
* Header
*/
outBufferToWar.addChars('W3E!'); // file id
outBufferToWar.addInt(12); // file version
outBufferToWar.addChar(terrainJson.tileset); // base tileset
outBufferToWar.addInt(+terrainJson.customTileset); // 1 = using custom tileset, 0 = not
/*
* Tiles
*/
outBufferToWar.addInt(terrainJson.tilePalette?.length || 0);
terrainJson.tilePalette?.forEach((tile) => {
outBufferToWar.addChars(tile);
});
/*
* Cliffs
*/
outBufferToWar.addInt(terrainJson.cliffTilePalette?.length || 0);
terrainJson.cliffTilePalette?.forEach((cliffTile) => {
outBufferToWar.addChars(cliffTile);
});
/*
* Map size data
*/
outBufferToWar.addInt(terrainJson.map.width + 1);
outBufferToWar.addInt(terrainJson.map.height + 1);
/*
* Map offset
*/
outBufferToWar.addFloat(terrainJson.map.offset.x);
outBufferToWar.addFloat(terrainJson.map.offset.y);
/*
* Tile points
*/
// Partition the terrainJson masks into "chunks" (i.e. rows) of (width+1) length,
// reverse that list of rows (due to vertical flipping), and then write the rows out
const rows = {
groundHeight: splitLargeArrayIntoWidthArrays(terrainJson.groundHeight, terrainJson.map.width + 1) as number[][],
waterHeight: splitLargeArrayIntoWidthArrays(terrainJson.waterHeight, terrainJson.map.width + 1) as number[][],
boundaryFlag: splitLargeArrayIntoWidthArrays(terrainJson.boundaryFlag, terrainJson.map.width + 1) as boolean[][],
flags: splitLargeArrayIntoWidthArrays(terrainJson.flags, terrainJson.map.width + 1) as number[][],
groundTexture: splitLargeArrayIntoWidthArrays(terrainJson.groundTexture, terrainJson.map.width + 1) as number[][],
groundVariation: splitLargeArrayIntoWidthArrays(terrainJson.groundVariation, terrainJson.map.width + 1) as number[][],
cliffVariation: splitLargeArrayIntoWidthArrays(terrainJson.cliffVariation, terrainJson.map.width + 1) as number[][],
cliffTexture: splitLargeArrayIntoWidthArrays(terrainJson.cliffTexture, terrainJson.map.width + 1) as number[][],
layerHeight: splitLargeArrayIntoWidthArrays(terrainJson.layerHeight, terrainJson.map.width + 1) as number[][],
tileset: '',
customTileset: false,
tilePalette: [],
cliffTilePalette: [],
};
rows.groundHeight.reverse();
rows.waterHeight.reverse();
rows.boundaryFlag.reverse();
rows.flags.reverse();
rows.groundTexture.reverse();
rows.groundVariation.reverse();
rows.cliffVariation.reverse();
rows.cliffTexture.reverse();
rows.layerHeight.reverse();
for (let i = 0; i < rows.groundHeight.length; i++) {
for (let j = 0; j < rows.groundHeight[i].length; j++) {
// these bit operations are based off documentation from https://github.com/stijnherfst/HiveWE/wiki/war3map.w3e-Terrain
const groundHeight = rows.groundHeight[i][j];
const waterHeight = rows.waterHeight[i][j];
const boundaryFlag = rows.boundaryFlag[i][j];
const flags = rows.flags[i][j];
const groundTexture = rows.groundTexture[i][j];
const groundVariation = rows.groundVariation[i][j];
const cliffVariation = rows.cliffVariation[i][j];
const cliffTexture = rows.cliffTexture[i][j];
const layerHeight = rows.layerHeight[i][j];
const hasBoundaryFlag = boundaryFlag ? 0x4000 : 0;
outBufferToWar.addShort(groundHeight);
outBufferToWar.addShort(waterHeight | hasBoundaryFlag);
outBufferToWar.addShort((flags << 2) | groundTexture);
outBufferToWar.addByte(groundVariation | cliffVariation);
outBufferToWar.addByte(cliffTexture | layerHeight);
}
}
return {
errors: [],
buffer: outBufferToWar.getBuffer()
};
}
public warToJson(buffer: Buffer): JsonResult<Terrain> {
// create buffer
const result: Terrain = {
tileset: '',
customTileset: false,
tilePalette: [],
cliffTilePalette: [],
map: {
width: 1,
height: 1,
offset: {
x: 0,
y: 0
}
},
groundHeight: [],
waterHeight: [],
boundaryFlag: [],
flags: [],
groundTexture: [],
groundVariation: [],
cliffVariation: [],
cliffTexture: [],
layerHeight: []
};
const outBufferToJSON = new W3Buffer(buffer);
/**
* Header
*/
const w3eHeader = outBufferToJSON.readChars(4); // W3E!
const version = outBufferToJSON.readInt(); // 0C 00 00 00
const tileset = outBufferToJSON.readChars(1); // tileset
const customTileset = (outBufferToJSON.readInt() === 1);
result.tileset = tileset;
result.customTileset = customTileset;
/**
* Tiles
*/
const numTilePalettes = outBufferToJSON.readInt();
const tilePalettes: string[] = [];
for (let i = 0; i < numTilePalettes; i++) {
tilePalettes.push(outBufferToJSON.readChars(4));
}
result.tilePalette = tilePalettes;
/**
* Cliffs
*/
const numCliffTilePalettes = outBufferToJSON.readInt();
const cliffPalettes: string[] = [];
for (let i = 0; i < numCliffTilePalettes; i++) {
const cliffPalette = outBufferToJSON.readChars(4);
cliffPalettes.push(cliffPalette);
}
result.cliffTilePalette = cliffPalettes;
/**
* map dimensions
*/
const width = outBufferToJSON.readInt() - 1;
const height = outBufferToJSON.readInt() - 1;
result.map = { width, height, offset: { x: 0, y: 0 } };
const offsetX = outBufferToJSON.readFloat();
const offsetY = outBufferToJSON.readFloat();
result.map.offset = { x: offsetX, y: offsetY };
/**
* map tiles
*/
const arrGroundHeight: number[] = [];
const arrWaterHeight: number[] = [];
const arrBoundaryFlag: boolean[] = [];
const arrFlags: number[] = [];
const arrGroundTexture: number[] = [];
const arrGroundVariation: number[] = [];
const arrCliffVariation: number[] = [];
const arrCliffTexture: number[] = [];
const arrLayerHeight: number[] = [];
while (!outBufferToJSON.isExhausted()) {
const groundHeight = outBufferToJSON.readShort();
const waterHeightAndBoundary = outBufferToJSON.readShort();
const waterHeight = waterHeightAndBoundary & 32767;
const boundaryFlag = (waterHeightAndBoundary & 0x4000) === 0x4000;
let flags;
let groundTexture;
if (version >= 12){
const flagsAndGroundTexture = outBufferToJSON.readShort();
flags = (flagsAndGroundTexture & 0xFFC0) >> 2;
groundTexture = flagsAndGroundTexture & 0x3F;
} else {
const flagsAndGroundTexture = outBufferToJSON.readByte();
flags = flagsAndGroundTexture & 0xF0;
groundTexture = flagsAndGroundTexture & 0x0F;
}
const groundAndCliffVariation = outBufferToJSON.readByte();
const cliffTextureAndLayerHeight = outBufferToJSON.readByte();
const groundVariation = groundAndCliffVariation & 0xF8;
const cliffVariation = groundAndCliffVariation & 0x07;
const cliffTexture = cliffTextureAndLayerHeight & 0XF0;
const layerHeight = cliffTextureAndLayerHeight & 0x0F;
arrGroundHeight.push(groundHeight);
arrWaterHeight.push(waterHeight);
arrBoundaryFlag.push(boundaryFlag);
arrFlags.push(flags); //TODO: properly parse flags
arrGroundTexture.push(groundTexture);
arrGroundVariation.push(groundVariation);
arrCliffVariation.push(cliffVariation);
arrCliffTexture.push(cliffTexture);
arrLayerHeight.push(layerHeight);
}
function convertArrayOfArraysIntoFlatArray(arr: unknown[][]): unknown {
return arr.reduce((a: unknown[], b: unknown[]) => {
return [...a, ...b];
});
}
// The map was read in "backwards" because wc3 maps have origin (0,0)
// at the bottom left instead of top left as we desire. Flip the rows
// vertically to fix this.
result.groundHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrGroundHeight, result.map.width + 1).reverse()) as number[][];
result.waterHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrWaterHeight, result.map.width + 1).reverse()) as number[][];
result.boundaryFlag = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrBoundaryFlag, result.map.width + 1).reverse()) as boolean[][];
result.flags = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrFlags, result.map.width + 1).reverse()) as number[];
result.groundTexture = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrGroundTexture, result.map.width + 1).reverse()) as number[][];
result.groundVariation = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrGroundVariation, result.map.width + 1).reverse()) as number[][];
result.cliffVariation = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrCliffVariation, result.map.width + 1).reverse()) as number[][];
result.cliffTexture = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrCliffTexture, result.map.width + 1).reverse()) as number[][];
result.layerHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrLayerHeight, result.map.width + 1).reverse()) as number[][];
return {
errors: [],
json: result
};
}
}