mcdata-to-json
Version:
Node.js app to turn Minecraft .dat and .json files into unified json.
138 lines (126 loc) • 4.63 kB
JavaScript
const path = require("path");
const fs = require("fs");
const NbtTools = require("./NbtTools");
const PATHS = require("./helpers/PathReference").paths;
const logger = require("./helpers/Logger").getLogger();
const mca = require("mca-js");
const nbt = require("nbt");
const { ensureDirSync } = require("./helpers/PathReference");
const { promiseStringify } = require("./helpers/JsonPromises");
const DOMAIN = "MCAParser";
/**
* Used with Array.filter to uniq an array
* @param {*} value
* @param {number} index
* @param {*} self
* @return {boolean}
*/
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
/**
* @param {*} rawfile
* @param {string} filename
* @return {Promise}
*/
function convertChunksFromRegionfile(rawfile, filename) {
return new Promise((resolve, reject) => {
try {
mca.getData(rawfile, 0, 0);
} catch (err) {
logger.warn(`${filename} failed grabbing chunk 0,0. Returning nothing.\n${JSON.stringify(err)}`, {
domain: DOMAIN,
});
return resolve([]);
}
const chunkdata = [];
for (let x = 0; x < 32; x++) {
for (let z = 0; z < 32; z++) {
try {
const chunknbt = mca.getData(rawfile, x, z);
if (chunknbt)
nbt.parse(chunknbt, (err, data) => {
if (err) throw err;
const chunkjson = NbtTools.condenseNbt(data);
if (chunkjson.Level) {
chunkdata.push({
Biomes: chunkjson.Level.Biomes ? chunkjson.Level.Biomes.filter(onlyUnique) : [],
Entities: chunkjson.Level.Entities,
InhabitedTime: chunkjson.Level.InhabitedTime,
LastUpdate: chunkjson.Level.LastUpdate,
Status: chunkjson.Level.Status,
Structures: chunkjson.Level.Structures,
TileEntities: chunkjson.Level.TileEntities,
xPos: chunkjson.Level.xPos,
zPos: chunkjson.Level.zPos,
});
}
});
} catch (err) {
console.log(err);
reject(err);
}
}
}
resolve(chunkdata);
});
}
/**
*
* @param {string} regionDir
*/
function convertRegionDirToJSON(regionDir) {
if (!fs.existsSync(regionDir)) {
logger.warn(`Was given non-existent directory for mcaDirectory ${regionDir}`, { domain: DOMAIN });
return;
}
const conversionPromises = [];
const discoveredworldname = path.basename(path.dirname(regionDir));
const WORLD_MCAJSON_DIR = path.join(PATHS.CACHED_MCA_JSON_DIR, discoveredworldname);
ensureDirSync(WORLD_MCAJSON_DIR);
const files = fs.readdirSync(regionDir).filter((fname) => path.extname(fname) === ".mca");
logger.verbose(`${files.length} MCA files in given ${discoveredworldname}`, { domain: DOMAIN });
logger.silly(`File list:\n${JSON.stringify(files, null, 2)}`, { domain: DOMAIN });
files.map((filename) => {
// Check if we previous created a JSON file for this region. If so, skip!
if (fs.existsSync(path.join(WORLD_MCAJSON_DIR, filename.replace(/.mca/, ".json")))) {
if (
fs.statSync(WORLD_MCAJSON_DIR, filename.replace(/.mca/, ".json")).mtime >
fs.statSync(path.join(regionDir, filename)).mtime
) {
logger.verbose(`The JSON version of ${filename.replace(/.mca/, "")} is up to date.`, { domain: DOMAIN });
return;
}
}
conversionPromises.push(
fs.promises
.readFile(path.join(regionDir, filename))
.then((data) => {
logger.verbose(`Starting MCA ⟶ JSON for ${filename}`, { domain: DOMAIN });
return convertChunksFromRegionfile(data, filename);
})
.then((regionJSON) => {
return new Promise((res) => {
logger.verbose(`Filtering invalid chunks from ${filename} JSON`, { domain: DOMAIN });
res(regionJSON.filter((chunk) => chunk !== null));
});
})
.then(promiseStringify)
.then((stringified) => {
logger.verbose(`Saving JSON for ${filename.replace(/.mca/, "")}`, { domain: DOMAIN });
return fs.promises.writeFile(path.join(WORLD_MCAJSON_DIR, filename.replace(/.mca/, ".json")), stringified);
})
.then(() => {
logger.verbose(`Saved mcajson/${discoveredworldname}/${filename.replace(/.mca/, ".json")}`, {
domain: DOMAIN,
});
})
.catch((err) => {
logger.warn(`Caught error ${JSON.stringify(err)}`, { domain: DOMAIN });
})
);
});
}
module.exports = {
convertRegionDirToJSON,
};