Created
December 2, 2020 01:42
-
-
Save Kruithne/b203936ec49d4cfb70f7af439e5a378f to your computer and use it in GitHub Desktop.
Tile Merge script for wow.export
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Tile Merge Script by Kruithne | |
// Disclaimer: This was written for a very specific purpose and comes with no warranty. | |
// If this explodes, ping @Kruithne in Party Tortollans Discord (wow.export channel). | |
// If this teleports your dog into the vacuum of space, please do not contact me. | |
// Before Usage: | |
// - Make sure you have Node v15.3.0+ installed (https://nodejs.org/en/) | |
// Usage: | |
// 1) Copy the contents of this script to a file called 'tile_merge.js' | |
// 2) Open a command prompt (or PowerShell, etc) in the same directory as the script. | |
// 3) If this is your first time, run "node ./tile_merge.js" (without the quotation marks) followed by "npm install" as instructed. | |
// 4) Run "node ./tile_merge.js" with directories containing tiles to merge as arguments, example: "node ./tile_merge D:\wow.export.out\maps\azeroth" | |
// Notes: | |
// - Tiles must be in "tex_<x>_<y>_<index>.png" format. | |
// - 256 files are not required, but tile count must be even to form a square. | |
// - All tiles must match the size of the first tile as the final size is not pre-calculated by assumed based on the first. | |
const fs = require('fs'); | |
const path = require('path'); | |
const PACKAGE_NAME = 'package.json'; | |
(async () => { | |
// User has never run the script before, write a package file for dependencies. | |
if (!fs.existsSync(PACKAGE_NAME)) { | |
fs.writeFileSync(PACKAGE_NAME, JSON.stringify({ | |
"dependencies": { | |
"jimp": "^0.16.1" | |
} | |
}, null, '\t')); | |
console.log('Boop! First run detected, a package manifest has been written.'); | |
console.log('Run "npm install" in this directory now to install dependencies.'); | |
return; | |
} | |
const Jimp = require('jimp'); | |
const argv = process.argv.splice(2); | |
// User has provided no arguments to the script. | |
if (argv.length === 0) | |
return console.log('No arguments provided. Example syntax: node .\\tile_merge.js D:\\wow.export.out\\maps\\azeroth'); | |
// Iterate over all arguments provided. | |
for (let mapDir of argv) { | |
mapDir = path.resolve(mapDir); | |
// Ensure provided path exists. | |
if (!fs.existsSync(mapDir)) | |
return console.log('Failed: Unable to resolve provided directory: %s', mapDir); | |
// Ensure provided path is a directory. | |
const dirStat = fs.statSync(mapDir); | |
if (!dirStat.isDirectory()) | |
return console.log('Failed: Provided path exists but is not a directory: %s', mapDir); | |
// Retrieve a directory listing and filter it to only include files. | |
const entries = fs.readdirSync(mapDir, { withFileTypes: true }).filter(e => e.isFile()); | |
const tileMap = new Map(); | |
// Iterate files and separate files by ADT ID. | |
for (const entry of entries) { | |
const match = entry.name.match(/tex_(\d+_\d+)_(\d+).png/); | |
if (match !== null) { | |
const adtID = match[1]; | |
const entryPath = path.join(mapDir, entry.name); | |
const tileIndex = parseInt(match[2]); | |
const insertEntry = { entryPath, tileIndex }; | |
const tileMapEntry = tileMap.get(adtID); | |
tileMapEntry ? tileMapEntry.push(insertEntry) : tileMap.set(adtID, [insertEntry]); | |
} | |
} | |
// To ensure tiles write to the correct place, let's sort them quickly. | |
for (const fileList of tileMap.values()) | |
fileList.sort((a, b) => a.tileIndex < b.tileIndex ? -1 : (a.tileIndex > b.tileIndex ? 1 : 0)); | |
for (const [adtID, files] of tileMap) { | |
// To combine tiles in a square, we need an even number of tiles. | |
if (files.length % 2 !== 0) | |
return console.log('Failed: Map tile %s does not have an even number of tiles.', adtID); | |
const tileCount = Math.sqrt(files.length); | |
console.log('Stitching %s (%d files, %d x %d)', adtID, files.length, tileCount, tileCount); | |
let tileIndex = 0; | |
let tileSize = null; | |
let tileCanvas = null; | |
for (let y = 0; y < tileCount; y++) { | |
for (let x = 0; x < tileCount; x++) { | |
const tileEntry = files[tileIndex++]; | |
const tileFile = tileEntry.entryPath; | |
const tileData = await Jimp.read(tileFile); | |
const tileWidth = tileData.bitmap.width; | |
const tileHeight = tileData.bitmap.height; | |
// If tileSize has not yet been set, set to the dimensions of the | |
// first tile. From this we decide the resulting canvas size. | |
if (tileSize === null) { | |
// First tile is not uniform size, that's a problem. | |
if (tileWidth !== tileHeight) | |
return console.log('Failed: Map tile is not a uniform size (%d != %d) @ %s', tileWidth, tileHeight, tileFile); | |
tileSize = tileWidth; | |
// Create a new canvas to fit all of the tiles on. | |
const canvasSize = tileSize * tileCount; | |
tileCanvas = await new Promise((resolve, reject) => { | |
new Jimp(canvasSize, canvasSize, (err, image) => { | |
err ? reject(err) : resolve(image); | |
}); | |
}); | |
} else { | |
// Every tile must match the dimensions of the first since we don't check them all in advance. | |
if (tileWidth !== tileSize || tileHeight !== tileSize) | |
return console.log('Failed: Map tile does not match the expected size (%d, %d != %d, %d) @ %s', tileWidth, tileHeight, tileSize, tileSize, tileFile); | |
} | |
// Composite the tile onto the canvas at the correct location. | |
tileCanvas.composite(tileData, x * tileSize, y * tileSize); | |
} | |
} | |
// All done? Save the canvas. | |
const outputFile = path.join(mapDir, adtID + '_joined.png'); | |
tileCanvas.write(outputFile); | |
console.log('Merged tiles written to %s', outputFile); | |
} | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment