From 99cf83d86b6f95252b6f682d4e415a181fbe0e07 Mon Sep 17 00:00:00 2001 From: Mathias Bynens Date: Mon, 6 Jul 2015 18:29:13 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 12 ++ .gitattributes | 2 + .gitignore | 21 +++ LICENSE-MIT.txt | 20 +++ README.md | 33 +++++ bin/cli.js | 73 +++++++++++ package.json | 46 +++++++ src/array-to-marker.js | 26 ++++ src/colors.js | 34 +++++ src/from-maps.js | 261 ++++++++++++++++++++++++++++++++++++++ src/generate-bounds.js | 60 +++++++++ src/handle-sequence.js | 11 ++ src/icons.js | 33 +++++ src/pixel-data-to-map.js | 30 +++++ src/pixel-data-to-path.js | 29 +++++ src/save-canvas-to-png.js | 19 +++ src/to-maps.js | 126 ++++++++++++++++++ src/write-json.js | 13 ++ test/maps/12512115.map | Bin 0 -> 131076 bytes test/maps/12512207.map | Bin 0 -> 131122 bytes test/test.sh | 29 +++++ 21 files changed, 878 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE-MIT.txt create mode 100644 README.md create mode 100755 bin/cli.js create mode 100644 package.json create mode 100644 src/array-to-marker.js create mode 100644 src/colors.js create mode 100644 src/from-maps.js create mode 100644 src/generate-bounds.js create mode 100644 src/handle-sequence.js create mode 100644 src/icons.js create mode 100644 src/pixel-data-to-map.js create mode 100644 src/pixel-data-to-path.js create mode 100644 src/save-canvas-to-png.js create mode 100644 src/to-maps.js create mode 100644 src/write-json.js create mode 100755 test/maps/12512115.map create mode 100755 test/maps/12512207.map create mode 100755 test/test.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dbd9e6b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[{README.md,package.json,.travis.yml}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0a91f75 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Automatically normalize line endings for all text-based files +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e82f0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +Automap +!Automap/.gitignore +Automap-new +data +test/data +test/maps-new + +# Installed npm modules +node_modules + +# Folder view configuration files +.DS_Store +Desktop.ini + +# Thumbnail cache files +._* +Thumbs.db + +# Files that might appear on external disks +.Spotlight-V100 +.Trashes diff --git a/LICENSE-MIT.txt b/LICENSE-MIT.txt new file mode 100644 index 0000000..a41e0a7 --- /dev/null +++ b/LICENSE-MIT.txt @@ -0,0 +1,20 @@ +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cd551e --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# `tibia-maps` CLI + +`tibia-maps` is a command-line utility to convert between binary [Tibia](https://secure.tibia.com/) maps and human-readable forms of the map data. + +## Installation + +**Note:** [io.js](https://iojs.org/en/) is required. + +```sh +npm install -g tibia-maps +``` + +## Usage + +### `*.map` → `*.png` + `*.json` + +To generate PNGs for the maps + pathfinding visualization and JSON for the marker data based on the map files in the `Automap` directory, run: + +```sh +tibia-maps --from-maps=./Automap --output-dir=./data +``` + +The output is saved in the `data` directory. + +### `*.png` + `*.json` → `*.map` + +To generate Tibia-compatible `*.map` files based on the PNGs and JSON files in the `data` directory, run: + +```sh +tibia-maps --from-data=./data --output-dir=./Automap-new +``` + +The output is saved in the `Automap-new` directory. diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..83e405b --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +'use strict'; + +const path = require('path'); + +const argv = require('argh').argv; +const mkdirp = require('mkdirp'); +const rimraf = require('rimraf'); + +const convertFromMaps = require('../src/from-maps.js'); +const convertToMaps = require('../src/to-maps.js'); +const generateBounds = require('../src/generate-bounds.js'); + +const emptyDirectory = function(path) { + return new Promise(function(resolve, reject) { + rimraf(`${path}/*`, function() { + mkdirp(path, function() { + resolve(); + }); + }); + }); +}; + +const main = function() { + if (!argv['from-maps'] && !argv['from-data']) { + console.log('Missing `--from-maps` or `--from-data` flag.'); + return process.exit(1); + } + + if (argv['from-maps'] && argv['from-data']) { + console.log('Cannot use `--from-maps` and `--from-data` at the same time. Pick one.'); + return process.exit(1); + } + + if (argv['from-maps']) { + if (argv['from-maps'] === true) { + console.log('`--from-maps` path not specified. Using the default, i.e. `Automap`.'); + argv['from-maps'] = 'Automap'; + } + const mapsDirectory = path.resolve(argv['from-maps']); + if (!argv['output-dir'] || argv['output-dir'] === true) { + console.log('`--output-dir` path not specified. Using the default, i.e. `data`.'); + argv['output-dir'] = 'data'; + } + const dataDirectory = path.resolve(argv['output-dir']); + emptyDirectory(dataDirectory).then(function() { + return generateBounds(mapsDirectory, dataDirectory); + }).then(function(bounds) { + return convertFromMaps(bounds, mapsDirectory, dataDirectory); + }); + return; + } + + if (argv['from-data']) { + if (argv['from-data'] === true) { + console.log('`--from-data` path not specified. Using the default, i.e. `data`.'); + argv['from-data'] = 'data'; + } + const dataDirectory = path.resolve(argv['from-data']); + if (!argv['output-dir'] || argv['output-dir'] === true) { + console.log('`--output-dir` path not specified. Using the default, i.e. `Automap-new`.'); + argv['output-dir'] = 'Automap-new'; + } + const outputDirectory = path.resolve(argv['output-dir']); + emptyDirectory(outputDirectory).then(function() { + convertToMaps(dataDirectory, outputDirectory); + }); + return; + } +}; + +main(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..95e79a6 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "tibia-maps", + "version": "0.0.0", + "description": "A command-line utility to convert between binary Tibia maps and human-readable forms of the map data.", + "homepage": "https://mths.be/tibiamaps", + "main": "bin/cli.js", + "bin": "bin/cli.js", + "keywords": [ + "data", + "mmorpg", + "tibia", + "tibia-maps" + ], + "license": "MIT", + "author": { + "name": "Mathias Bynens", + "url": "https://mathiasbynens.be/" + }, + "repository": { + "type": "git", + "url": "https://github.com/tibiamaps/tibia-maps-script.git" + }, + "bugs": "https://github.com/tibiamaps/tibia-maps-script/issues", + "files": [ + "LICENSE-MIT.txt", + "bin/", + "src/" + ], + "directories": { + "bin": "bin", + "test": "test" + }, + "scripts": { + "test": "./test/test.sh" + }, + "dependencies": { + "argh": "^0.1.4", + "canvas": "^1.2.3", + "glob": "^5.0.12", + "lodash.padleft": "^3.1.1", + "lodash.range": "^3.0.1", + "mkdirp": "^0.5.1", + "rimraf": "^2.4.1", + "sort-object": "^2.0.2" + } +} diff --git a/src/array-to-marker.js b/src/array-to-marker.js new file mode 100644 index 0000000..fa2a812 --- /dev/null +++ b/src/array-to-marker.js @@ -0,0 +1,26 @@ +'use strict'; + +const icons = require('./icons.js'); + +const arrayToMarkerBuffer = function(array) { + let result = new Buffer(4); + result.writeUIntLE(array.length, 0x0, 4); + for (const marker of array) { + const markerBuffer = new Buffer(14 + marker.description.length); + markerBuffer.writeUInt8(marker.xPosition, 0x0); + markerBuffer.writeUInt8(marker.xTile, 0x1); + markerBuffer.write('\0\0', 0x2, 2, 'utf8'); + markerBuffer.writeUInt8(marker.yPosition, 0x4); + markerBuffer.writeUInt8(marker.yTile, 0x5); + markerBuffer.write('\0\0', 0x6, 2, 'utf8'); + const iconByte = icons.byName[marker.icon]; + console.assert(iconByte != null); + markerBuffer.writeUIntLE(iconByte, 0x8, 4); + markerBuffer.writeUIntLE(marker.description.length, 0xC, 2); + markerBuffer.write(marker.description, 0xE, marker.description.length, 'ascii'); + result = Buffer.concat([result, markerBuffer]); + } + return result; +}; + +module.exports = arrayToMarkerBuffer; diff --git a/src/colors.js b/src/colors.js new file mode 100644 index 0000000..c463df4 --- /dev/null +++ b/src/colors.js @@ -0,0 +1,34 @@ +'use strict'; + +const byByte = { + 0x00: { r: 0, g: 0, b: 0 }, // black (empty) + 0x0C: { r: 0, g: 102, b: 0 }, // dark green (trees) + 0x18: { r: 0, g: 204, b: 0 }, // green (grass) + 0x1E: { r: 0, g: 255, b: 0 }, // light green (old swamp) + 0x28: { r: 51, g: 0, b: 204 }, // blue (old water) + 0x33: { r: 51, g: 102, b: 153 }, // light blue + 0x56: { r: 102, g: 102, b: 102 }, // dark gray (stone/mountains) + 0x72: { r: 153, g: 51, b: 0 }, // dark brown (earth/stalagmites) + 0x79: { r: 153, g: 102, b: 51 }, // brown (earth) + 0x81: { r: 153, g: 153, b: 153 }, // gray (floor) + 0x8C: { r: 153, g: 255, b: 102 }, // light green + 0xB3: { r: 204, g: 255, b: 255 }, // light blue (ice) + 0xBA: { r: 255, g: 51, b: 0 }, // red (city/walls) + 0xC0: { r: 255, g: 102, b: 0 }, // orange (lava) + 0xCF: { r: 255, g: 204, b: 153 }, // beige (sand) + 0xD2: { r: 255, g: 255, b: 0 }, // yellow (ladders/holes/…) + 0xD7: { r: 255, g: 255, b: 255 } // white (snow / target?) +}; + +const byColor = {}; +Object.keys(byByte).forEach(function(key) { + const byteValue = Number(key); + const color = byByte[byteValue]; + const id = `${color.r},${color.g},${color.b}`; + byColor[id] = byteValue; +}); + +module.exports = { + 'byByte': byByte, + 'byColor': byColor +}; diff --git a/src/from-maps.js b/src/from-maps.js new file mode 100644 index 0000000..a3aa51f --- /dev/null +++ b/src/from-maps.js @@ -0,0 +1,261 @@ +'use strict'; + +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +const Canvas = require('canvas'); +const Image = Canvas.Image; +const range = require('lodash.range'); +const sortObject = require('sort-object'); + +const GLOBALS = {}; +const resetContext = function(context, fillStyle) { + context.fillStyle = fillStyle; + context.fillRect(0, 0, GLOBALS.bounds.width, GLOBALS.bounds.height); +}; + +const icons = require('./icons.js'); +const colors = require('./colors.js'); +const writeJSON = require('./write-json.js'); +const saveCanvasToPNG = require('./save-canvas-to-png.js'); +const handleSequence = require('./handle-sequence.js'); + +const mapPixelPalette = {}; +const pixelCanvas = new Canvas(1, 1); +const pixelContext = pixelCanvas.getContext('2d'); +Object.keys(colors.byByte).forEach(function(pixelByte) { + const color = colors.byByte[pixelByte]; + const imageData = pixelContext.createImageData(1, 1); + const data = imageData.data; + data[0] = color.r; + data[1] = color.g; + data[2] = color.b; + data[3] = 0xFF; + mapPixelPalette[pixelByte] = imageData; +}); + +const pathPixelPalette = {}; +for (const pixelByte of range(0, 255 + 1)) { + const component = 0xFF - pixelByte; + const imageData = pixelContext.createImageData(1, 1); + const data = imageData.data; + data[0] = component; + data[1] = component; + data[2] = component; + data[3] = 0xFF; + pathPixelPalette[pixelByte] = imageData; +} + +let markers = {}; +const resetMarkers = function() { + markers = {}; +}; + +const parseMapData = function(buffer, xOffset, yOffset) { + // TODO: instead of passing in xOffset/yOffset and using them in here, + // this function should just render a 256×256px image as ImageData and return + // it. Then the outer function can position it. + // Note: the map data first contains the 256 pixels in the first column, then + // the 256 pixels in the second column, etc. I.e. the pixels go from top to + // bottom, rather than from left to right. + let xIndex = -1; + let bufferIndex = -1; + while (++xIndex < 256) { + let yIndex = -1; + while (++yIndex < 256) { + const pixelByte = buffer[++bufferIndex]; + GLOBALS.mapContext.putImageData( + mapPixelPalette[pixelByte], + xOffset + xIndex, + yOffset + yIndex + ); + } + } +}; + +const parsePathData = function(buffer, xOffset, yOffset) { + // TODO: instead of passing in xOffset/yOffset and using them in here, + // this function should just render a 256×256px image as ImageData and return + // it (or even return an image buffer). Then the outer function can position + // it. + let xIndex = -1; + let bufferIndex = -1; + while (++xIndex < 256) { + let yIndex = -1; + while (++yIndex < 256) { + const pixelByte = buffer[++bufferIndex]; + GLOBALS.pathContext.putImageData( + pathPixelPalette[pixelByte], + xOffset + xIndex, + yOffset + yIndex + ); + } + } +}; + +const parseMarkerData = function(buffer) { + const markers = []; + let index = 0; + // The first 4 bytes indicate the number of markers on the map. + const markerCount = buffer.readUIntLE(index, 4); + index += 4; + // If there are no markers, our work is done here. + if (markerCount == 0) { + return markers; + } + + // For each marker… + while (markers.length < markerCount) { + const marker = {}; + // The first byte is the `x` position. + marker.xPosition = buffer.readUInt8(index++, 1); + // The second byte is the map tile it is in on the `x` axis. + marker.xTile = buffer.readUInt8(index++, 1); + // The next two bytes are blank. + console.assert(index++, 0x0); + console.assert(index++, 0x0); + + // The next byte is the `y` position. + marker.yPosition = buffer.readUInt8(index++, 1); + // The next byte is the map tile it is in on the `y` axis. + marker.yTile = buffer.readUInt8(index++, 1); + // The next two bytes are blank. + console.assert(index++, 0x0); + console.assert(index++, 0x0); + + // The next 4 bytes are the image ID of the marker icon. + const id = buffer.readUIntLE(index, 4); + index += 4; + marker.icon = icons.byID[id]; + + // The next 2 bytes indicate the size of the string that follows. + const descriptionLength = buffer.readUIntLE(index, 2); + index += 2; + + // Read the string, i.e. the marker’s description. Note: adding an in-game + // marker with “Iñtërnâtiônàlizætiøn☃💩” as its description results in + // “Iñtërnâtiônàlizætiøn☃???”, i.e. astral Unicode symbols don’t seem to be + // supported. + const descriptionBuffer = buffer.slice(index, index + descriptionLength); + index += descriptionLength; + marker.description = descriptionBuffer.toString('binary'); + + const sorted = sortObject(marker); + markers.push(sorted); + } + return markers; +}; + +const drawMapSection = function(fileName) { + return new Promise(function(resolve, reject) { + + const id = path.basename(fileName, '.map'); + if (id == '13112807') { + // `13112807.map` is the hacked map TibiaMaps.org file containing + // impossible color values that break the script because it intentionally + // doesn’t support non-standard color values. + // https://i.imgur.com/GPBwhL7.png + resolve(); + return; + } + const x = Number(id.slice(0, 3)); + const xOffset = (x - GLOBALS.bounds.xMin) * 256; + const y = Number(id.slice(3, 6)); + const yOffset = (y - GLOBALS.bounds.yMin) * 256; + const z = Number(id.slice(6, 8)); + + fs.readFile(fileName, function(error, buffer) { + + if (error) { + reject(error); + } + + // The first 0x10000 (256×256) bytes of the map file form the graphical + // portion of the map. Each byte represents a single visible map pixel. + const mapData = buffer.slice(0, 0x10000); + parseMapData(mapData, xOffset, yOffset); // changes global state + + // The next 0x10000 bytes form the map that is used for pathfinding. Each + // of these 256×256 bytes represents the walking speed on a specific tile. + // 0 = unexplored/unknown + // 1–254: the lower the value, the higher your movement speed on that tile + // 255 = non-walkable + const pathData = buffer.slice(0x10000, 0x20000); + parsePathData(pathData, xOffset, yOffset); // changes global state + + // The remaining bytes are map marker data. + const markerData = buffer.slice(0x20000); + if (!markerData.length) { + // In the TibiaMaps.org package, `12712113.map` lacks the 4 null bytes + // at the end to indicate it has no markers. + console.warn(`File with invalid marker data: ${fileName}. Fix:`); + console.log(`printf '\\0\\0\\0\\0' >> ${fileName}`); + } + + const results = parseMarkerData(markerData); + if (results.length) { + markers[id] = results; + } + resolve(); + + }); + + }); +}; + +const renderFloor = function(floorID, mapDirectory, dataDirectory) { + console.log(`Rendering floor ${floorID}…`); + return new Promise(function(resolve, reject) { + const unexplored = colors.byByte['0']; + resetContext( + GLOBALS.mapContext, + `rgb(${unexplored.r}, ${unexplored.g}, ${unexplored.b}` + ); + resetContext(GLOBALS.pathContext, '#000'); + resetMarkers(); + glob(`${mapDirectory}/*${floorID}.map`, function(error, files) { + // Handle all map files for this floor sequentially. + handleSequence(files, drawMapSection).then(function() { + return saveCanvasToPNG( + `${dataDirectory}/floor-${floorID}-map.png`, + GLOBALS.mapCanvas + ); + }).then(function() { + return saveCanvasToPNG( + `${dataDirectory}/floor-${floorID}-path.png`, + GLOBALS.pathCanvas + ); + }).then(function() { + return writeJSON( + `${dataDirectory}/floor-${floorID}-markers.json`, + markers + ); + }).then(function() { + resolve(); + }).catch(function(error) { + console.error(error.stack); + reject(error); + }); + }); + }); +}; + +const convertFromMaps = function(bounds, mapDirectory, dataDirectory) { + GLOBALS.bounds = bounds; + GLOBALS.mapCanvas = new Canvas(bounds.width, bounds.height); + GLOBALS.mapContext = GLOBALS.mapCanvas.getContext('2d'); + GLOBALS.pathCanvas = new Canvas(bounds.width, bounds.height); + GLOBALS.pathContext = GLOBALS.pathCanvas.getContext('2d'); + if (!mapDirectory) { + mapDirectory = 'Automap'; + } + if (!dataDirectory) { + dataDirectory = 'data'; + } + handleSequence(bounds.floorIDs, function(floorID) { + return renderFloor(floorID, mapDirectory, dataDirectory); + }); +}; + +module.exports = convertFromMaps; diff --git a/src/generate-bounds.js b/src/generate-bounds.js new file mode 100644 index 0000000..4820b25 --- /dev/null +++ b/src/generate-bounds.js @@ -0,0 +1,60 @@ +'use strict'; + +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +const padLeft = require('lodash.padleft'); + +const writeJSON = require('./write-json.js'); + +const generateBounds = function(mapsDirectory, dataDirectory) { + return new Promise(function(resolve, reject) { + glob(`${mapsDirectory}/*.map`, function(error, files) { + const bounds = { + 'xMin': +Infinity, + 'xMax': -Infinity, + 'yMin': +Infinity, + 'yMax': -Infinity, + 'zMin': +Infinity, + 'zMax': -Infinity + }; + const floorIDs = []; + for (const file of files) { + const id = path.basename(file); + const x = Number(id.slice(0, 3)); + const y = Number(id.slice(3, 6)); + const z = Number(id.slice(6, 8)); + if (bounds.xMin > x) { + bounds.xMin = x; + } + if (bounds.xMax < x) { + bounds.xMax = x; + } + if (bounds.yMin > y) { + bounds.yMin = y; + } + if (bounds.yMax < y) { + bounds.yMax = y; + } + if (bounds.zMin > z) { + bounds.zMin = z; + } + if (bounds.zMax < z) { + bounds.zMax = z; + } + const floorID = padLeft(z, 2, '0'); + if (floorIDs.indexOf(floorID) == -1) { + floorIDs.push(floorID); + } + } + bounds.width = (1 + bounds.xMax - bounds.xMin) * 256; + bounds.height = (1 + bounds.yMax - bounds.yMin) * 256; + bounds.floorIDs = floorIDs.sort(); + writeJSON(`${dataDirectory}/bounds.json`, bounds); + resolve(bounds); + }); + }); +}; + +module.exports = generateBounds; diff --git a/src/handle-sequence.js b/src/handle-sequence.js new file mode 100644 index 0000000..8b9d0b2 --- /dev/null +++ b/src/handle-sequence.js @@ -0,0 +1,11 @@ +'use strict'; + +const handleSequence = function(array, callback) { + return array.reduce(function(promise, item) { + return promise.then(function() { + return callback(item); + }); + }, Promise.resolve()); +}; + +module.exports = handleSequence; diff --git a/src/icons.js b/src/icons.js new file mode 100644 index 0000000..474e3b1 --- /dev/null +++ b/src/icons.js @@ -0,0 +1,33 @@ +const byID = { + '0': 'checkmark', // green checkmark ✔ + '1': '?', // blue question mark ❓ + '2': '!', // red exclamation mark ❗ + '3': 'star', // orange star 🟊 + '4': 'crossmark', // bright red crossmark ❌ + '5': 'cross', // dark red cross 🕇 + '6': 'mouth', // mouth with red lips 👄 + '7': 'shovel', // shovel ⛏ + '8': 'sword', // sword ⚔ + '9': 'flag', // blue flag ⚑ + '10': 'lock', // golden lock 🔒 + '11': 'bag', // brown bag 👛 + '12': 'skull', // skull 💀 + '13': '$', // green dollar sign 💰💲 + '14': 'red up', // red arrow up ⬆️🔺 + '15': 'red down', // red arrow down ⬇🔻 + '16': 'red right', // red arrow right ➡️ + '17': 'red left', // red arrow left ⬅️ + '18': 'up', // green arrow up ⬆ + '19': 'down' // green arrow down ⬇ +}; + +const byName = {}; +Object.keys(byID).forEach(function(id) { + const name = byID[id]; + byName[name] = id; +}); + +module.exports = { + byID: byID, + byName: byName +}; diff --git a/src/pixel-data-to-map.js b/src/pixel-data-to-map.js new file mode 100644 index 0000000..c1ce456 --- /dev/null +++ b/src/pixel-data-to-map.js @@ -0,0 +1,30 @@ +'use strict'; + +const colors = require('./colors.js'); + +const pixelDataToMapBuffer = function(data) { + const buffer = new Buffer(0x10000); + let bufferIndex = -1; + let xIndex = -1; + while (++xIndex < 256) { + const xOffset = xIndex * 4; + let yIndex = -1; + while (++yIndex < 256) { + const yOffset = yIndex * 256 * 4; + const offset = yOffset + xOffset; + const r = data[offset]; + const g = data[offset + 1]; + const b = data[offset + 2]; + // Discard alpha channel data; it’s always 0xFF anyway. + //const a = data[offset + 3]; + // Get the byte value that corresponds to this color. + const id = `${r},${g},${b}`; + const byteValue = colors.byColor[id]; + console.assert(byteValue != null); + buffer.writeUInt8(byteValue, ++bufferIndex); + } + } + return buffer; +}; + +module.exports = pixelDataToMapBuffer; diff --git a/src/pixel-data-to-path.js b/src/pixel-data-to-path.js new file mode 100644 index 0000000..daaffea --- /dev/null +++ b/src/pixel-data-to-path.js @@ -0,0 +1,29 @@ +'use strict'; + +const pixelDataToPathBuffer = function(data) { + const buffer = new Buffer(0x10000); + let bufferIndex = -1; + let xIndex = -1; + while (++xIndex < 256) { + const xOffset = xIndex * 4; + let yIndex = -1; + while (++yIndex < 256) { + const yOffset = yIndex * 256 * 4; + const offset = yOffset + xOffset; + const r = data[offset]; + const g = data[offset + 1]; + const b = data[offset + 2]; + // Discard alpha channel data; it’s always 0xFF anyway. + //const a = data[offset + 3]; + // Verify that `r, `g`, and `b` are equal. + console.assert(r == g); + console.assert(r == b); + // Get the byte value that corresponds to this color. + const byteValue = 0xFF - r; + buffer.writeUInt8(byteValue, ++bufferIndex); + } + } + return buffer; +}; + +module.exports = pixelDataToPathBuffer; diff --git a/src/save-canvas-to-png.js b/src/save-canvas-to-png.js new file mode 100644 index 0000000..a5c5633 --- /dev/null +++ b/src/save-canvas-to-png.js @@ -0,0 +1,19 @@ +'use strict'; + +const fs = require('fs'); + +const saveCanvasToPNG = function(fileName, canvas) { + return new Promise(function(resolve, reject) { + const writeStream = fs.createWriteStream(fileName); + const pngStream = canvas.pngStream(); + pngStream.on('data', function(chunk) { + writeStream.write(chunk); + }); + pngStream.on('end', function() { + console.log(`${fileName} created successfully.`); + resolve(); + }); + }); +}; + +module.exports = saveCanvasToPNG; diff --git a/src/to-maps.js b/src/to-maps.js new file mode 100644 index 0000000..0b3e52c --- /dev/null +++ b/src/to-maps.js @@ -0,0 +1,126 @@ +'use strict'; + +const fs = require('fs'); + +const Canvas = require('canvas'); +const Image = Canvas.Image; +const padLeft = require('lodash.padleft'); + +const handleSequence = require('./handle-sequence.js'); +const writeJSON = require('./write-json.js'); + +const pixelDataToMapBuffer = require('./pixel-data-to-map.js'); +const pixelDataToPathBuffer = require('./pixel-data-to-path.js'); +const arrayToMarkerBuffer = require('./array-to-marker.js'); + +const globals = {}; + +const RESULTS = {}; +const addResult = function(id, type, result) { + if (!RESULTS[id]) { + RESULTS[id] = {}; + } + const reference = RESULTS[id]; + reference[type] = result; +}; + +const forEachTile = function(map, callback, name, floorID) { + const bounds = globals.bounds; + const image = new Image(); + image.src = map; + globals.context.drawImage(image, 0, 0, bounds.width, bounds.height); + // Extract each 256×256px tile. + let yOffset = 0; + while (yOffset < bounds.height) { + const y = bounds.yMin + (yOffset / 256); + const yID = padLeft(y, 3, '0'); + let xOffset = 0; + while (xOffset < bounds.width) { + const x = bounds.xMin + (xOffset / 256); + const xID = padLeft(x, 3, '0'); + const pixels = globals.context.getImageData(xOffset, yOffset, 256, 256); + const buffer = callback(pixels.data); + const id = `${xID}${yID}${floorID}`; + addResult(id, name, buffer); + xOffset += 256; + } + yOffset += 256; + } +}; + +const createBinaryMap = function(floorID) { + return new Promise(function(resolve, reject) { + fs.readFile(`${globals.dataDirectory}/floor-${floorID}-map.png`, function(error, map) { + if (error) { + throw new Error(error); + } + forEachTile(map, pixelDataToMapBuffer, 'mapBuffer', floorID); + resolve(); + }); + }); +}; + +const createBinaryPath = function(floorID) { + return new Promise(function(resolve, reject) { + fs.readFile(`${globals.dataDirectory}/floor-${floorID}-path.png`, function(error, map) { + if (error) { + throw new Error(error); + } + forEachTile(map, pixelDataToPathBuffer, 'pathBuffer', floorID); + resolve(); + }); + }); +}; + +const createBinaryMarkers = function(floorID) { + return new Promise(function(resolve, reject) { + const data = require(`${globals.dataDirectory}/floor-${floorID}-markers.json`); + Object.keys(data).forEach(function(id) { + const markers = data[id]; + const markerBuffer = arrayToMarkerBuffer(markers); + addResult(id, 'markerBuffer', markerBuffer); + }); + resolve(); + }); +}; + +const convertToMaps = function(dataDirectory, mapsDirectory) { + if (!dataDirectory) { + dataDirectory = 'data'; + } + if (!mapsDirectory) { + mapsDirectory = 'Automap-new'; + } + globals.dataDirectory = dataDirectory; + globals.mapsDirectory = mapsDirectory; + const bounds = JSON.parse(fs.readFileSync(`${dataDirectory}/bounds.json`)); + globals.bounds = bounds; + globals.canvas = new Canvas(bounds.width, bounds.height); + globals.context = globals.canvas.getContext('2d'); + const floorIDs = bounds.floorIDs; + handleSequence(floorIDs, createBinaryMap).then(function() { + return handleSequence(floorIDs, createBinaryPath); + }).then(function() { + return handleSequence(floorIDs, createBinaryMarkers); + }).then(function() { + Object.keys(RESULTS).forEach(function(id) { + const data = RESULTS[id]; + const noMarkersBuffer = new Buffer([0x0, 0x0, 0x0, 0x0]); + const buffer = Buffer.concat([ + data.mapBuffer, + data.pathBuffer, + data.markerBuffer || noMarkersBuffer + ]); + const fileName = `${mapsDirectory}/${id}.map`; + const writeStream = fs.createWriteStream(fileName); + writeStream.write(buffer); + writeStream.end(); + console.log(`${fileName} created successfully.`); + }); + }).catch(function(error) { + console.error(error.stack); + reject(error); + }); +}; + +module.exports = convertToMaps; diff --git a/src/write-json.js b/src/write-json.js new file mode 100644 index 0000000..2c46b87 --- /dev/null +++ b/src/write-json.js @@ -0,0 +1,13 @@ +'use strict'; + +const fs = require('fs'); + +const writeJSON = function(fileName, data) { + const writeStream = fs.createWriteStream(fileName); + const json = JSON.stringify(data, null, '\t'); + writeStream.write(`${json}\n`); + writeStream.end(); + console.log(`${fileName} created successfully.`); +}; + +module.exports = writeJSON; diff --git a/test/maps/12512115.map b/test/maps/12512115.map new file mode 100755 index 0000000000000000000000000000000000000000..6c7107f30d71f70bbb233e68a7d0cc97e3ac6d19 GIT binary patch literal 131076 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 E0xGmc#Q*>R literal 0 HcmV?d00001 diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..29a41fe --- /dev/null +++ b/test/test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# This script checks if the generated map files based on the generated PNG and +# JSON data match the original map files, and calls out any differences. +# Use it after roundtripping, i.e.: +# +# $ tibia-maps --from-maps && tibia-maps --to-maps + +# Enable writing paths relative to the `test` folder. +cd "$(dirname "${BASH_SOURCE}")"; + +npm link > /dev/null; +tibia-maps --from-maps=./maps --output-dir=./data; +tibia-maps --from-data=./data --output-dir=./maps-new; + +for file in maps/*.map; do + f=$(basename "${file}"); + [ -f "maps-new/${f}" ] || echo "Missing file: ${f}"; + expected=$(md5 -q "maps/${f}"); + actual=$(md5 -q "maps-new/${f}"); + if [ "${expected}" != "${actual}" ]; then + echo "MD5 mismatch: ${f}"; + # Show the first few bytes that differ. + cmp -l {maps,maps-new}/"${f}" | \ + gawk '{printf "%08X %02X %02X\n", $1, strtonum(0$2), strtonum(0$3)}' | \ + head -n 5; + exit 1; + fi; +done;